You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TLS endpoints conflate abrupt TCP EOF with authenticated close_notify #173
ClientTls and RemoterTls wrap sockets without overriding Python's default suppress_ragged_eofs=True. An abrupt TCP EOF can therefore be converted into an ordinary empty TLS read:
That makes four materially different outcomes difficult or impossible to distinguish:
authenticated peer shutdown through TLS close_notify;
truncated TLS transport caused by abrupt TCP EOF;
retryable nonblocking WANT state; and
fatal TLS or socket failure.
The endpoint consequently cannot report whether the protected stream ended authentically, should be retried later, or failed.
Background
TLS 1.2 and TLS 1.3 agree that close_notify authenticates the end of one peer's protected data, but they differ in what the recipient must do next.
TLS 1.2 requires the recipient to send its own close_notify, close immediately, and discard pending writes. TLS 1.3 removes that reciprocal-close requirement: peer close_notify ends the peer's write direction without closing the local write direction.
This issue owns truthful classification of the receive outcome. The negotiated-version policy applied after authenticated closure is tracked by #177.
Python defaults to suppressing ragged EOF for compatibility. With suppression enabled, an unexpected underlying EOF becomes b"" instead of ssl.SSLEOFError. That default is an interoperability choice, not proof that TLS ended cleanly. HIO should expose the distinction first; an application may then explicitly tolerate missing close_notify when its own authenticated framing proves completeness.
Reproduction
Establish TLS between a HIO client and server endpoint.
Complete the handshake.
Close one underlying socket without sending close_notify.
Service receives on the peer.
Observe that current main reports an ordinary empty read rather than retained TLS truncation.
A deterministic regression can mock SSLContext.wrap_socket(), require suppress_ragged_eofs=False, and make recv() raise SSLEOFError. Equivalent coverage is required for both endpoint roles.
Required behavior
SSLWantReadError, SSLWantWriteError, EAGAIN, and EWOULDBLOCK remain retryable and do not change terminal state.
SSLZeroReturnError, or an authenticated empty TLS read, records peer close_notify and delegates to negotiated-version policy.
Under TLS 1.3, authenticated peer close ends local receive while transmit remains available.
Under TLS 1.2, authenticated peer close begins reciprocal shutdown and prevents further application transmission.
SSLEOFError reports truncation, retains the original exception, closes both directions, and force-closes the transport.
Fatal SSL and connection-wide socket failures likewise retain and surface their cause and force-close.
Unsent output remains inspectable on terminal paths.
Proposed direction
Disable ragged-EOF suppression for client and server wrapping:
Classify typed TLS exceptions before generic OSError. Authenticated closure should enter a shared handler whose version-specific behavior is defined by #177; abrupt EOF and fatal transport failures must never pass through that clean-close path.
Security boundary
A ragged EOF is not automatically an exploitable truncation: an application protocol may authenticate message length or otherwise prove that its final record is complete. The transport still must not erase the event. Reporting abrupt EOF as clean TLS shutdown prevents the application from applying its own policy and makes security-sensitive and compatibility-tolerant callers indistinguishable. The default endpoint contract should therefore report the authenticated transport fact without silently choosing application tolerance.
Problem
ClientTlsandRemoterTlswrap sockets without overriding Python's defaultsuppress_ragged_eofs=True. An abrupt TCP EOF can therefore be converted into an ordinary empty TLS read:That makes four materially different outcomes difficult or impossible to distinguish:
close_notify;The endpoint consequently cannot report whether the protected stream ended authentically, should be retried later, or failed.
Background
TLS 1.2 and TLS 1.3 agree that
close_notifyauthenticates the end of one peer's protected data, but they differ in what the recipient must do next.TLS 1.2 requires the recipient to send its own
close_notify, close immediately, and discard pending writes. TLS 1.3 removes that reciprocal-close requirement: peerclose_notifyends the peer's write direction without closing the local write direction.This issue owns truthful classification of the receive outcome. The negotiated-version policy applied after authenticated closure is tracked by #177.
Python defaults to suppressing ragged EOF for compatibility. With suppression enabled, an unexpected underlying EOF becomes
b""instead ofssl.SSLEOFError. That default is an interoperability choice, not proof that TLS ended cleanly. HIO should expose the distinction first; an application may then explicitly tolerate missingclose_notifywhen its own authenticated framing proves completeness.Reproduction
close_notify.mainreports an ordinary empty read rather than retained TLS truncation.A deterministic regression can mock
SSLContext.wrap_socket(), requiresuppress_ragged_eofs=False, and makerecv()raiseSSLEOFError. Equivalent coverage is required for both endpoint roles.Required behavior
SSLWantReadError,SSLWantWriteError,EAGAIN, andEWOULDBLOCKremain retryable and do not change terminal state.SSLZeroReturnError, or an authenticated empty TLS read, records peerclose_notifyand delegates to negotiated-version policy.SSLEOFErrorreports truncation, retains the original exception, closes both directions, and force-closes the transport.Proposed direction
Disable ragged-EOF suppression for client and server wrapping:
Classify typed TLS exceptions before generic
OSError. Authenticated closure should enter a shared handler whose version-specific behavior is defined by #177; abrupt EOF and fatal transport failures must never pass through that clean-close path.Security boundary
A ragged EOF is not automatically an exploitable truncation: an application protocol may authenticate message length or otherwise prove that its final record is complete. The transport still must not erase the event. Reporting abrupt EOF as clean TLS shutdown prevents the application from applying its own policy and makes security-sensitive and compatibility-tolerant callers indistinguishable. The default endpoint contract should therefore report the authenticated transport fact without silently choosing application tolerance.
Related work
SSLSocket.unwrap().Acceptance criteria