Skip to content

Commit 073adbd

Browse files
authored
Fix SSL read over-consuming TCP data (#7418)
Use single-record reading (recv_one_tls_record) for all SSL reads, not just handshake. This prevents rustls from eagerly consuming close_notify alongside application data, which left the TCP buffer empty and caused select()-based servers to miss readability and time out. Also fix recv_one_tls_record to return Eof (not WantRead) when peek returns empty bytes, since empty peek means the peer has closed the TCP connection.
1 parent 7c0981b commit 073adbd

File tree

3 files changed

+69
-54
lines changed

3 files changed

+69
-54
lines changed

Lib/test/test_ftplib.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,6 @@ def retr():
903903
retr()
904904

905905

906-
@unittest.skip("TODO: RUSTPYTHON; SSL + asyncore has problem")
907906
@skipUnless(ssl, "SSL not available")
908907
@requires_subprocess()
909908
class TestTLS_FTPClassMixin(TestFTPClass):
@@ -922,7 +921,6 @@ def setUp(self, encoding=DEFAULT_ENCODING):
922921

923922

924923
@skipUnless(ssl, "SSL not available")
925-
@unittest.skip("TODO: RUSTPYTHON; SSL + asyncore has problem")
926924
@requires_subprocess()
927925
class TestTLS_FTPClass(TestCase):
928926
"""Specific TLS_FTP class tests."""
@@ -971,7 +969,6 @@ def test_data_connection(self):
971969
LIST_DATA.encode(self.client.encoding))
972970
self.assertEqual(self.client.voidresp(), "226 transfer complete")
973971

974-
@unittest.skip('TODO: RUSTPYTHON flaky TimeoutError')
975972
def test_login(self):
976973
# login() is supposed to implicitly secure the control connection
977974
self.assertNotIsInstance(self.client.sock, ssl.SSLSocket)
@@ -984,7 +981,6 @@ def test_auth_issued_twice(self):
984981
self.client.auth()
985982
self.assertRaises(ValueError, self.client.auth)
986983

987-
@unittest.skip('TODO: RUSTPYTHON flaky TimeoutError')
988984
def test_context(self):
989985
self.client.quit()
990986
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

crates/stdlib/src/ssl.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3629,18 +3629,17 @@ mod _ssl {
36293629
return return_data(buf, &buffer, vm);
36303630
}
36313631
}
3632-
// Clean closure with close_notify
3633-
// CPython behavior depends on whether we've sent our close_notify:
3634-
// - If we've already sent close_notify (unwrap was called): raise SSLZeroReturnError
3635-
// - If we haven't sent close_notify yet: return empty bytes
3632+
// Clean closure via close_notify from peer.
3633+
// If we already sent close_notify (unwrap was called),
3634+
// raise SSLZeroReturnError (bidirectional shutdown).
3635+
// Otherwise return empty bytes, which callers (asyncore,
3636+
// asyncio sslproto) interpret as EOF.
36363637
let our_shutdown_state = *self.shutdown_state.lock();
36373638
if our_shutdown_state == ShutdownState::SentCloseNotify
36383639
|| our_shutdown_state == ShutdownState::Completed
36393640
{
3640-
// We already sent close_notify, now receiving peer's → SSLZeroReturnError
36413641
Err(create_ssl_zero_return_error(vm).upcast())
36423642
} else {
3643-
// We haven't sent close_notify yet → return empty bytes
36443643
return_data(vec![], &buffer, vm)
36453644
}
36463645
}

crates/stdlib/src/ssl/compat.rs

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,21 +1164,20 @@ fn handshake_write_loop(
11641164
/// TLS record header size (content_type + version + length).
11651165
const 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+
12411271
fn 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

Comments
 (0)