@@ -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,
@@ -1195,7 +1194,10 @@ fn handshake_recv_one_record(socket: &PySSLSocket, vm: &VirtualMachine) -> SslRe
11951194 let peeked_bytes = peeked. borrow_buf ( ) ;
11961195
11971196 if peeked_bytes. is_empty ( ) {
1198- return Err ( SslError :: WantRead ) ;
1197+ // Empty peek means the peer has closed the TCP connection (FIN).
1198+ // Non-blocking sockets would have returned EAGAIN/EWOULDBLOCK
1199+ // (caught above as WantRead), so empty bytes here always means EOF.
1200+ return Err ( SslError :: Eof ) ;
11991201 }
12001202
12011203 if peeked_bytes. len ( ) < TLS_RECORD_HEADER_SIZE {
@@ -1238,6 +1240,34 @@ fn handshake_recv_one_record(socket: &PySSLSocket, vm: &VirtualMachine) -> SslRe
12381240 } )
12391241}
12401242
1243+ /// Read a single TLS record for post-handshake I/O while preserving the
1244+ /// SSL-vs-socket error precedence from the old sock_recv() path.
1245+ fn recv_one_tls_record_for_data (
1246+ conn : & mut TlsConnection ,
1247+ socket : & PySSLSocket ,
1248+ vm : & VirtualMachine ,
1249+ ) -> SslResult < PyObjectRef > {
1250+ match recv_one_tls_record ( socket, vm) {
1251+ Ok ( data) => Ok ( data) ,
1252+ Err ( SslError :: Eof ) => {
1253+ if let Err ( rustls_err) = conn. process_new_packets ( ) {
1254+ return Err ( SslError :: from_rustls ( rustls_err) ) ;
1255+ }
1256+ Ok ( vm. ctx . new_bytes ( vec ! [ ] ) . into ( ) )
1257+ }
1258+ Err ( SslError :: Py ( e) ) => {
1259+ if let Err ( rustls_err) = conn. process_new_packets ( ) {
1260+ return Err ( SslError :: from_rustls ( rustls_err) ) ;
1261+ }
1262+ if is_connection_closed_error ( & e, vm) {
1263+ return Err ( SslError :: Eof ) ;
1264+ }
1265+ Err ( SslError :: Py ( e) )
1266+ }
1267+ Err ( e) => Err ( e) ,
1268+ }
1269+ }
1270+
12411271fn handshake_read_data (
12421272 conn : & mut TlsConnection ,
12431273 socket : & PySSLSocket ,
@@ -1272,7 +1302,7 @@ fn handshake_read_data(
12721302 // record. This matches OpenSSL's default (no read-ahead) behaviour
12731303 // and keeps remaining data in the kernel buffer where select() can
12741304 // detect it.
1275- handshake_recv_one_record ( socket, vm) ?
1305+ recv_one_tls_record ( socket, vm) ?
12761306 } else {
12771307 match socket. sock_recv ( SSL3_RT_MAX_PLAIN_LENGTH , vm) {
12781308 Ok ( d) => d,
@@ -1716,17 +1746,8 @@ pub(super) fn ssl_read(
17161746 }
17171747 // Blocking socket or socket with timeout: try to read more data from socket.
17181748 // 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- } ;
1749+ // Use single-record reading to avoid consuming close_notify alongside data.
1750+ let data = recv_one_tls_record_for_data ( conn, socket, vm) ?;
17301751
17311752 let bytes_read = data
17321753 . clone ( )
@@ -2128,28 +2149,27 @@ fn ssl_ensure_data_available(
21282149 // else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select
21292150 }
21302151
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 ) ;
2152+ // Read one TLS record at a time for non-BIO sockets (matching
2153+ // OpenSSL's default no-read-ahead behaviour). This prevents
2154+ // consuming a close_notify that arrives alongside application data,
2155+ // keeping it in the kernel buffer where select() can detect it.
2156+ let data = if !is_bio {
2157+ recv_one_tls_record_for_data ( conn , socket , vm ) ?
2158+ } else {
2159+ match socket. sock_recv ( 2048 , vm ) {
2160+ Ok ( data ) => data ,
2161+ Err ( e ) => {
2162+ if is_blocking_io_error ( & e , vm ) {
2163+ return Err ( SslError :: WantRead ) ;
2164+ }
2165+ if let Err ( rustls_err ) = conn . process_new_packets ( ) {
2166+ return Err ( SslError :: from_rustls ( rustls_err ) ) ;
2167+ }
2168+ if is_connection_closed_error ( & e , vm ) {
2169+ return Err ( SslError :: Eof ) ;
2170+ }
2171+ return Err ( SslError :: Py ( e ) ) ;
21512172 }
2152- return Err ( SslError :: Py ( e) ) ;
21532173 }
21542174 } ;
21552175
0 commit comments