@@ -1164,21 +1164,20 @@ fn handshake_write_loop(
11641164/// TLS record header size (content_type + version + length).
11651165const TLS_RECORD_HEADER_SIZE : usize = 5 ;
11661166
1167- /// Determine how many bytes to read from the socket during a TLS handshake .
1167+ /// Read exactly one TLS record from the TCP socket .
11681168///
11691169/// OpenSSL reads one TLS record at a time (no read-ahead by default).
11701170/// Rustls, however, consumes all available TCP data when fed via read_tls().
1171- /// If application data arrives simultaneously with the final handshake record,
1172- /// the eager read drains the TCP buffer, leaving the app data in rustls's
1173- /// internal buffer where select() cannot see it. This causes asyncore-based
1174- /// servers (which rely on select() for readability) to miss the data and the
1175- /// peer times out.
1171+ /// If a close_notify or other control record arrives alongside application
1172+ /// data, the eager read drains the TCP buffer, leaving the control record in
1173+ /// rustls's internal buffer where select() cannot see it. This causes
1174+ /// asyncore-based servers (which rely on select() for readability) to miss
1175+ /// the data and the peer times out.
11761176///
11771177/// Fix: peek at the TCP buffer to find the first complete TLS record boundary
1178- /// and recv() only that many bytes. Any remaining data (including application
1179- /// data that piggybacked on the same TCP segment) stays in the kernel buffer
1180- /// and remains visible to select().
1181- fn handshake_recv_one_record ( socket : & PySSLSocket , vm : & VirtualMachine ) -> SslResult < PyObjectRef > {
1178+ /// and recv() only that many bytes. Any remaining data stays in the kernel
1179+ /// buffer and remains visible to select().
1180+ fn recv_one_tls_record ( socket : & PySSLSocket , vm : & VirtualMachine ) -> SslResult < PyObjectRef > {
11821181 // Peek at what is available without consuming it.
11831182 let peeked_obj = match socket. sock_peek ( SSL3_RT_MAX_PLAIN_LENGTH , vm) {
11841183 Ok ( d) => d,
@@ -1272,7 +1271,7 @@ fn handshake_read_data(
12721271 // record. This matches OpenSSL's default (no read-ahead) behaviour
12731272 // and keeps remaining data in the kernel buffer where select() can
12741273 // detect it.
1275- handshake_recv_one_record ( socket, vm) ?
1274+ recv_one_tls_record ( socket, vm) ?
12761275 } else {
12771276 match socket. sock_recv ( SSL3_RT_MAX_PLAIN_LENGTH , vm) {
12781277 Ok ( d) => d,
@@ -1716,17 +1715,8 @@ pub(super) fn ssl_read(
17161715 }
17171716 // Blocking socket or socket with timeout: try to read more data from socket.
17181717 // Even though rustls says it doesn't want to read, more TLS records may arrive.
1719- // This handles the case where rustls processed all buffered TLS records but
1720- // more data is coming over the network.
1721- let data = match socket. sock_recv ( 2048 , vm) {
1722- Ok ( data) => data,
1723- Err ( e) => {
1724- if is_connection_closed_error ( & e, vm) {
1725- return Err ( SslError :: Eof ) ;
1726- }
1727- return Err ( SslError :: Py ( e) ) ;
1728- }
1729- } ;
1718+ // Use single-record reading to avoid consuming close_notify alongside data.
1719+ let data = recv_one_tls_record ( socket, vm) ?;
17301720
17311721 let bytes_read = data
17321722 . clone ( )
@@ -2128,28 +2118,27 @@ fn ssl_ensure_data_available(
21282118 // else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select
21292119 }
21302120
2131- let data = match socket . sock_recv ( 2048 , vm ) {
2132- Ok ( data ) => data ,
2133- Err ( e ) => {
2134- if is_blocking_io_error ( & e , vm ) {
2135- return Err ( SslError :: WantRead ) ;
2136- }
2137- // Before returning socket error, check if rustls already has a queued TLS alert
2138- // This mirrors CPython/OpenSSL behavior: SSL errors take precedence over socket errors
2139- // On Windows, TCP RST may arrive before we read the alert, but rustls may have
2140- // already received and buffered the alert from a previous read
2141- if let Err ( rustls_err ) = conn . process_new_packets ( ) {
2142- return Err ( SslError :: from_rustls ( rustls_err ) ) ;
2143- }
2144- // In SSL context, connection closed errors (ECONNABORTED, ECONNRESET) indicate
2145- // unexpected connection termination - the peer closed without proper TLS shutdown.
2146- // This is semantically equivalent to "EOF occurred in violation of protocol"
2147- // because no close_notify alert was received.
2148- // On Windows, TCP RST can arrive before we read the TLS alert, causing these errors.
2149- if is_connection_closed_error ( & e , vm ) {
2150- return Err ( SslError :: Eof ) ;
2121+ // Read one TLS record at a time for non-BIO sockets (matching
2122+ // OpenSSL's default no-read-ahead behaviour). This prevents
2123+ // consuming a close_notify that arrives alongside application data,
2124+ // keeping it in the kernel buffer where select() can detect it.
2125+ let data = if !is_bio {
2126+ recv_one_tls_record ( socket , vm ) ?
2127+ } else {
2128+ match socket. sock_recv ( 2048 , vm ) {
2129+ Ok ( data ) => data ,
2130+ Err ( e ) => {
2131+ if is_blocking_io_error ( & e , vm ) {
2132+ return Err ( SslError :: WantRead ) ;
2133+ }
2134+ if let Err ( rustls_err ) = conn . process_new_packets ( ) {
2135+ return Err ( SslError :: from_rustls ( rustls_err ) ) ;
2136+ }
2137+ if is_connection_closed_error ( & e , vm ) {
2138+ return Err ( SslError :: Eof ) ;
2139+ }
2140+ return Err ( SslError :: Py ( e ) ) ;
21512141 }
2152- return Err ( SslError :: Py ( e) ) ;
21532142 }
21542143 } ;
21552144
0 commit comments