Skip to content

Commit 083f2bf

Browse files
committed
Fix SSL handshake over-reading in STARTTLS
During STARTTLS handshake, sock_recv(16KB) could consume application data that arrived alongside handshake records. The consumed data ended up in rustls's internal buffer where select() could not detect it, causing asyncore-based servers to miss readable events and the peer to time out. Use MSG_PEEK to find the TLS record boundary, then recv() only one complete record. Remaining data stays in the kernel TCP buffer, visible to select(). This matches OpenSSL's default no-read-ahead behaviour. Fixes flaky test_poplib (TestPOP3_TLSClass) failures.
1 parent f4eaee1 commit 083f2bf

File tree

1 file changed

+100
-15
lines changed

1 file changed

+100
-15
lines changed

crates/stdlib/src/ssl/compat.rs

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,87 @@ fn handshake_write_loop(
11611161
///
11621162
/// Waits for and reads TLS records from the peer, handling SNI callback setup.
11631163
/// Returns (made_progress, is_first_sni_read).
1164+
/// TLS record header size (content_type + version + length).
1165+
const TLS_RECORD_HEADER_SIZE: usize = 5;
1166+
1167+
/// Determine how many bytes to read from the socket during a TLS handshake.
1168+
///
1169+
/// OpenSSL reads one TLS record at a time (no read-ahead by default).
1170+
/// 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.
1176+
///
1177+
/// 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(
1182+
socket: &PySSLSocket,
1183+
vm: &VirtualMachine,
1184+
) -> SslResult<PyObjectRef> {
1185+
// Peek at what is available without consuming it.
1186+
let peeked_obj = match socket.sock_peek(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
1187+
Ok(d) => d,
1188+
Err(e) => {
1189+
if is_blocking_io_error(&e, vm) {
1190+
return Err(SslError::WantRead);
1191+
}
1192+
return Err(SslError::Py(e));
1193+
}
1194+
};
1195+
1196+
let peeked = ArgBytesLike::try_from_object(vm, peeked_obj)
1197+
.map_err(|_| SslError::Syscall("Expected bytes-like object from peek".to_string()))?;
1198+
let peeked_bytes = peeked.borrow_buf();
1199+
1200+
if peeked_bytes.is_empty() {
1201+
return Err(SslError::WantRead);
1202+
}
1203+
1204+
if peeked_bytes.len() < TLS_RECORD_HEADER_SIZE {
1205+
// Not enough data for a TLS record header yet.
1206+
// Read all available bytes so rustls can buffer the partial header;
1207+
// this avoids busy-waiting because the kernel buffer is now empty
1208+
// and select() will only wake us when new data arrives.
1209+
return socket.sock_recv(peeked_bytes.len(), vm).map_err(|e| {
1210+
if is_blocking_io_error(&e, vm) {
1211+
SslError::WantRead
1212+
} else {
1213+
SslError::Py(e)
1214+
}
1215+
});
1216+
}
1217+
1218+
// Parse the TLS record length from the header.
1219+
let record_body_len =
1220+
u16::from_be_bytes([peeked_bytes[3], peeked_bytes[4]]) as usize;
1221+
let total_record_size = TLS_RECORD_HEADER_SIZE + record_body_len;
1222+
1223+
let recv_size = if peeked_bytes.len() >= total_record_size {
1224+
// Complete record available — consume exactly one record.
1225+
total_record_size
1226+
} else {
1227+
// Incomplete record — consume everything so the kernel buffer is
1228+
// drained and select() will block until more data arrives.
1229+
peeked_bytes.len()
1230+
};
1231+
1232+
// Must drop the borrow before calling sock_recv (which re-enters Python).
1233+
drop(peeked_bytes);
1234+
drop(peeked);
1235+
1236+
socket.sock_recv(recv_size, vm).map_err(|e| {
1237+
if is_blocking_io_error(&e, vm) {
1238+
SslError::WantRead
1239+
} else {
1240+
SslError::Py(e)
1241+
}
1242+
})
1243+
}
1244+
11641245
fn handshake_read_data(
11651246
conn: &mut TlsConnection,
11661247
socket: &PySSLSocket,
@@ -1189,23 +1270,27 @@ fn handshake_read_data(
11891270
}
11901271
}
11911272

1192-
let data_obj = match socket.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
1193-
Ok(d) => d,
1194-
Err(e) => {
1195-
if is_blocking_io_error(&e, vm) {
1196-
return Err(SslError::WantRead);
1197-
}
1198-
// In socket mode, if recv times out and we're only waiting for read
1199-
// (no wants_write), we might be waiting for optional NewSessionTicket in TLS 1.3
1200-
// Consider the handshake complete
1201-
if !is_bio && !conn.wants_write() {
1202-
// Check if it's a timeout exception
1203-
if e.fast_isinstance(vm.ctx.exceptions.timeout_error) {
1204-
// Timeout waiting for optional data - handshake is complete
1205-
return Ok((false, false));
1273+
let data_obj = if !is_bio {
1274+
// In socket mode, read one TLS record at a time to avoid consuming
1275+
// application data that may arrive alongside the final handshake
1276+
// record. This matches OpenSSL's default (no read-ahead) behaviour
1277+
// and keeps remaining data in the kernel buffer where select() can
1278+
// detect it.
1279+
handshake_recv_one_record(socket, vm)?
1280+
} else {
1281+
match socket.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
1282+
Ok(d) => d,
1283+
Err(e) => {
1284+
if is_blocking_io_error(&e, vm) {
1285+
return Err(SslError::WantRead);
1286+
}
1287+
if !conn.wants_write() {
1288+
if e.fast_isinstance(vm.ctx.exceptions.timeout_error) {
1289+
return Ok((false, false));
1290+
}
12061291
}
1292+
return Err(SslError::Py(e));
12071293
}
1208-
return Err(SslError::Py(e));
12091294
}
12101295
};
12111296

0 commit comments

Comments
 (0)