Skip to content

TLS streams are not guaranteed to be "split" (full-duplex) safe #40

Description

@Matthias247

Currently every TLS stream can be split into a reading half and a writing half using tokio::util::split. This operation is however not always guaranteed to be correct and safe to perform:

The reason here is that with TLS reading and writing on a stream are not guaranteed to be decoupled, since TLS streams also transfer control data besides application data. Due to this, performing a read operation on the TLS stream might trigger a write operation on the socket (e.g. for sending a key update or alert), and the other way around.

This property can break assumptions that tokio/tokio-tls and the libraries make at the moment. As one example:

  • The user performs a read, which triggers an alert to be sent to the peer
  • The TLS library performs a write on the underlying socket, which report a blocked status. It forwards that blocked status to the application.
  • The task yields and waits to get woken up based on read readiness. Which might not happen, since the task which last registered for write readiness might be woken up. So the reading half would be stuck.
  • The last point happens if the read task doesn't install its Waker for write readiness. If it instead identifies being write blocked, and overwrites the write Waker the situation is not necessarily better. Now a concurrent write operation might instead be starved, since the Waker is gone.

What will exactly happen or won't happen is a bit of a property of the TLS library, and might therefore be handled completely different by rustls, openssl, schannel and security-framework. Therefore it's rather hard to describe what exactly "could go wrong" if someone tries to split a TLS stream.

With rustls reading and writing onto actual sockets is currently purely handled by tokio-tls, therefore its the most easy thing to grasp. Here any read or write operations in the wrapper simply don't seem to care whether rustls wants to perform IO in the opposite direction. That might not lead to Waker stealing and tasks getting starved. However I guess it could lead to a delay in sending certain updates. @ctz might know more whether this is problematic.

With native-tls + openssl it is very likely that it will perform IO in the opposite direction and handle readiness notifications wrong. https://dzone.com/articles/using-openssl-with-libuv provides a bit of information what needs to be done to handle those cases, but I think native-tls doesn't since it purely forwards all TLS calls to openssl which uses the fd to perform IO. From there on things rely on mio to report readiness, which only cares about the sockets read/write state and not the TLS streams read/write state.

How can this be fixed

It's unfortunately not easy. To avoid people running into starved streams, one solution is to get rid of tokio::util::split and hightlight in docs that streams support only one common Waker for both read and write operations. But that's not making streams full duplex.

I think to really enable full-duplex the following things could be done:

  • Make sure the TLS libraries don't directly perform IO, they just get fed incoming data, and buffer outgoing data (or write it to a buffered writer). If that buffer is full they exercise backpressure on the caller. That is kind of what rustls is already doing.
  • Besides the applications read and write task, there is a TLS IO task which makes sure that if there is any buffered outgoing data (either produced via a read or write operation) that data gets written to a socket. Only that task deals with OS readiness notifications. If sufficient data had been flushed, it wakes up the potentially blocked application write task. If new data from the socket had been received and decoded via the TLS library, it wakes up the read task.

This is kind of also the model that other multiplexing solutions - like HTTP/2 - use in their implementations. The downside is obviously the need for spawning as task, and potential overhead of task-switching which can degrade performance - especially if a multithreaded runtime is used, which would require synchronization between the application and TLS IO task.
It also makes the solution less runtime agnostic, and harder to force-cancel ongoing IO.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions