@@ -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