|
1 | | -// spell-checker: ignore ssleof aesccm aesgcm getblocking setblocking ENDTLS TLSEXT |
| 1 | +// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT |
2 | 2 |
|
3 | 3 | //! Pure Rust SSL/TLS implementation using rustls |
4 | 4 | //! |
@@ -2786,6 +2786,16 @@ mod _ssl { |
2786 | 2786 | recv_method.call((self.sock.clone(), vm.ctx.new_int(size)), vm) |
2787 | 2787 | } |
2788 | 2788 |
|
| 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((self.sock.clone(), vm.ctx.new_int(size), msg_peek), vm) |
| 2797 | + } |
| 2798 | + |
2789 | 2799 | /// Socket send - just sends data, caller must handle pending flush |
2790 | 2800 | /// Use flush_pending_tls_output before this if ordering is important |
2791 | 2801 | pub(crate) fn sock_send(&self, data: &[u8], vm: &VirtualMachine) -> PyResult<PyObjectRef> { |
@@ -4287,45 +4297,118 @@ mod _ssl { |
4287 | 4297 | conn: &mut TlsConnection, |
4288 | 4298 | vm: &VirtualMachine, |
4289 | 4299 | ) -> PyResult<bool> { |
4290 | | - // Try to read incoming data |
| 4300 | + // In socket mode, peek first to avoid consuming post-TLS cleartext |
| 4301 | + // data. During STARTTLS, after close_notify exchange, the socket |
| 4302 | + // transitions to cleartext. Without peeking, sock_recv may consume |
| 4303 | + // cleartext data meant for the application after unwrap(). |
| 4304 | + if self.incoming_bio.is_none() { |
| 4305 | + return self.try_read_close_notify_socket(conn, vm); |
| 4306 | + } |
| 4307 | + |
| 4308 | + // BIO mode: read from incoming BIO |
4291 | 4309 | match self.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) { |
4292 | 4310 | Ok(bytes_obj) => { |
4293 | 4311 | let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?; |
4294 | 4312 | let data = bytes.borrow_buf(); |
4295 | 4313 |
|
4296 | 4314 | if data.is_empty() { |
4297 | | - // Empty read could mean EOF or just "no data yet" in BIO mode |
4298 | 4315 | if let Some(ref bio) = self.incoming_bio { |
4299 | 4316 | // BIO mode: check if EOF was signaled via write_eof() |
4300 | 4317 | let bio_obj: PyObjectRef = bio.clone().into(); |
4301 | 4318 | let eof_attr = bio_obj.get_attr("eof", vm)?; |
4302 | 4319 | let is_eof = eof_attr.try_to_bool(vm)?; |
4303 | 4320 | if !is_eof { |
4304 | | - // No EOF signaled, just no data available yet |
4305 | 4321 | return Ok(false); |
4306 | 4322 | } |
4307 | 4323 | } |
4308 | | - // Socket mode or BIO with EOF: peer closed connection |
4309 | | - // This is "ragged EOF" - peer closed without close_notify |
4310 | 4324 | return Ok(true); |
4311 | 4325 | } |
4312 | 4326 |
|
4313 | | - // Feed data to TLS connection |
4314 | 4327 | let data_slice: &[u8] = data.as_ref(); |
4315 | 4328 | let mut cursor = std::io::Cursor::new(data_slice); |
4316 | 4329 | let _ = conn.read_tls(&mut cursor); |
| 4330 | + let _ = conn.process_new_packets(); |
| 4331 | + Ok(false) |
| 4332 | + } |
| 4333 | + Err(e) => { |
| 4334 | + if is_blocking_io_error(&e, vm) { |
| 4335 | + return Ok(false); |
| 4336 | + } |
| 4337 | + Ok(true) |
| 4338 | + } |
| 4339 | + } |
| 4340 | + } |
4317 | 4341 |
|
4318 | | - // Process packets |
| 4342 | + /// Socket-mode close_notify reader that respects TLS record boundaries. |
| 4343 | + /// Uses MSG_PEEK to inspect data before consuming, preventing accidental |
| 4344 | + /// consumption of post-TLS cleartext data during STARTTLS transitions. |
| 4345 | + /// |
| 4346 | + /// Equivalent to OpenSSL's `SSL_set_read_ahead(ssl, 0)` — rustls has no |
| 4347 | + /// such knob, so we enforce record-level reads manually via peek. |
| 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 = u16::from_be_bytes([peek_data[3], peek_data[4]]) as usize; |
| 4383 | + 5 + record_length |
| 4384 | + } else { |
| 4385 | + // Partial header available - read just these bytes for now |
| 4386 | + peek_data.len() |
| 4387 | + }; |
| 4388 | + |
| 4389 | + drop(peek_data); |
| 4390 | + drop(peeked); |
| 4391 | + |
| 4392 | + // Now consume exactly one TLS record from the socket |
| 4393 | + match self.sock_recv(recv_size, vm) { |
| 4394 | + Ok(bytes_obj) => { |
| 4395 | + let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?; |
| 4396 | + let data = bytes.borrow_buf(); |
| 4397 | + |
| 4398 | + if data.is_empty() { |
| 4399 | + return Ok(true); |
| 4400 | + } |
| 4401 | + |
| 4402 | + let data_slice: &[u8] = data.as_ref(); |
| 4403 | + let mut cursor = std::io::Cursor::new(data_slice); |
| 4404 | + let _ = conn.read_tls(&mut cursor); |
4319 | 4405 | let _ = conn.process_new_packets(); |
4320 | 4406 | Ok(false) |
4321 | 4407 | } |
4322 | 4408 | Err(e) => { |
4323 | | - // BlockingIOError means no data yet |
4324 | 4409 | if is_blocking_io_error(&e, vm) { |
4325 | 4410 | return Ok(false); |
4326 | 4411 | } |
4327 | | - // Connection reset, EOF, or other error means peer closed |
4328 | | - // ECONNRESET, EPIPE, broken pipe, etc. |
4329 | 4412 | Ok(true) |
4330 | 4413 | } |
4331 | 4414 | } |
|
0 commit comments