Skip to content

connection: harden shutdown, EOF, and concurrent I/O handling - #56

Merged
ianic merged 1 commit into
ianic:zig-0.16.xfrom
lalinsky:connection-shutdown-safety
Aug 27, 2026
Merged

ianic merged 1 commit into
ianic:zig-0.16.xfrom
lalinsky:connection-shutdown-safety

Conversation

@lalinsky

@lalinsky lalinsky commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

This is a big change, I'm sorry, but there is no way to split it further. My main motivations are

  1. explicit error sets
  2. reliable shutdown/close/EOF handling (now it matches the Go TLS client)
  3. concurrent usage of the reader and writer

I spent a lot of time thinking about the reader.err and writer.err fields. For the transport errors, when the source stream fails, there are three main options for how to handle, keep the err as null (std.crypto.tls.Client does this), store ReadFailed in err (std.compress does this, but not consistently), or rename it and store e.g. TransportReadFailed in err. I originally went for TransportReadFailed, to make it super obvious, but then chickened out and went back to ReadFailed. I'm hoping the interface will change in the future, but if not, this is a good enough pattern. I'm also open to making it null, to make it compatible with std.crypto.tls and makes the user code a bit nicer.

https://ziggit.dev/t/making-it-harder-to-swallow-error-canceled/16556/58?u=lalinsky

Details from the commit message:


Replace the close-notify flags with an explicit connection state machine. The previous boolean and default alert value could not reliably distinguish a clean shutdown, a pending fatal alert, and a failed connection, and the shared state was unsafe when a reader and writer ran concurrently.

Declare precise read and write error sets so Reader.err and Writer.err remain switchable, while ReadFailed and WriteFailed continue to expose failures from the underlying transport.

Distinguish EOF at a TLS record boundary from EOF in the middle of a record. The latter is always TlsConnectionTruncated. Record-boundary EOF is accepted by default because it is common among otherwise compatible servers, while the new strict_close_notify option lets callers require a proper TLS shutdown when their protocol needs truncation protection.

Make close idempotent, defer read-side fatal alerts to the writer, reject writes after terminal failure, and treat user_canceled as a warning rather than a shutdown. Support one concurrent reader and writer by making state transitions atomic and limiting TLS 1.3 key updates to their respective cipher direction.

Keep incomplete input and insufficient output space retriable in the nonblocking API. Partial trailing records are left for the next decrypt call, and key-update requests are claimed only once their response is certain to fit.

Update examples to expose underlying I/O errors and add tests for shutdown orderings, truncation, partial records, key updates, and reader/writer races, including ThreadSanitizer coverage.

Replace the close-notify flags with an explicit connection state machine. The previous boolean and default alert value could not reliably distinguish a clean shutdown, a pending fatal alert, and a failed connection, and the shared state was unsafe when a reader and writer ran concurrently.

Declare precise read and write error sets so Reader.err and Writer.err remain switchable, while ReadFailed and WriteFailed continue to expose failures from the underlying transport.

Distinguish EOF at a TLS record boundary from EOF in the middle of a record. The latter is always TlsConnectionTruncated. Record-boundary EOF is accepted by default because it is common among otherwise compatible servers, while the new strict_close_notify option lets callers require a proper TLS shutdown when their protocol needs truncation protection.

Make close idempotent, defer read-side fatal alerts to the writer, reject writes after terminal failure, and treat user_canceled as a warning rather than a shutdown. Support one concurrent reader and writer by making state transitions atomic and limiting TLS 1.3 key updates to their respective cipher direction.

Keep incomplete input and insufficient output space retriable in the nonblocking API. Partial trailing records are left for the next decrypt call, and key-update requests are claimed only once their response is certain to fit.

Update examples to expose underlying I/O errors and add tests for shutdown orderings, truncation, partial records, key updates, and reader/writer races, including ThreadSanitizer coverage.
@ianic
ianic merged commit 2dd6164 into ianic:zig-0.16.x Aug 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants