Skip to content

Commit ca8936c

Browse files
committed
fix ssl
1 parent e2fda95 commit ca8936c

File tree

1 file changed

+94
-10
lines changed

1 file changed

+94
-10
lines changed

crates/stdlib/src/ssl.rs

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2786,6 +2786,19 @@ mod _ssl {
27862786
recv_method.call((self.sock.clone(), vm.ctx.new_int(size)), vm)
27872787
}
27882788

2789+
/// Peek at socket data without consuming it (MSG_PEEK).
2790+
/// Used during TLS shutdown to avoid consuming post-TLS cleartext data.
2791+
pub(crate) fn sock_peek(&self, size: usize, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
2792+
let socket_mod = vm.import("socket", 0)?;
2793+
let socket_class = socket_mod.get_attr("socket", vm)?;
2794+
let recv_method = socket_class.get_attr("recv", vm)?;
2795+
let msg_peek = socket_mod.get_attr("MSG_PEEK", vm)?;
2796+
recv_method.call(
2797+
(self.sock.clone(), vm.ctx.new_int(size), msg_peek),
2798+
vm,
2799+
)
2800+
}
2801+
27892802
/// Socket send - just sends data, caller must handle pending flush
27902803
/// Use flush_pending_tls_output before this if ordering is important
27912804
pub(crate) fn sock_send(&self, data: &[u8], vm: &VirtualMachine) -> PyResult<PyObjectRef> {
@@ -4287,45 +4300,116 @@ mod _ssl {
42874300
conn: &mut TlsConnection,
42884301
vm: &VirtualMachine,
42894302
) -> PyResult<bool> {
4290-
// Try to read incoming data
4303+
// In socket mode, peek first to avoid consuming post-TLS cleartext
4304+
// data. During STARTTLS, after close_notify exchange, the socket
4305+
// transitions to cleartext. Without peeking, sock_recv may consume
4306+
// cleartext data meant for the application after unwrap().
4307+
if self.incoming_bio.is_none() {
4308+
return self.try_read_close_notify_socket(conn, vm);
4309+
}
4310+
4311+
// BIO mode: read from incoming BIO
42914312
match self.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
42924313
Ok(bytes_obj) => {
42934314
let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?;
42944315
let data = bytes.borrow_buf();
42954316

42964317
if data.is_empty() {
4297-
// Empty read could mean EOF or just "no data yet" in BIO mode
42984318
if let Some(ref bio) = self.incoming_bio {
42994319
// BIO mode: check if EOF was signaled via write_eof()
43004320
let bio_obj: PyObjectRef = bio.clone().into();
43014321
let eof_attr = bio_obj.get_attr("eof", vm)?;
43024322
let is_eof = eof_attr.try_to_bool(vm)?;
43034323
if !is_eof {
4304-
// No EOF signaled, just no data available yet
43054324
return Ok(false);
43064325
}
43074326
}
4308-
// Socket mode or BIO with EOF: peer closed connection
4309-
// This is "ragged EOF" - peer closed without close_notify
43104327
return Ok(true);
43114328
}
43124329

4313-
// Feed data to TLS connection
43144330
let data_slice: &[u8] = data.as_ref();
43154331
let mut cursor = std::io::Cursor::new(data_slice);
43164332
let _ = conn.read_tls(&mut cursor);
4333+
let _ = conn.process_new_packets();
4334+
Ok(false)
4335+
}
4336+
Err(e) => {
4337+
if is_blocking_io_error(&e, vm) {
4338+
return Ok(false);
4339+
}
4340+
Ok(true)
4341+
}
4342+
}
4343+
}
4344+
4345+
/// Socket-mode close_notify reader that respects TLS record boundaries.
4346+
/// Uses MSG_PEEK to inspect data before consuming, preventing accidental
4347+
/// consumption of post-TLS cleartext data during STARTTLS transitions.
4348+
fn try_read_close_notify_socket(
4349+
&self,
4350+
conn: &mut TlsConnection,
4351+
vm: &VirtualMachine,
4352+
) -> PyResult<bool> {
4353+
// Peek at the first 5 bytes (TLS record header size)
4354+
let peeked_obj = match self.sock_peek(5, vm) {
4355+
Ok(obj) => obj,
4356+
Err(e) => {
4357+
if is_blocking_io_error(&e, vm) {
4358+
return Ok(false);
4359+
}
4360+
return Ok(true);
4361+
}
4362+
};
4363+
4364+
let peeked = ArgBytesLike::try_from_object(vm, peeked_obj)?;
4365+
let peek_data = peeked.borrow_buf();
4366+
4367+
if peek_data.is_empty() {
4368+
return Ok(true); // EOF
4369+
}
4370+
4371+
// TLS record content types: ChangeCipherSpec(20), Alert(21),
4372+
// Handshake(22), ApplicationData(23)
4373+
let content_type = peek_data[0];
4374+
if !(20..=23).contains(&content_type) {
4375+
// Not a TLS record - post-TLS cleartext data.
4376+
// Peer has completed TLS shutdown; don't consume this data.
4377+
return Ok(true);
4378+
}
4379+
4380+
// Determine how many bytes to read for exactly one TLS record
4381+
let recv_size = if peek_data.len() >= 5 {
4382+
let record_length =
4383+
u16::from_be_bytes([peek_data[3], peek_data[4]]) as usize;
4384+
5 + record_length
4385+
} else {
4386+
// Partial header available - read just these bytes for now
4387+
peek_data.len()
4388+
};
43174389

4318-
// Process packets
4390+
drop(peek_data);
4391+
drop(peeked);
4392+
4393+
// Now consume exactly one TLS record from the socket
4394+
match self.sock_recv(recv_size, vm) {
4395+
Ok(bytes_obj) => {
4396+
let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?;
4397+
let data = bytes.borrow_buf();
4398+
4399+
if data.is_empty() {
4400+
return Ok(true);
4401+
}
4402+
4403+
let data_slice: &[u8] = data.as_ref();
4404+
let mut cursor = std::io::Cursor::new(data_slice);
4405+
let _ = conn.read_tls(&mut cursor);
43194406
let _ = conn.process_new_packets();
43204407
Ok(false)
43214408
}
43224409
Err(e) => {
4323-
// BlockingIOError means no data yet
43244410
if is_blocking_io_error(&e, vm) {
43254411
return Ok(false);
43264412
}
4327-
// Connection reset, EOF, or other error means peer closed
4328-
// ECONNRESET, EPIPE, broken pipe, etc.
43294413
Ok(true)
43304414
}
43314415
}

0 commit comments

Comments
 (0)