Rustls integration improvements (#7946)

* Do not call `import socket` on each send()/recv() when using rustls

Use method references cached during socket creation.

* Implement reading of at most one TLS record from socket

Previous algorithm didn't take into account that recv() may return less
data than requested even for blocking sockets.

* Remove special handling of rustls "buffer full" errors

First of all, existing code does not really work and this leads to an
infinite loop: https://github.com/RustPython/RustPython/issues/7891

Second, this should never happen when rustls used properly (wrt
wants_read() and wants_write()) and thus all such errors are
implementation bugs that must be properly fixed.

* Replace own TlsConnection with rustls::Connection

* Fix waiting on a socket

1) Ensure that socket_wait() called from TLS glue code allows threads
2) Ensure that socket_wait() called from TLS glue code properly handles
   EINTR on *nix
3) Ensure that select() or poll() error conditions are checked
4) Use poll() on *nix so socket descriptor values are not limited

* Remove dead code from rustls glue

* Do not present rustls errors as OSError(0, "Success")

* Remove infinite loop "detection" from rustls glue

TLS handshake cannot be infinite. Any infinite loop here is a serious
bug in implementation and should be fixed properly.

This code triggers in some cases (very short reads) with misleading
`ssl_error.SSLWantReadError: The operation did not complete (read)`.

* Add test for 1-byte max recv in TLS client

* Add regression test for https://github.com/RustPython/RustPython/issues/7891

* Fix constants in rustls glue code

* Deduplicate verify flags / record-size constants
* Larger "max encrypted TLS record length"
This commit is contained in:
Ivan Mironov authored and GitHub committed 2026-05-22 20:04:15 +09:00
1 parent 4eb9534646
commit 2a163609cd
8 files changed
+736 -583

No files matched your search

-33
View File
@@ -3,20 +3,10 @@ use crate::os::CheckLibcResult;
#[cfg(unix)]
use core::ffi::CStr;
#[cfg(unix)]
use core::time::Duration;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::{io, os::fd::BorrowedFd};
#[cfg(unix)]
#[derive(Copy, Clone)]
pub enum PollKind {
Read,
Write,
Connect,
}
#[cfg(all(unix, not(target_os = "redox")))]
pub fn sethostname(hostname: &str) -> io::Result<()> {
nix::unistd::sethostname(hostname).map_err(io::Error::from)
@@ -111,29 +101,6 @@ pub fn setsockopt_none(fd: libc::c_int, level: i32, name: i32, optlen: u32) -> i
Ok(())
}
#[cfg(unix)]
pub fn poll_socket(
fd: BorrowedFd<'_>,
kind: PollKind,
interval: Option<Duration>,
) -> io::Result<bool> {
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
let events = match kind {
PollKind::Read => PollFlags::POLLIN,
PollKind::Write => PollFlags::POLLOUT,
PollKind::Connect => PollFlags::POLLOUT | PollFlags::POLLERR,
};
let mut pollfd = [PollFd::new(fd, events)];
let timeout = match interval {
Some(d) => d.try_into().unwrap_or(PollTimeout::MAX),
None => PollTimeout::NONE,
};
poll(&mut pollfd, timeout)
.map(|ret| ret == 0)
.map_err(io::Error::from)
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
+131 -71
View File
@@ -3,7 +3,7 @@
pub(crate) use _socket::module_def;
#[cfg(feature = "ssl")]
pub(super) use _socket::{PySocket, SelectKind, sock_select, timeout_error_msg};
pub(super) use _socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg};
#[pymodule]
mod _socket {
@@ -1060,20 +1060,20 @@ mod _socket {
fn sock_op<F, R>(
&self,
vm: &VirtualMachine,
select: SelectKind,
wait_kind: SockWaitKind,
f: F,
) -> Result<R, IoOrPyException>
where
F: FnMut() -> io::Result<R>,
{
let timeout = self.get_timeout().ok();
self.sock_op_timeout_err(vm, select, timeout, f)
self.sock_op_timeout_err(vm, wait_kind, timeout, f)
}
fn sock_op_timeout_err<F, R>(
&self,
vm: &VirtualMachine,
select: SelectKind,
wait_kind: SockWaitKind,
timeout: Option<Duration>,
mut f: F,
) -> Result<R, IoOrPyException>
@@ -1083,19 +1083,9 @@ mod _socket {
let deadline = timeout.map(Deadline::new);
loop {
if deadline.is_some() || matches!(select, SelectKind::Connect) {
let interval = deadline.as_ref().map(|d| d.time_until()).transpose()?;
if deadline.is_some() || matches!(wait_kind, SockWaitKind::Connect) {
let sock = self.sock()?;
let res = vm.allow_threads(|| sock_select(&sock, select, interval));
match res {
Ok(true) => return Err(IoOrPyException::Timeout),
Err(e) if e.kind() == io::ErrorKind::Interrupted => {
vm.check_signals()?;
continue;
}
Err(e) => return Err(e.into()),
Ok(false) => {} // no timeout, continue as normal
}
sock_wait_deadline(&sock, wait_kind, &deadline, vm)?;
}
let err = loop {
@@ -1339,16 +1329,11 @@ mod _socket {
};
if wait_connect {
// basically, connect() is async, and it registers an "error" on the socket when it's
// done connecting. SelectKind::Connect fills the errorfds fd_set, so if we wake up
// from poll and the error is EISCONN then we know that the connect is done
self.sock_op(vm, SelectKind::Connect, || {
self.sock_op(vm, SockWaitKind::Connect, || {
let sock = self.sock()?;
let err = sock.take_error()?;
match err {
Some(e) if e.posix_errno() == libc::EISCONN => Ok(()),
Some(e) => Err(e),
// TODO: is this accurate?
None => Ok(()),
}
})
@@ -1587,7 +1572,8 @@ mod _socket {
) -> Result<(RawSocket, PyObjectRef), IoOrPyException> {
// Use accept_raw() instead of accept() to avoid socket2's set_common_flags()
// which tries to set SO_NOSIGPIPE and fails with EINVAL on Unix domain sockets on macOS
let (sock, addr) = self.sock_op(vm, SelectKind::Read, || self.sock()?.accept_raw())?;
let (sock, addr) =
self.sock_op(vm, SockWaitKind::Read, || self.sock()?.accept_raw())?;
let fd = into_sock_fileno(sock);
Ok((fd, get_addr_tuple(&addr, vm)))
}
@@ -1602,7 +1588,7 @@ mod _socket {
let flags = flags.unwrap_or(0);
let mut buffer = Vec::with_capacity(bufsize);
let sock = self.sock()?;
let n = self.sock_op(vm, SelectKind::Read, || {
let n = self.sock_op(vm, SockWaitKind::Read, || {
sock.recv_with_flags(buffer.spare_capacity_mut(), flags)
})?;
unsafe { buffer.set_len(n) };
@@ -1633,7 +1619,7 @@ mod _socket {
};
let buf = &mut buf[..read_len];
self.sock_op(vm, SelectKind::Read, || {
self.sock_op(vm, SockWaitKind::Read, || {
sock.recv_with_flags(unsafe { slice_as_uninit(buf) }, flags)
})
}
@@ -1650,7 +1636,7 @@ mod _socket {
.to_usize()
.ok_or_else(|| vm.new_value_error("negative buffersize in recvfrom"))?;
let mut buffer = Vec::with_capacity(bufsize);
let (n, addr) = self.sock_op(vm, SelectKind::Read, || {
let (n, addr) = self.sock_op(vm, SockWaitKind::Read, || {
self.sock()?
.recv_from_with_flags(buffer.spare_capacity_mut(), flags)
})?;
@@ -1681,7 +1667,7 @@ mod _socket {
};
let flags = flags.unwrap_or(0);
let sock = self.sock()?;
let (n, addr) = self.sock_op(vm, SelectKind::Read, || {
let (n, addr) = self.sock_op(vm, SockWaitKind::Read, || {
sock.recv_from_with_flags(unsafe { slice_as_uninit(buf) }, flags)
})?;
Ok((n, get_addr_tuple(&addr, vm)))
@@ -1697,7 +1683,7 @@ mod _socket {
let flags = flags.unwrap_or(0);
let buf = bytes.borrow_buf();
let buf = &*buf;
self.sock_op(vm, SelectKind::Write, || {
self.sock_op(vm, SockWaitKind::Write, || {
self.sock()?.send_with_flags(buf, flags)
})
}
@@ -1721,7 +1707,7 @@ mod _socket {
// now we have like 3 layers of interrupt loop :)
while buf_offset < buf.len() {
let interval = deadline.as_ref().map(|d| d.time_until()).transpose()?;
self.sock_op_timeout_err(vm, SelectKind::Write, interval, || {
self.sock_op_timeout_err(vm, SockWaitKind::Write, interval, || {
let subbuf = &buf[buf_offset..];
buf_offset += self.sock()?.send_with_flags(subbuf, flags)?;
Ok(())
@@ -1754,7 +1740,7 @@ mod _socket {
let addr = self.extract_address(address, "sendto", vm)?;
let buf = bytes.borrow_buf();
let buf = &*buf;
self.sock_op(vm, SelectKind::Write, || {
self.sock_op(vm, SockWaitKind::Write, || {
self.sock()?.send_to_with_flags(buf, &addr, flags)
})
}
@@ -1812,7 +1798,7 @@ mod _socket {
}
}
self.sock_op(vm, SelectKind::Write, || {
self.sock_op(vm, SockWaitKind::Write, || {
let sock = self.sock()?;
sock.sendmsg(&msg, flags)
})
@@ -1848,7 +1834,7 @@ mod _socket {
.collect::<Vec<_>>();
let iv = iv.map(|iv| iv.borrow_buf().to_vec());
self.sock_op(vm, SelectKind::Write, || {
self.sock_op(vm, SockWaitKind::Write, || {
let sock = self.sock()?;
let fd = unsafe { BorrowedFd::borrow_raw(sock_fileno(&sock)) };
host_socket::sendmsg_afalg(fd, &buffers, op, iv.as_deref(), assoclen, flags)
@@ -1881,7 +1867,7 @@ mod _socket {
let flags = flags.unwrap_or(0);
let msg = self
.sock_op(vm, SelectKind::Read, || {
.sock_op(vm, SockWaitKind::Read, || {
let sock = self.sock()?;
let fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(sock_fileno(&sock)) };
host_socket::recvmsg(fd, bufsize, ancbufsize, flags)
@@ -2436,61 +2422,135 @@ mod _socket {
}
#[derive(Copy, Clone)]
pub(crate) enum SelectKind {
pub(crate) enum SockWaitKind {
Read,
Write,
Connect,
}
/// returns true if timed out
pub(crate) fn sock_select(
/// returns Ok(true) on timeout
pub(crate) fn sock_wait(
sock: &Socket,
kind: SelectKind,
interval: Option<Duration>,
) -> io::Result<bool> {
wait_kind: SockWaitKind,
timeout: Option<Duration>,
vm: &VirtualMachine,
) -> PyResult<bool> {
match sock_wait_deadline(sock, wait_kind, &timeout.map(Deadline::new), vm) {
Ok(()) => Ok(false),
Err(IoOrPyException::Timeout) => Ok(true),
Err(e) => Err(e.into_pyexception(vm)),
}
}
/// returns Err(IoOrPyException::Timeout) on timeout
fn sock_wait_deadline(
sock: &Socket,
wait_kind: SockWaitKind,
deadline: &Option<Deadline>,
vm: &VirtualMachine,
) -> Result<(), IoOrPyException> {
#[cfg(unix)]
{
use std::os::fd::AsFd;
let kind = match kind {
SelectKind::Read => host_socket::PollKind::Read,
SelectKind::Write => host_socket::PollKind::Write,
SelectKind::Connect => host_socket::PollKind::Connect,
};
host_socket::poll_socket(sock.as_fd(), kind, interval)
use rustpython_host_env::select::{PollFd, poll_fds};
let mut events = 0;
if matches!(wait_kind, SockWaitKind::Read) {
events |= libc::POLLIN | libc::POLLPRI;
}
if matches!(wait_kind, SockWaitKind::Write | SockWaitKind::Connect) {
events |= libc::POLLOUT;
}
let mut fds = [PollFd {
fd: sock_fileno(sock),
events,
revents: 0,
}; 1];
loop {
let (timeout, is_capped) = deadline
.as_ref()
.map(|d| {
d.time_until().map(|t| {
let timeout_ms = t.as_millis();
let is_capped = timeout_ms > i32::MAX as u128;
let timeout = if is_capped {
i32::MAX
} else {
timeout_ms as i32
};
(timeout, is_capped)
})
})
.transpose()?
.unwrap_or((-1, false));
match vm.allow_threads(|| poll_fds(&mut fds, timeout)) {
Ok(0) => {
if is_capped {
continue;
}
break Err(IoOrPyException::Timeout);
}
Ok(_) => {
if fds[0].revents & libc::POLLNVAL != 0 {
break Err(io::Error::from_raw_os_error(libc::EBADF).into());
}
break Ok(());
}
Err(e) => {
if e.kind() == io::ErrorKind::Interrupted {
vm.check_signals()?;
continue;
}
break Err(e.into());
}
}
}
}
#[cfg(windows)]
{
use rustpython_host_env::select as host_select;
use rustpython_host_env::select::{FdSet, select, timeval};
let fd = sock_fileno(sock);
let fd = sock_fileno(sock) as usize;
let mut reads = host_select::FdSet::new();
let mut writes = host_select::FdSet::new();
let mut errs = host_select::FdSet::new();
let mut reads = FdSet::new();
let mut writes = FdSet::new();
let mut errs = FdSet::new();
let fd = fd as usize;
match kind {
SelectKind::Read => reads.insert(fd),
SelectKind::Write => writes.insert(fd),
SelectKind::Connect => {
writes.insert(fd);
errs.insert(fd);
}
if matches!(wait_kind, SockWaitKind::Read) {
reads.insert(fd);
errs.insert(fd);
}
if matches!(wait_kind, SockWaitKind::Write | SockWaitKind::Connect) {
writes.insert(fd);
errs.insert(fd);
}
let mut interval = interval.map(|dur| host_select::timeval {
tv_sec: dur.as_secs() as _,
tv_usec: dur.subsec_micros() as _,
});
let mut timeout = deadline
.as_ref()
.map(|d| {
d.time_until().map(|dur| timeval {
tv_sec: dur.as_secs() as _,
tv_usec: dur.subsec_micros() as _,
})
})
.transpose()?;
host_select::select(
fd as i32 + 1,
&mut reads,
&mut writes,
&mut errs,
interval.as_mut(),
)
.map(|ret| ret == 0)
match vm.allow_threads(|| {
select(
0, // nfds is ignored on windows
&mut reads,
&mut writes,
&mut errs,
timeout.as_mut(),
)
}) {
Ok(0) => Err(IoOrPyException::Timeout),
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
}
+190 -152
View File
@@ -36,13 +36,13 @@ mod _ssl {
hash::PyHash,
lock::{PyMutex, PyRwLock},
},
socket::{PySocket, SelectKind, sock_select, timeout_error_msg},
socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg},
vm::{
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
VirtualMachine,
builtins::{
PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef,
PyUtf8StrRef,
PyBaseExceptionRef, PyByteArray, PyBytesRef, PyListRef, PyStrRef, PyType,
PyTypeRef, PyUtf8StrRef,
},
convert::IntoPyException,
function::{
@@ -75,7 +75,7 @@ mod _ssl {
use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
use pem_rfc7468::{LineEnding, encode_string};
use rustls::{
ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection,
ClientConnection, Connection, HandshakeKind, RootCertStore, ServerConfig, ServerConnection,
client::{ClientSessionMemoryCache, ClientSessionStore},
crypto::SupportedKxGroup,
pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer, ServerName},
@@ -94,9 +94,8 @@ mod _ssl {
// Import compat module (OpenSSL compatibility layer)
use super::compat::{
ClientConfigOptions, MultiCertResolver, ProtocolSettings, ServerConfigOptions, SslError,
TlsConnection, create_client_config, create_server_config, curve_name_to_kx_group,
extract_cipher_info, get_cipher_encryption_desc, is_blocking_io_error,
normalize_cipher_name, ssl_do_handshake,
create_client_config, create_server_config, curve_name_to_kx_group, extract_cipher_info,
get_cipher_encryption_desc, is_blocking_io_error, normalize_cipher_name, ssl_do_handshake,
};
// Type aliases for better readability
@@ -154,8 +153,28 @@ mod _ssl {
// Buffer sizes and limits (OpenSSL/CPython compatibility)
const PEM_BUFSIZE: usize = 1024;
// OpenSSL: ssl/ssl_local.h
const SSL3_RT_HEADER_LENGTH: usize = 5;
// This is the maximum MAC (digest) size used by the SSL library. Currently
// maximum of 20 is used by SHA1, but we reserve for future extension for
// 512-bit hashes.
const SSL3_RT_MAX_MD_SIZE: usize = 64;
// Maximum plaintext length: defined by SSL/TLS standards
const SSL3_RT_MAX_PLAIN_LENGTH: usize = 16384;
// Maximum compression overhead: defined by SSL/TLS standards
const SSL3_RT_MAX_COMPRESSED_OVERHEAD: usize = 1024;
// The standards give a maximum encryption overhead of 1024 bytes. In
// practice the value is lower than this. The overhead is the maximum number
// of padding bytes (256) plus the mac size.
const SSL3_RT_MAX_ENCRYPTED_OVERHEAD: usize = 256 + SSL3_RT_MAX_MD_SIZE;
const SSL3_RT_MAX_COMPRESSED_LENGTH: usize =
SSL3_RT_MAX_PLAIN_LENGTH + SSL3_RT_MAX_COMPRESSED_OVERHEAD;
const SSL3_RT_MAX_ENCRYPTED_LENGTH: usize =
SSL3_RT_MAX_ENCRYPTED_OVERHEAD + SSL3_RT_MAX_COMPRESSED_LENGTH;
pub(crate) const SSL3_RT_MAX_PACKET_SIZE: usize =
SSL3_RT_MAX_ENCRYPTED_LENGTH + SSL3_RT_HEADER_LENGTH;
// SSL session cache size (common practice, similar to OpenSSL defaults)
const SSL_SESSION_CACHE_SIZE: usize = 256;
@@ -167,21 +186,26 @@ mod _ssl {
#[pyattr]
const CERT_REQUIRED: i32 = 2;
// Certificate requirements
// SSL Verification Flags / Certificate requirements
#[pyattr]
const VERIFY_DEFAULT: i32 = 0;
#[pyattr]
const VERIFY_CRL_CHECK_LEAF: i32 = 4;
#[pyattr]
const VERIFY_CRL_CHECK_CHAIN: i32 = 12;
/// VERIFY_X509_STRICT flag for RFC 5280 strict compliance
/// When set, performs additional validation including AKI extension checks
#[pyattr]
const VERIFY_X509_STRICT: i32 = 32;
pub(crate) const VERIFY_X509_STRICT: i32 = 32;
#[pyattr]
const VERIFY_ALLOW_PROXY_CERTS: i32 = 64;
#[pyattr]
const VERIFY_X509_TRUSTED_FIRST: i32 = 32768;
/// VERIFY_X509_PARTIAL_CHAIN flag for partial chain validation
/// When set, accept certificates if any certificate in the chain is in the trust store
/// (not just root CAs). This matches OpenSSL's X509_V_FLAG_PARTIAL_CHAIN behavior.
#[pyattr]
const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
pub(crate) const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
// Options (OpenSSL-compatible flags, mostly no-op in rustls)
#[pyattr]
@@ -398,8 +422,7 @@ mod _ssl {
// Session data structure for tracking TLS sessions
#[derive(Debug, Clone)]
struct SessionData {
#[allow(dead_code)]
server_name: String,
_server_name: String,
session_id: Vec<u8>,
creation_time: SystemTime,
lifetime: u64,
@@ -477,7 +500,7 @@ mod _ssl {
let creation_time = SystemTime::now();
let server_name_str = server_name.to_str();
let session_data = SessionData {
server_name: server_name_str.as_ref().to_string(),
_server_name: server_name_str.as_ref().to_string(),
session_id: generate_session_id_from_metadata(
server_name_str.as_ref(),
&creation_time,
@@ -521,7 +544,7 @@ mod _ssl {
let creation_time = SystemTime::now();
let server_name_str = server_name.to_str();
let session_data = SessionData {
server_name: server_name_str.to_string(),
_server_name: server_name_str.to_string(),
session_id: generate_session_id_from_metadata(
server_name_str.as_ref(),
&creation_time,
@@ -720,10 +743,6 @@ mod _ssl {
#[pytraverse(skip)]
verify_flags: PyRwLock<i32>,
// Rustls configuration (built lazily)
#[allow(dead_code)]
#[pytraverse(skip)]
client_config: PyRwLock<Option<Arc<ClientConfig>>>,
#[allow(dead_code)]
#[pytraverse(skip)]
server_config: PyRwLock<Option<Arc<ServerConfig>>>,
// Certificate store
@@ -747,19 +766,11 @@ mod _ssl {
#[pytraverse(skip)]
cert_keys: PyRwLock<Vec<CertKeyPair>>,
// Options
#[allow(dead_code)]
#[pytraverse(skip)]
options: PyRwLock<i32>,
// ALPN protocols
#[allow(dead_code)]
#[pytraverse(skip)]
alpn_protocols: PyRwLock<Vec<Vec<u8>>>,
// ALPN strict matching flag
// When false (default), mimics OpenSSL behavior: no ALPN negotiation failure
// When true, requires ALPN match (Rustls default behavior)
#[allow(dead_code)]
#[pytraverse(skip)]
require_alpn_match: PyRwLock<bool>,
// TLS 1.3 features
#[pytraverse(skip)]
post_handshake_auth: PyRwLock<bool>,
@@ -1825,6 +1836,9 @@ mod _ssl {
args: WrapSocketArgs,
vm: &VirtualMachine,
) -> PyResult<PyRef<PySSLSocket>> {
let socket_mod = vm.import("socket", 0)?;
let socket_class = socket_mod.get_attr("socket", vm)?;
// Convert server_hostname to Option<String>
// Handle both missing argument and None value
let hostname = match args.server_hostname.into_option().flatten() {
@@ -1877,6 +1891,12 @@ mod _ssl {
// Create _SSLSocket instance
let ssl_socket = PySSLSocket {
sock: args.sock.clone(),
sock_send_method: socket_class.get_attr("send", vm)?,
sock_recv_method: socket_class.get_attr("recv", vm)?,
tls_record_header_buf: vm
.ctx
.new_bytearray(Vec::with_capacity(TLS_RECORD_HEADER_SIZE))
.into(),
context: PyRwLock::new(zelf),
server_side: args.server_side,
server_hostname: PyRwLock::new(hostname),
@@ -1886,12 +1906,12 @@ mod _ssl {
owner: PyRwLock::new(args.owner.into_option()),
// Filter out Python None objects - only store actual SSLSession objects
session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))),
verified_chain: PyRwLock::new(None),
incoming_bio: None,
outgoing_bio: None,
sni_state: PyRwLock::new(None),
pending_context: PyRwLock::new(None),
client_hello_buffer: PyMutex::new(None),
sni_callback_processed: PyMutex::new(false),
shutdown_state: PyMutex::new(ShutdownState::NotStarted),
pending_tls_output: PyMutex::new(Vec::new()),
write_buffered_len: PyMutex::new(0),
@@ -1948,7 +1968,12 @@ mod _ssl {
// Create _SSLSocket instance with BIO mode
let ssl_socket = PySSLSocket {
sock: vm.ctx.none(), // No socket in BIO mode
// No socket in BIO mode
sock: vm.ctx.none(),
sock_send_method: vm.ctx.none(),
sock_recv_method: vm.ctx.none(),
tls_record_header_buf: vm.ctx.none(),
context: PyRwLock::new(zelf),
server_side,
server_hostname: PyRwLock::new(hostname),
@@ -1958,12 +1983,12 @@ mod _ssl {
owner: PyRwLock::new(args.owner.into_option()),
// Filter out Python None objects - only store actual SSLSession objects
session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))),
verified_chain: PyRwLock::new(None),
incoming_bio: Some(args.incoming),
outgoing_bio: Some(args.outgoing),
sni_state: PyRwLock::new(None),
pending_context: PyRwLock::new(None),
client_hello_buffer: PyMutex::new(None),
sni_callback_processed: PyMutex::new(false),
shutdown_state: PyMutex::new(ShutdownState::NotStarted),
pending_tls_output: PyMutex::new(Vec::new()),
write_buffered_len: PyMutex::new(0),
@@ -2261,7 +2286,6 @@ mod _ssl {
check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT),
verify_mode: PyRwLock::new(default_verify_mode),
verify_flags: PyRwLock::new(default_verify_flags),
client_config: PyRwLock::new(None),
server_config: PyRwLock::new(None),
root_certs: PyRwLock::new(RootCertStore::empty()),
ca_certs_der: PyRwLock::new(Vec::new()),
@@ -2270,7 +2294,6 @@ mod _ssl {
cert_keys: PyRwLock::new(Vec::new()),
options: PyRwLock::new(default_options),
alpn_protocols: PyRwLock::new(Vec::new()),
require_alpn_match: PyRwLock::new(false),
post_handshake_auth: PyRwLock::new(false),
num_tickets: PyRwLock::new(2), // TLS 1.3 default
minimum_version: PyRwLock::new(min_version),
@@ -2302,6 +2325,15 @@ mod _ssl {
pub(crate) struct PySSLSocket {
// Underlying socket
sock: PyObjectRef,
// Cached socket.socket.send
#[pytraverse(skip)]
sock_send_method: PyObjectRef,
// Cached socket.socket.recv
#[pytraverse(skip)]
sock_recv_method: PyObjectRef,
// Header of currently read TLS record.
#[pytraverse(skip)]
tls_record_header_buf: PyObjectRef,
// SSL context
context: PyRwLock<PyRef<PySSLContext>>,
// Server-side or client-side
@@ -2312,7 +2344,7 @@ mod _ssl {
server_hostname: PyRwLock<Option<String>>,
// TLS connection state
#[pytraverse(skip)]
connection: PyMutex<Option<TlsConnection>>,
connection: PyMutex<Option<Connection>>,
// Handshake completed flag
#[pytraverse(skip)]
handshake_done: PyMutex<bool>,
@@ -2323,10 +2355,6 @@ mod _ssl {
owner: PyRwLock<Option<PyObjectRef>>,
// Session for resumption
session: PyRwLock<Option<PyObjectRef>>,
// Verified certificate chain (built during verification)
#[allow(dead_code)]
#[pytraverse(skip)]
verified_chain: PyRwLock<Option<Vec<CertificateDer<'static>>>>,
// MemoryBIO mode (optional)
incoming_bio: Option<PyRef<PyMemoryBIO>>,
outgoing_bio: Option<PyRef<PyMemoryBIO>>,
@@ -2338,6 +2366,9 @@ mod _ssl {
// Buffer to store ClientHello for connection recreation
#[pytraverse(skip)]
client_hello_buffer: PyMutex<Option<Vec<u8>>>,
// Whether the Python SNI callback has already been run for this handshake
#[pytraverse(skip)]
sni_callback_processed: PyMutex<bool>,
// Shutdown state for tracking close-notify exchange
#[pytraverse(skip)]
shutdown_state: PyMutex<ShutdownState>,
@@ -2367,6 +2398,9 @@ mod _ssl {
Completed, // unwrap() completed successfully
}
/// TLS record header size (content_type + version + length).
const TLS_RECORD_HEADER_SIZE: usize = 5;
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
impl PySSLSocket {
// Check if this is BIO mode
@@ -2547,7 +2581,7 @@ mod _ssl {
.connection
.lock()
.as_ref()
.is_some_and(|conn| conn.is_session_resumed());
.is_some_and(|conn| conn.handshake_kind() == Some(HandshakeKind::Resumed));
*self.session_was_reused.lock() = was_resumed;
@@ -2575,7 +2609,7 @@ mod _ssl {
// Internal implementation with timeout control
pub(crate) fn sock_wait_for_io_impl(
&self,
kind: SelectKind,
wait_kind: SockWaitKind,
vm: &VirtualMachine,
) -> PyResult<bool> {
if self.is_bio_mode() {
@@ -2600,16 +2634,13 @@ mod _ssl {
.sock()
.map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?;
let timed_out = sock_select(&socket, kind, timeout)
.map_err(|e| vm.new_os_error(format!("select failed: {e}")))?;
Ok(timed_out)
sock_wait(&socket, wait_kind, timeout, vm)
}
// Internal implementation with explicit timeout override
pub(crate) fn sock_wait_for_io_with_timeout(
&self,
kind: SelectKind,
wait_kind: SockWaitKind,
timeout: Option<core::time::Duration>,
vm: &VirtualMachine,
) -> PyResult<bool> {
@@ -2630,19 +2661,16 @@ mod _ssl {
.sock()
.map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?;
let timed_out = sock_select(&socket, kind, timeout)
.map_err(|e| vm.new_os_error(format!("select failed: {e}")))?;
Ok(timed_out)
sock_wait(&socket, wait_kind, timeout, vm).map_err(|e| e.into_pyexception(vm))
}
// SNI (Server Name Indication) Helper Methods:
// These methods support the server-side handshake SNI callback mechanism
/// Check if this is the first read during handshake (for SNI callback)
/// Returns true if we haven't processed ClientHello yet, regardless of SNI presence
/// Returns true until the SNI callback has been processed.
pub(crate) fn is_first_sni_read(&self) -> bool {
self.client_hello_buffer.lock().is_none()
!*self.sni_callback_processed.lock()
}
/// Check if SNI callback is configured
@@ -2651,9 +2679,13 @@ mod _ssl {
self.context.read().sni_callback.read().is_some()
}
/// Save ClientHello data from PyObjectRef for potential connection recreation
/// Save ClientHello data for potential connection recreation.
pub(crate) fn save_client_hello_from_bytes(&self, bytes_data: &[u8]) {
*self.client_hello_buffer.lock() = Some(bytes_data.to_vec());
let mut buffer = self.client_hello_buffer.lock();
match buffer.as_mut() {
Some(existing) => existing.extend_from_slice(bytes_data),
None => *buffer = Some(bytes_data.to_vec()),
}
}
/// Get the extracted SNI name from resolver
@@ -2771,23 +2803,85 @@ mod _ssl {
return read_method.call((vm.ctx.new_int(size),), vm);
}
// Normal socket mode
let socket_mod = vm.import("socket", 0)?;
let socket_class = socket_mod.get_attr("socket", vm)?;
// Call socket.socket.recv(self.sock, size, flags)
let recv_method = socket_class.get_attr("recv", vm)?;
recv_method.call((self.sock.clone(), vm.ctx.new_int(size)), vm)
self.sock_recv_method
.call((self.sock.clone(), vm.ctx.new_int(size)), vm)
}
/// Peek at socket data without consuming it (MSG_PEEK).
/// Used during TLS shutdown to avoid consuming post-TLS cleartext data.
pub(crate) fn sock_peek(&self, size: usize, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
let socket_mod = vm.import("socket", 0)?;
let socket_class = socket_mod.get_attr("socket", vm)?;
let recv_method = socket_class.get_attr("recv", vm)?;
let msg_peek = socket_mod.get_attr("MSG_PEEK", vm)?;
recv_method.call((self.sock.clone(), vm.ctx.new_int(size), msg_peek), vm)
// Helper to receive data for at most one TLS record.
// May return incomplete data but never returns more when completes a
// previously incomplete TLS record.
pub(crate) fn sock_recv_at_most_one_tls_record(
&self,
vm: &VirtualMachine,
) -> PyResult<PyBytesRef> {
let obj_to_bytes = |bytes_obj| {
PyBytesRef::try_from_object(vm, bytes_obj)
.map_err(|_| vm.new_os_error("Expected bytes from recv".to_string()))
};
let tls_record_header_buf = self
.tls_record_header_buf
.clone()
.downcast::<PyByteArray>()
.expect("BUG: tls_record_header_buf is not PyByteArray");
let buf_len = tls_record_header_buf.borrow_buf().len();
let (mut with_header, mut remaining_record_body_len) =
if buf_len < TLS_RECORD_HEADER_SIZE {
// We do not have a full TLS record header, start receiving one.
let bytes_obj = self.sock_recv(TLS_RECORD_HEADER_SIZE - buf_len, vm)?;
let bytes = obj_to_bytes(bytes_obj)?;
let mut buf = tls_record_header_buf.borrow_buf_mut();
buf.extend_from_slice(bytes.as_bytes());
if buf.len() < TLS_RECORD_HEADER_SIZE {
return Ok(bytes);
}
// Parse the remaining length.
let record_body_len = u16::from_be_bytes([buf[3], buf[4]]);
// Validity of length value will be checked by rustls.
// Zero-length TLS record.
if record_body_len == 0 {
buf.clear();
return Ok(bytes);
}
let mut bytes_vec = bytes.as_bytes().to_vec();
bytes_vec.reserve(record_body_len as usize);
(Some(bytes_vec), record_body_len)
} else {
let buf = tls_record_header_buf.borrow_buf();
let remaining_record_body_len = u16::from_be_bytes([buf[3], buf[4]]);
(None, remaining_record_body_len)
};
// We have full record header and are in a process of receiving a record.
let bytes_obj = self.sock_recv(remaining_record_body_len as usize, vm)?;
let bytes = obj_to_bytes(bytes_obj)?;
if let Some(with_header) = with_header.as_mut() {
with_header.extend_from_slice(bytes.as_bytes());
}
let mut buf = tls_record_header_buf.borrow_buf_mut();
remaining_record_body_len -= bytes.len() as u16;
if remaining_record_body_len == 0 {
// Record received completely, need to start a new one beginning with its header.
buf.clear();
} else {
// Update remaining length in the header.
buf.as_mut_slice()[3..5].copy_from_slice(&remaining_record_body_len.to_be_bytes());
}
if let Some(with_header) = with_header {
Ok(vm.ctx.new_bytes(with_header))
} else {
Ok(bytes)
}
}
/// Socket send - just sends data, caller must handle pending flush
@@ -2800,13 +2894,8 @@ mod _ssl {
return write_method.call((vm.ctx.new_bytes(data.to_vec()),), vm);
}
// Normal socket mode
let socket_mod = vm.import("socket", 0)?;
let socket_class = socket_mod.get_attr("socket", vm)?;
// Call socket.socket.send(self.sock, data)
let send_method = socket_class.get_attr("send", vm)?;
send_method.call((self.sock.clone(), vm.ctx.new_bytes(data.to_vec())), vm)
self.sock_send_method
.call((self.sock.clone(), vm.ctx.new_bytes(data.to_vec())), vm)
}
/// Flush any pending TLS output data to the socket
@@ -2842,13 +2931,12 @@ mod _ssl {
socket_timeout
};
// Use sock_select directly with calculated timeout
// Use sock_wait directly with calculated timeout
let py_socket: PyRef<PySocket> = self.sock.clone().try_into_value(vm)?;
let socket = py_socket
.sock()
.map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?;
let timed_out = sock_select(&socket, SelectKind::Write, timeout_to_use)
.map_err(|e| vm.new_os_error(format!("select failed: {e}")))?;
let timed_out = sock_wait(&socket, SockWaitKind::Write, timeout_to_use, vm)?;
if timed_out {
// Keep unsent data in pending buffer
@@ -2909,7 +2997,7 @@ mod _ssl {
let mut sent_total = 0;
while sent_total < buf.len() {
let timed_out = self.sock_wait_for_io_impl(SelectKind::Write, vm)?;
let timed_out = self.sock_wait_for_io_impl(SockWaitKind::Write, vm)?;
if timed_out {
// Save unsent data to pending buffer
self.pending_tls_output
@@ -2981,8 +3069,7 @@ mod _ssl {
let socket = py_socket
.sock()
.map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?;
let timed_out = sock_select(&socket, SelectKind::Write, timeout)
.map_err(|e| vm.new_os_error(format!("select failed: {e}")))?;
let timed_out = sock_wait(&socket, SockWaitKind::Write, timeout, vm)?;
if timed_out {
return Err(
@@ -2998,7 +3085,7 @@ mod _ssl {
let mut pending = self.pending_tls_output.lock();
pending.drain(..sent);
}
// If sent == 0, loop will retry with sock_select
// If sent == 0, loop will retry with sock_wait
}
Err(e) => {
if is_blocking_io_error(&e, vm) {
@@ -3112,7 +3199,7 @@ mod _ssl {
/// Returns the configured ServerConnection.
fn initialize_server_connection(
&self,
conn_guard: &mut Option<TlsConnection>,
conn_guard: &mut Option<Connection>,
vm: &VirtualMachine,
) -> PyResult<()> {
let ctx = self.context.read();
@@ -3282,11 +3369,11 @@ mod _ssl {
vm.new_value_error(format!("Failed to create server connection: {e}"))
})?;
*conn_guard = Some(TlsConnection::Server(conn));
*conn_guard = Some(Connection::Server(conn));
// If ClientHello buffer exists (from SNI callback), re-inject it
if let Some(ref hello_data) = *self.client_hello_buffer.lock()
&& let Some(TlsConnection::Server(ref mut server)) = *conn_guard
&& let Some(Connection::Server(ref mut server)) = *conn_guard
{
let mut cursor = std::io::Cursor::new(hello_data.as_slice());
let _ = server.read_tls(&mut cursor);
@@ -3409,14 +3496,14 @@ mod _ssl {
vm.new_value_error(format!("Failed to create client connection: {e}"))
})?;
*conn_guard = Some(TlsConnection::Client(conn));
*conn_guard = Some(Connection::Client(conn));
}
}
// Perform the actual handshake by exchanging data with the socket/BIO
let conn = conn_guard.as_mut().expect("unreachable");
let is_client = matches!(conn, TlsConnection::Client(_));
let is_client = matches!(conn, Connection::Client(_));
let handshake_result = ssl_do_handshake(conn, self, vm);
drop(conn_guard);
@@ -3442,6 +3529,7 @@ mod _ssl {
// Now safe to call Python callback (no locks held)
self.invoke_sni_callback(sni_name.as_deref(), vm)?;
*self.sni_callback_processed.lock() = true;
// Clear connection to trigger recreation
*self.connection.lock() = None;
@@ -4171,7 +4259,7 @@ mod _ssl {
// Wait for socket to be readable
let timed_out = self.sock_wait_for_io_with_timeout(
SelectKind::Read,
SockWaitKind::Read,
remaining_timeout,
vm,
)?;
@@ -4242,7 +4330,7 @@ mod _ssl {
}
// Helper: Write all pending TLS data (including close_notify) to outgoing buffer/BIO
fn write_pending_tls(&self, conn: &mut TlsConnection, vm: &VirtualMachine) -> PyResult<()> {
fn write_pending_tls(&self, conn: &mut Connection, vm: &VirtualMachine) -> PyResult<()> {
// First, flush any previously pending TLS output
// Must succeed before sending new data to maintain order
self.flush_pending_tls_output(vm, None)?;
@@ -4252,7 +4340,7 @@ mod _ssl {
break;
}
let mut buf = vec![0u8; SSL3_RT_MAX_PLAIN_LENGTH];
let mut buf = vec![0u8; SSL3_RT_MAX_PACKET_SIZE];
let written = conn
.write_tls(&mut buf.as_mut_slice())
.map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?;
@@ -4272,7 +4360,7 @@ mod _ssl {
// Returns true if peer closed connection (with or without close_notify)
fn try_read_close_notify(
&self,
conn: &mut TlsConnection,
conn: &mut Connection,
vm: &VirtualMachine,
) -> PyResult<bool> {
// In socket mode, peek first to avoid consuming post-TLS cleartext
@@ -4280,11 +4368,11 @@ mod _ssl {
// transitions to cleartext. Without peeking, sock_recv may consume
// cleartext data meant for the application after unwrap().
if self.incoming_bio.is_none() {
return self.try_read_close_notify_socket(conn, vm);
return Ok(self.try_read_close_notify_socket(conn, vm));
}
// BIO mode: read from incoming BIO
match self.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
match self.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) {
Ok(bytes_obj) => {
let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?;
let data = bytes.borrow_buf();
@@ -4323,81 +4411,31 @@ mod _ssl {
///
/// Equivalent to OpenSSL's `SSL_set_read_ahead(ssl, 0)` — rustls has no
/// such knob, so we enforce record-level reads manually via peek.
fn try_read_close_notify_socket(
&self,
conn: &mut TlsConnection,
vm: &VirtualMachine,
) -> PyResult<bool> {
// Peek at the first 5 bytes (TLS record header size)
let peeked_obj = match self.sock_peek(5, vm) {
Ok(obj) => obj,
Err(e) => {
if is_blocking_io_error(&e, vm) {
return Ok(false);
}
return Ok(true);
}
};
let peeked = ArgBytesLike::try_from_object(vm, peeked_obj)?;
let peek_data = peeked.borrow_buf();
if peek_data.is_empty() {
return Ok(true); // EOF
}
// TLS record content types: ChangeCipherSpec(20), Alert(21),
// Handshake(22), ApplicationData(23)
let content_type = peek_data[0];
if !(20..=23).contains(&content_type) {
// Not a TLS record - post-TLS cleartext data.
// Peer has completed TLS shutdown; don't consume this data.
return Ok(true);
}
// Determine how many bytes to read for exactly one TLS record
let recv_size = if peek_data.len() >= 5 {
let record_length = u16::from_be_bytes([peek_data[3], peek_data[4]]) as usize;
5 + record_length
} else {
// Partial header available - read just these bytes for now
peek_data.len()
};
drop(peek_data);
drop(peeked);
// Now consume exactly one TLS record from the socket
match self.sock_recv(recv_size, vm) {
Ok(bytes_obj) => {
let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?;
let data = bytes.borrow_buf();
fn try_read_close_notify_socket(&self, conn: &mut Connection, vm: &VirtualMachine) -> bool {
// Consume at most one TLS record from the socket
match self.sock_recv_at_most_one_tls_record(vm) {
Ok(data) => {
if data.is_empty() {
return Ok(true);
return true;
}
let data_slice: &[u8] = data.as_ref();
let mut cursor = std::io::Cursor::new(data_slice);
let _ = conn.read_tls(&mut cursor);
let _ = conn.process_new_packets();
Ok(false)
false
}
Err(e) => {
if is_blocking_io_error(&e, vm) {
return Ok(false);
return false;
}
Ok(true)
true
}
}
}
// Helper: Check if peer has sent close_notify
fn check_peer_closed(
&self,
conn: &mut TlsConnection,
vm: &VirtualMachine,
) -> PyResult<bool> {
fn check_peer_closed(&self, conn: &mut Connection, vm: &VirtualMachine) -> PyResult<bool> {
// Process any remaining packets and check peer_has_closed
let io_state = conn
.process_new_packets()
@@ -4459,12 +4497,12 @@ mod _ssl {
let conn_guard = self.connection.lock();
if let Some(conn) = conn_guard.as_ref() {
let version = match conn {
TlsConnection::Client(_) => {
Connection::Client(_) => {
return Err(vm.new_value_error(
"Post-handshake authentication requires server socket",
));
}
TlsConnection::Server(server) => server.protocol_version(),
Connection::Server(server) => server.protocol_version(),
};
// Post-handshake auth is only available in TLS 1.3
+1 -1
View File
@@ -22,7 +22,7 @@ use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine};
use std::collections::HashSet;
use x509_parser::prelude::*;
use super::compat::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT};
use super::_ssl::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT};
// Certificate Verification Constants
+80 -324
View File
@@ -15,18 +15,17 @@
#[path = "../openssl/ssl_data_31.rs"]
mod ssl_data;
use crate::socket::{SelectKind, timeout_error_msg};
use crate::socket::{SockWaitKind, timeout_error_msg};
use crate::vm::VirtualMachine;
use alloc::sync::Arc;
use parking_lot::RwLock as ParkingRwLock;
use rustls::Connection;
use rustls::RootCertStore;
use rustls::client::ClientConfig;
use rustls::client::ClientConnection;
use rustls::crypto::SupportedKxGroup;
use rustls::pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer};
use rustls::server::ResolvesServerCert;
use rustls::server::ServerConfig;
use rustls::server::ServerConnection;
use rustls::sign::CertifiedKey;
use rustpython_vm::builtins::{PyBaseException, PyBaseExceptionRef};
use rustpython_vm::convert::IntoPyException;
@@ -36,7 +35,7 @@ use std::io::Read;
use std::sync::Once;
// Import PySSLSocket from parent module
use super::_ssl::PySSLSocket;
use super::_ssl::{PySSLSocket, SSL3_RT_MAX_PACKET_SIZE, VERIFY_X509_STRICT};
// Import error types and helper functions from error module
use super::error::{
@@ -44,16 +43,6 @@ use super::error::{
create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_zero_return_error,
};
// SSL Verification Flags
/// VERIFY_X509_STRICT flag for RFC 5280 strict compliance
/// When set, performs additional validation including AKI extension checks
pub(super) const VERIFY_X509_STRICT: i32 = 0x20;
/// VERIFY_X509_PARTIAL_CHAIN flag for partial chain validation
/// When set, accept certificates if any certificate in the chain is in the trust store
/// (not just root CAs). This matches OpenSSL's X509_V_FLAG_PARTIAL_CHAIN behavior.
pub(super) const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
// CryptoProvider Initialization:
/// Ensure the default CryptoProvider is installed (thread-safe, runs once)
@@ -73,10 +62,6 @@ fn ensure_default_provider() {
// OpenSSL Constants:
// OpenSSL TLS record maximum plaintext size (ssl/ssl_local.h)
// #define SSL3_RT_MAX_PLAIN_LENGTH 16384
const SSL3_RT_MAX_PLAIN_LENGTH: usize = 16384;
// OpenSSL error library codes (include/openssl/err.h)
// #define ERR_LIB_SSL 20
const ERR_LIB_SSL: i32 = 20;
@@ -95,74 +80,15 @@ const X509_V_FLAG_CRL_CHECK: i32 = 4;
// verification. They are used to map rustls certificate errors to OpenSSL
// error codes for compatibility.
pub(super) use x509::{
X509_V_ERR_CERT_HAS_EXPIRED, X509_V_ERR_CERT_NOT_YET_VALID, X509_V_ERR_CERT_REVOKED,
X509_V_ERR_HOSTNAME_MISMATCH, X509_V_ERR_INVALID_PURPOSE, X509_V_ERR_IP_ADDRESS_MISMATCH,
X509_V_ERR_UNABLE_TO_GET_CRL, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,
X509_V_ERR_UNSPECIFIED,
};
#[allow(dead_code)]
mod x509 {
pub(super) const X509_V_OK: i32 = 0;
pub(crate) const X509_V_ERR_UNSPECIFIED: i32 = 1;
pub(super) const X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: i32 = 2;
pub(crate) const X509_V_ERR_UNABLE_TO_GET_CRL: i32 = 3;
pub(super) const X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE: i32 = 4;
pub(super) const X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE: i32 = 5;
pub(super) const X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY: i32 = 6;
pub(super) const X509_V_ERR_CERT_SIGNATURE_FAILURE: i32 = 7;
pub(super) const X509_V_ERR_CRL_SIGNATURE_FAILURE: i32 = 8;
pub(crate) const X509_V_ERR_CERT_NOT_YET_VALID: i32 = 9;
pub(crate) const X509_V_ERR_CERT_HAS_EXPIRED: i32 = 10;
pub(super) const X509_V_ERR_CRL_NOT_YET_VALID: i32 = 11;
pub(super) const X509_V_ERR_CRL_HAS_EXPIRED: i32 = 12;
pub(super) const X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: i32 = 13;
pub(super) const X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: i32 = 14;
pub(super) const X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD: i32 = 15;
pub(super) const X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD: i32 = 16;
pub(super) const X509_V_ERR_OUT_OF_MEM: i32 = 17;
pub(super) const X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: i32 = 18;
pub(super) const X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: i32 = 19;
pub(crate) const X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: i32 = 20;
pub(super) const X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE: i32 = 21;
pub(super) const X509_V_ERR_CERT_CHAIN_TOO_LONG: i32 = 22;
pub(crate) const X509_V_ERR_CERT_REVOKED: i32 = 23;
pub(super) const X509_V_ERR_INVALID_CA: i32 = 24;
pub(super) const X509_V_ERR_PATH_LENGTH_EXCEEDED: i32 = 25;
pub(crate) const X509_V_ERR_INVALID_PURPOSE: i32 = 26;
pub(super) const X509_V_ERR_CERT_UNTRUSTED: i32 = 27;
pub(super) const X509_V_ERR_CERT_REJECTED: i32 = 28;
pub(super) const X509_V_ERR_SUBJECT_ISSUER_MISMATCH: i32 = 29;
pub(super) const X509_V_ERR_AKID_SKID_MISMATCH: i32 = 30;
pub(super) const X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH: i32 = 31;
pub(super) const X509_V_ERR_KEYUSAGE_NO_CERTSIGN: i32 = 32;
pub(super) const X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER: i32 = 33;
pub(super) const X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION: i32 = 34;
pub(super) const X509_V_ERR_KEYUSAGE_NO_CRL_SIGN: i32 = 35;
pub(super) const X509_V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION: i32 = 36;
pub(super) const X509_V_ERR_INVALID_NON_CA: i32 = 37;
pub(super) const X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED: i32 = 38;
pub(super) const X509_V_ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE: i32 = 39;
pub(super) const X509_V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED: i32 = 40;
pub(super) const X509_V_ERR_INVALID_EXTENSION: i32 = 41;
pub(super) const X509_V_ERR_INVALID_POLICY_EXTENSION: i32 = 42;
pub(super) const X509_V_ERR_NO_EXPLICIT_POLICY: i32 = 43;
pub(super) const X509_V_ERR_DIFFERENT_CRL_SCOPE: i32 = 44;
pub(super) const X509_V_ERR_UNSUPPORTED_EXTENSION_FEATURE: i32 = 45;
pub(super) const X509_V_ERR_UNNESTED_RESOURCE: i32 = 46;
pub(super) const X509_V_ERR_PERMITTED_VIOLATION: i32 = 47;
pub(super) const X509_V_ERR_EXCLUDED_VIOLATION: i32 = 48;
pub(super) const X509_V_ERR_SUBTREE_MINMAX: i32 = 49;
pub(super) const X509_V_ERR_APPLICATION_VERIFICATION: i32 = 50;
pub(super) const X509_V_ERR_UNSUPPORTED_CONSTRAINT_TYPE: i32 = 51;
pub(super) const X509_V_ERR_UNSUPPORTED_CONSTRAINT_SYNTAX: i32 = 52;
pub(super) const X509_V_ERR_UNSUPPORTED_NAME_SYNTAX: i32 = 53;
pub(super) const X509_V_ERR_CRL_PATH_VALIDATION_ERROR: i32 = 54;
pub(crate) const X509_V_ERR_HOSTNAME_MISMATCH: i32 = 62;
pub(super) const X509_V_ERR_EMAIL_MISMATCH: i32 = 63;
pub(crate) const X509_V_ERR_IP_ADDRESS_MISMATCH: i32 = 64;
}
pub(super) const X509_V_ERR_UNSPECIFIED: i32 = 1;
pub(super) const X509_V_ERR_UNABLE_TO_GET_CRL: i32 = 3;
pub(super) const X509_V_ERR_CERT_NOT_YET_VALID: i32 = 9;
pub(super) const X509_V_ERR_CERT_HAS_EXPIRED: i32 = 10;
pub(super) const X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: i32 = 20;
pub(super) const X509_V_ERR_CERT_REVOKED: i32 = 23;
pub(super) const X509_V_ERR_INVALID_PURPOSE: i32 = 26;
pub(super) const X509_V_ERR_HOSTNAME_MISMATCH: i32 = 62;
pub(super) const X509_V_ERR_IP_ADDRESS_MISMATCH: i32 = 64;
// Certificate Error Conversion Functions:
@@ -263,126 +189,6 @@ pub(super) fn create_ssl_cert_verification_error(
Ok(exc.upcast())
}
/// Unified TLS connection type (client or server)
#[derive(Debug)]
pub(super) enum TlsConnection {
Client(ClientConnection),
Server(ServerConnection),
}
impl TlsConnection {
/// Check if handshake is in progress
pub(super) fn is_handshaking(&self) -> bool {
match self {
Self::Client(conn) => conn.is_handshaking(),
Self::Server(conn) => conn.is_handshaking(),
}
}
/// Check if connection wants to read data
pub(super) fn wants_read(&self) -> bool {
match self {
Self::Client(conn) => conn.wants_read(),
Self::Server(conn) => conn.wants_read(),
}
}
/// Check if connection wants to write data
pub(super) fn wants_write(&self) -> bool {
match self {
Self::Client(conn) => conn.wants_write(),
Self::Server(conn) => conn.wants_write(),
}
}
/// Read TLS data from socket
pub(super) fn read_tls(&mut self, reader: &mut dyn std::io::Read) -> std::io::Result<usize> {
match self {
Self::Client(conn) => conn.read_tls(reader),
Self::Server(conn) => conn.read_tls(reader),
}
}
/// Write TLS data to socket
pub(super) fn write_tls(&mut self, writer: &mut dyn std::io::Write) -> std::io::Result<usize> {
match self {
Self::Client(conn) => conn.write_tls(writer),
Self::Server(conn) => conn.write_tls(writer),
}
}
/// Process new TLS packets
pub(super) fn process_new_packets(&mut self) -> Result<rustls::IoState, rustls::Error> {
match self {
Self::Client(conn) => conn.process_new_packets(),
Self::Server(conn) => conn.process_new_packets(),
}
}
/// Get reader for plaintext data (rustls native type)
pub(super) fn reader(&mut self) -> rustls::Reader<'_> {
match self {
Self::Client(conn) => conn.reader(),
Self::Server(conn) => conn.reader(),
}
}
/// Get writer for plaintext data (rustls native type)
pub(super) fn writer(&mut self) -> rustls::Writer<'_> {
match self {
Self::Client(conn) => conn.writer(),
Self::Server(conn) => conn.writer(),
}
}
/// Check if session was resumed
pub(super) fn is_session_resumed(&self) -> bool {
use rustls::HandshakeKind;
match self {
Self::Client(conn) => {
matches!(conn.handshake_kind(), Some(HandshakeKind::Resumed))
}
Self::Server(conn) => {
matches!(conn.handshake_kind(), Some(HandshakeKind::Resumed))
}
}
}
/// Send close_notify alert
pub(super) fn send_close_notify(&mut self) {
match self {
Self::Client(conn) => conn.send_close_notify(),
Self::Server(conn) => conn.send_close_notify(),
}
}
/// Get negotiated ALPN protocol
pub(super) fn alpn_protocol(&self) -> Option<&[u8]> {
match self {
Self::Client(conn) => conn.alpn_protocol(),
Self::Server(conn) => conn.alpn_protocol(),
}
}
/// Get negotiated cipher suite
pub(super) fn negotiated_cipher_suite(&self) -> Option<rustls::SupportedCipherSuite> {
match self {
Self::Client(conn) => conn.negotiated_cipher_suite(),
Self::Server(conn) => conn.negotiated_cipher_suite(),
}
}
/// Get peer certificates
pub(super) fn peer_certificates(
&self,
) -> Option<&[rustls::pki_types::CertificateDer<'static>]> {
match self {
Self::Client(conn) => conn.peer_certificates(),
Self::Server(conn) => conn.peer_certificates(),
}
}
}
/// Error types matching OpenSSL error codes
#[derive(Debug)]
pub(super) enum SslError {
@@ -584,6 +390,16 @@ impl SslError {
// Use the proper cert verification error creator
create_ssl_cert_verification_error(vm, &cert_err).expect("unlikely to happen")
}
Self::Io(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
create_ssl_eof_error(vm).upcast()
}
Self::Io(err) if err.raw_os_error().is_none() => vm
.new_os_subtype_error(
PySSLError::class(&vm.ctx).to_owned(),
None,
format!("SSL error: {err}"),
)
.upcast(),
Self::Io(err) => err.into_pyexception(vm),
Self::SniCallbackRestart => {
// This should be handled at PySSLSocket level
@@ -1058,11 +874,11 @@ fn send_all_bytes(
));
}
socket
.sock_wait_for_io_with_timeout(SelectKind::Write, Some(dl - now), vm)
.sock_wait_for_io_with_timeout(SockWaitKind::Write, Some(dl - now), vm)
.map_err(SslError::Py)?
} else {
socket
.sock_wait_for_io_impl(SelectKind::Write, vm)
.sock_wait_for_io_impl(SockWaitKind::Write, vm)
.map_err(SslError::Py)?
};
if timed_out {
@@ -1120,7 +936,7 @@ fn send_all_bytes(
/// Drains all pending TLS data from rustls and sends it to the peer.
/// Returns whether any progress was made.
fn handshake_write_loop(
conn: &mut TlsConnection,
conn: &mut Connection,
socket: &PySSLSocket,
force_initial_write: bool,
vm: &VirtualMachine,
@@ -1163,14 +979,10 @@ fn handshake_write_loop(
Ok(made_progress)
}
/// Read TLS handshake data from socket/BIO
/// Read at most one TLS record from the TCP socket.
///
/// Waits for and reads TLS records from the peer, handling SNI callback setup.
/// Returns (made_progress, is_first_sni_read).
/// TLS record header size (content_type + version + length).
const TLS_RECORD_HEADER_SIZE: usize = 5;
/// Read exactly one TLS record from the TCP socket.
/// May return incomplete data but never returns more when completes a
/// previously incomplete TLS record.
///
/// OpenSSL reads one TLS record at a time (no read-ahead by default).
/// Rustls, however, consumes all available TCP data when fed via read_tls().
@@ -1183,77 +995,32 @@ const TLS_RECORD_HEADER_SIZE: usize = 5;
/// Fix: peek at the TCP buffer to find the first complete TLS record boundary
/// and recv() only that many bytes. Any remaining data stays in the kernel
/// buffer and remains visible to select().
fn recv_one_tls_record(socket: &PySSLSocket, vm: &VirtualMachine) -> SslResult<PyObjectRef> {
// Peek at what is available without consuming it.
let peeked_obj = match socket.sock_peek(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
Ok(d) => d,
Err(e) => {
if is_blocking_io_error(&e, vm) {
return Err(SslError::WantRead);
}
return Err(SslError::Py(e));
}
};
let peeked = ArgBytesLike::try_from_object(vm, peeked_obj)
.map_err(|_| SslError::Syscall("Expected bytes-like object from peek".to_string()))?;
let peeked_bytes = peeked.borrow_buf();
if peeked_bytes.is_empty() {
// Empty peek means the peer has closed the TCP connection (FIN).
// Non-blocking sockets would have returned EAGAIN/EWOULDBLOCK
// (caught above as WantRead), so empty bytes here always means EOF.
return Err(SslError::Eof);
}
if peeked_bytes.len() < TLS_RECORD_HEADER_SIZE {
// Not enough data for a TLS record header yet.
// Read all available bytes so rustls can buffer the partial header;
// this avoids busy-waiting because the kernel buffer is now empty
// and select() will only wake us when new data arrives.
return socket.sock_recv(peeked_bytes.len(), vm).map_err(|e| {
if is_blocking_io_error(&e, vm) {
SslError::WantRead
} else {
SslError::Py(e)
}
});
}
// Parse the TLS record length from the header.
let record_body_len = u16::from_be_bytes([peeked_bytes[3], peeked_bytes[4]]) as usize;
let total_record_size = TLS_RECORD_HEADER_SIZE + record_body_len;
let recv_size = if peeked_bytes.len() >= total_record_size {
// Complete record available — consume exactly one record.
total_record_size
} else {
// Incomplete record — consume everything so the kernel buffer is
// drained and select() will block until more data arrives.
peeked_bytes.len()
};
// Must drop the borrow before calling sock_recv (which re-enters Python).
drop(peeked_bytes);
drop(peeked);
socket.sock_recv(recv_size, vm).map_err(|e| {
fn recv_at_most_one_tls_record(
socket: &PySSLSocket,
vm: &VirtualMachine,
) -> SslResult<PyObjectRef> {
let bytes = socket.sock_recv_at_most_one_tls_record(vm).map_err(|e| {
if is_blocking_io_error(&e, vm) {
SslError::WantRead
} else {
SslError::Py(e)
}
})
})?;
if bytes.is_empty() {
Err(SslError::Eof)
} else {
Ok(bytes.into())
}
}
/// Read a single TLS record for post-handshake I/O while preserving the
/// Read up to a single TLS record for post-handshake I/O while preserving the
/// SSL-vs-socket error precedence from the old sock_recv() path.
fn recv_one_tls_record_for_data(
conn: &mut TlsConnection,
fn recv_at_most_one_tls_record_for_data(
conn: &mut Connection,
socket: &PySSLSocket,
vm: &VirtualMachine,
) -> SslResult<PyObjectRef> {
match recv_one_tls_record(socket, vm) {
match recv_at_most_one_tls_record(socket, vm) {
Ok(data) => Ok(data),
Err(SslError::Eof) => {
if let Err(rustls_err) = conn.process_new_packets() {
@@ -1275,7 +1042,7 @@ fn recv_one_tls_record_for_data(
}
fn handshake_read_data(
conn: &mut TlsConnection,
conn: &mut Connection,
socket: &PySSLSocket,
is_bio: bool,
is_server: bool,
@@ -1285,14 +1052,15 @@ fn handshake_read_data(
return Ok((false, false));
}
// SERVER-SPECIFIC: Check if this is the first read (for SNI callback)
// Must check BEFORE reading data, so we can detect first time
let is_first_sni_read = is_server && socket.is_first_sni_read();
// SERVER-SPECIFIC: Check if this is before the SNI callback.
// sock_recv() may return only part of a TLS record, so keep capturing
// ClientHello fragments until process_new_packets() has produced a response.
let is_first_sni_read = is_server && socket.has_sni_callback() && socket.is_first_sni_read();
// Wait for data in socket mode
if !is_bio {
let timed_out = socket
.sock_wait_for_io_impl(SelectKind::Read, vm)
.sock_wait_for_io_impl(SockWaitKind::Read, vm)
.map_err(SslError::Py)?;
if timed_out {
@@ -1308,9 +1076,9 @@ fn handshake_read_data(
// record. This matches OpenSSL's default (no read-ahead) behaviour
// and keeps remaining data in the kernel buffer where select() can
// detect it.
recv_one_tls_record(socket, vm)?
recv_at_most_one_tls_record(socket, vm)?
} else {
match socket.sock_recv(SSL3_RT_MAX_PLAIN_LENGTH, vm) {
match socket.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) {
Ok(d) => d,
Err(e) => {
if is_blocking_io_error(&e, vm) {
@@ -1324,7 +1092,7 @@ fn handshake_read_data(
}
};
// SERVER-SPECIFIC: Save ClientHello on first read for potential connection recreation
// SERVER-SPECIFIC: Save ClientHello fragments for potential connection recreation.
if is_first_sni_read {
// Extract bytes from PyObjectRef
use rustpython_vm::builtins::PyBytes;
@@ -1344,7 +1112,7 @@ fn handshake_read_data(
/// Tries to send NewSessionTicket in non-blocking mode to avoid deadlocks.
/// Returns true if handshake is complete and we should exit.
fn handle_handshake_complete(
conn: &mut TlsConnection,
conn: &mut Connection,
socket: &PySSLSocket,
_is_server: bool,
vm: &VirtualMachine,
@@ -1419,7 +1187,7 @@ fn handle_handshake_complete(
///
/// Returns Ok(Some(n)) if n bytes were read, Ok(None) if would block,
/// or Err on real errors.
fn try_read_plaintext(conn: &mut TlsConnection, buf: &mut [u8]) -> SslResult<Option<usize>> {
fn try_read_plaintext(conn: &mut Connection, buf: &mut [u8]) -> SslResult<Option<usize>> {
let mut reader = conn.reader();
match reader.read(buf) {
Ok(0) => {
@@ -1448,7 +1216,7 @@ fn try_read_plaintext(conn: &mut TlsConnection, buf: &mut [u8]) -> SslResult<Opt
///
/// = SSL_do_handshake()
pub(super) fn ssl_do_handshake(
conn: &mut TlsConnection,
conn: &mut Connection,
socket: &PySSLSocket,
vm: &VirtualMachine,
) -> SslResult<()> {
@@ -1458,12 +1226,9 @@ pub(super) fn ssl_do_handshake(
}
let is_bio = socket.is_bio_mode();
let is_server = matches!(conn, TlsConnection::Server(_));
let is_server = matches!(conn, Connection::Server(_));
let mut first_iteration = true; // Track if this is the first loop iteration
let mut iteration_count = 0;
loop {
iteration_count += 1;
let mut made_progress = false;
// IMPORTANT: In BIO mode, force initial write even if wants_write() is false
@@ -1506,10 +1271,10 @@ pub(super) fn ssl_do_handshake(
return Err(SslError::from_rustls(e));
}
// SERVER-SPECIFIC: Check SNI callback after processing packets
// SNI name is extracted during process_new_packets()
// Invoke callback on FIRST read if callback is configured, regardless of SNI presence
if is_server && is_first_sni_read && socket.has_sni_callback() {
// SERVER-SPECIFIC: Check SNI callback after processing packets.
// A partial TLS record can be read without producing any handshake
// response. Wait until rustls has processed a complete ClientHello.
if is_server && is_first_sni_read && socket.has_sni_callback() && conn.wants_write() {
// IMPORTANT: Do NOT call the callback here!
// The connection lock is still held, which would cause deadlock.
// Return SniCallbackRestart to signal do_handshake to:
@@ -1533,7 +1298,7 @@ pub(super) fn ssl_do_handshake(
if conn.wants_write() {
// Write all pending TLS data to outgoing BIO
loop {
let mut buf = vec![0u8; SSL3_RT_MAX_PLAIN_LENGTH];
let mut buf = vec![0u8; SSL3_RT_MAX_PACKET_SIZE];
let n = match conn.write_tls(&mut buf.as_mut_slice()) {
Ok(n) => n,
Err(_) => break,
@@ -1581,11 +1346,6 @@ pub(super) fn ssl_do_handshake(
if !should_continue {
break;
}
// Safety check: prevent truly infinite loops (should never happen)
if iteration_count > 1000 {
break;
}
}
// If we exit the loop without completing handshake, return appropriate error
@@ -1599,9 +1359,9 @@ pub(super) fn ssl_do_handshake(
return Err(SslError::WantRead);
}
// Neither wants_read nor wants_write - this is a real error
Err(SslError::Syscall(format!(
"SSL handshake failed: incomplete after {iteration_count} iterations",
)))
Err(SslError::Syscall(
"SSL handshake failed: incomplete handshake".to_string(),
))
} else {
// Handshake completed successfully (shouldn't reach here normally)
Ok(())
@@ -1615,7 +1375,7 @@ pub(super) fn ssl_do_handshake(
///
/// = SSL_read_ex()
pub(super) fn ssl_read(
conn: &mut TlsConnection,
conn: &mut Connection,
buf: &mut [u8],
socket: &PySSLSocket,
vm: &VirtualMachine,
@@ -1753,7 +1513,7 @@ pub(super) fn ssl_read(
// Blocking socket or socket with timeout: try to read more data from socket.
// Even though rustls says it doesn't want to read, more TLS records may arrive.
// Use single-record reading to avoid consuming close_notify alongside data.
let data = recv_one_tls_record_for_data(conn, socket, vm)?;
let data = recv_at_most_one_tls_record_for_data(conn, socket, vm)?;
let bytes_read = data
.clone()
@@ -1788,11 +1548,6 @@ pub(super) fn ssl_read(
// Successfully read and processed TLS data
// Continue loop to try reading plaintext
}
Err(SslError::Io(ref io_err)) if io_err.to_string().contains("message buffer full") => {
// This case should be rare now that ssl_read_tls_records handles buffer full
// Just continue loop to try again
continue;
}
Err(e) => {
// Other errors - check for buffered plaintext before propagating
match try_read_plaintext(conn, buf)? {
@@ -1817,7 +1572,7 @@ pub(super) fn ssl_read(
///
/// = SSL_write_ex()
pub(super) fn ssl_write(
conn: &mut TlsConnection,
conn: &mut Connection,
data: &[u8],
socket: &PySSLSocket,
vm: &VirtualMachine,
@@ -1944,7 +1699,7 @@ pub(super) fn ssl_write(
return Err(SslError::WantRead);
}
// For socket mode, try to read TLS data
let recv_result = socket.sock_recv(4096, vm).map_err(SslError::Py)?;
let recv_result = recv_at_most_one_tls_record_for_data(conn, socket, vm)?;
ssl_read_tls_records(conn, recv_result, false, vm)?;
conn.process_new_packets().map_err(SslError::from_rustls)?;
// Continue loop
@@ -1994,7 +1749,7 @@ pub(super) fn ssl_write(
// Helper functions (private-ish, used by public SSL functions)
/// Write TLS records from rustls to socket
fn ssl_write_tls_records(conn: &mut TlsConnection) -> SslResult<Vec<u8>> {
fn ssl_write_tls_records(conn: &mut Connection) -> SslResult<Vec<u8>> {
let mut buf = Vec::new();
let n = conn
.write_tls(&mut buf as &mut dyn std::io::Write)
@@ -2005,7 +1760,7 @@ fn ssl_write_tls_records(conn: &mut TlsConnection) -> SslResult<Vec<u8>> {
/// Read TLS records from socket to rustls
fn ssl_read_tls_records(
conn: &mut TlsConnection,
conn: &mut Connection,
data: PyObjectRef,
is_bio: bool,
vm: &VirtualMachine,
@@ -2068,6 +1823,9 @@ fn ssl_read_tls_records(
}
Ok(n) => {
offset += n;
if offset < bytes_data.len() {
conn.process_new_packets().map_err(SslError::from_rustls)?;
}
}
Err(e) => {
return Err(SslError::Io(e));
@@ -2075,14 +1833,12 @@ fn ssl_read_tls_records(
}
} else {
offset += read_bytes;
if offset < bytes_data.len() {
conn.process_new_packets().map_err(SslError::from_rustls)?;
}
}
}
Err(e) => {
// Check if it's a buffer full error (unlikely but handle it)
if e.to_string().contains("buffer full") {
conn.process_new_packets().map_err(SslError::from_rustls)?;
continue;
}
// Real error - propagate it
return Err(SslError::Io(e));
}
@@ -2118,7 +1874,7 @@ fn is_connection_closed_error(exc: &Py<PyBaseException>, vm: &VirtualMachine) ->
/// Ensure TLS data is available for reading
/// Returns the number of bytes read from the socket
fn ssl_ensure_data_available(
conn: &mut TlsConnection,
conn: &mut Connection,
socket: &PySSLSocket,
vm: &VirtualMachine,
) -> SslResult<usize> {
@@ -2140,7 +1896,7 @@ fn ssl_ensure_data_available(
{
// Socket has timeout - use select to enforce it
let timed_out = socket
.sock_wait_for_io_impl(SelectKind::Read, vm)
.sock_wait_for_io_impl(SockWaitKind::Read, vm)
.map_err(SslError::Py)?;
if timed_out {
// Socket not ready within timeout - raise socket.timeout
@@ -2157,9 +1913,9 @@ fn ssl_ensure_data_available(
// consuming a close_notify that arrives alongside application data,
// keeping it in the kernel buffer where select() can detect it.
let data = if !is_bio {
recv_one_tls_record_for_data(conn, socket, vm)?
recv_at_most_one_tls_record_for_data(conn, socket, vm)?
} else {
match socket.sock_recv(2048, vm) {
match socket.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) {
Ok(data) => data,
Err(e) => {
if is_blocking_io_error(&e, vm) {
-2
View File
@@ -125,7 +125,6 @@ pub(crate) mod ssl_error {
)
}
#[allow(dead_code, reason = "This seems like a false positive")]
pub(crate) fn create_ssl_zero_return_error(vm: &VirtualMachine) -> PyRef<PyOSError> {
vm.new_os_subtype_error(
PySSLZeroReturnError::class(&vm.ctx).to_owned(),
@@ -134,7 +133,6 @@ pub(crate) mod ssl_error {
)
}
#[allow(dead_code, reason = "This seems like a false positive")]
pub(crate) fn create_ssl_syscall_error(
vm: &VirtualMachine,
msg: impl Into<String>,
@@ -0,0 +1,88 @@
import os
import socket
import ssl
import sys
import threading
if sys.implementation.name.lower() != "rustpython":
print("Ignored: stdlib_ssl_short_recv (RustPython only)")
raise SystemExit
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
CERTFILE = os.path.join(ROOT_DIR, "Lib/test/certdata/keycert.pem")
DATA = b"x" * 128
orig_recv = socket.socket.recv
client_sockname = None
recv_n = {}
def new_recv(sock, bufsize, flags=0):
sockname = sock.getsockname()
if sockname not in recv_n:
recv_n[sockname] = 0
bufsize = 1
if flags & socket.MSG_PEEK == 0:
recv_n[sockname] += 1
return orig_recv(sock, bufsize, flags)
socket.socket.recv = new_recv
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
addr, port = listener.getsockname()
server_errors = []
def server():
try:
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(CERTFILE)
sock, _ = listener.accept()
sock.settimeout(5.0)
ssock = server_context.wrap_socket(sock, server_side=True)
try:
ssock.sendall(DATA)
finally:
ssock.close()
except BaseException as exc:
server_errors.append(exc)
finally:
listener.close()
thread = threading.Thread(target=server)
thread.start()
raw = socket.create_connection((addr, port), timeout=5.0)
client_sockname = raw.getsockname()
raw.settimeout(5.0)
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
client = client_context.wrap_socket(raw, server_hostname=None)
try:
chunks = []
while sum(len(chunk) for chunk in chunks) < len(DATA):
chunk = client.recv(20000)
if not chunk:
break
chunks.append(chunk)
finally:
client.close()
thread.join(10.0)
assert not thread.is_alive(), "server thread did not stop"
assert not server_errors, server_errors
assert b"".join(chunks) == DATA
assert len(recv_n) == 2
assert all(n > 100 for n in recv_n.values())
@@ -0,0 +1,246 @@
import os
import socket
import ssl
import sys
import threading
import time
import urllib.request
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
CERTFILE = os.path.join(ROOT_DIR, "Lib/test/certdata/keycert.pem")
BODY = b"x" * 407_676
# TLS record body sizes observed from https://crates.io/api/v1/crates/tokio.
TLS_RECORD_BODY_SIZES = [
2855,
281,
53,
218,
1095,
1395,
1395,
483,
1395,
1395,
1395,
1395,
48,
1360,
1354,
1395,
1395,
1395,
1367,
1395,
1395,
1395,
1395,
1326,
1395,
1395,
1395,
47,
1395,
1395,
1395,
1395,
95,
1395,
1332,
1287,
1388,
1395,
1395,
1374,
1395,
1380,
794,
791,
1395,
1381,
1395,
1395,
1395,
1333,
1395,
1395,
1395,
1395,
1395,
1395,
965,
16401,
3914,
2526,
1041,
8209,
9233,
16401,
11650,
10262,
7486,
3468,
692,
1041,
16401,
12242,
9466,
1041,
8209,
9233,
8209,
9233,
16401,
1041,
8209,
9233,
6161,
2065,
9233,
16401,
16358,
10806,
1041,
8209,
16401,
3914,
16401,
16401,
3089,
9233,
4642,
478,
8209,
3140,
1752,
9233,
8209,
8209,
16401,
16064,
14676,
13288,
2065,
16401,
1041,
8209,
16401,
1041,
6374,
1007,
]
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(CERTFILE)
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
addr, port = listener.getsockname()
server_errors = []
finished = False
def guard_timeout():
time.sleep(20)
if not finished:
print(
"stdlib_urllib_https_misaligned_recv.py timed out",
file=sys.stderr,
flush=True,
)
os.abort()
threading.Thread(target=guard_timeout, daemon=True).start()
def drain_outgoing(outgoing, conn):
while True:
try:
data = outgoing.read()
except ssl.SSLWantReadError:
return
if not data:
return
conn.sendall(data)
def run_server():
try:
conn, _ = listener.accept()
conn.settimeout(5.0)
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
incoming = ssl.MemoryBIO()
outgoing = ssl.MemoryBIO()
tls = server_context.wrap_bio(incoming, outgoing, server_side=True)
while True:
try:
tls.do_handshake()
break
except ssl.SSLWantReadError:
drain_outgoing(outgoing, conn)
incoming.write(conn.recv(65536))
except ssl.SSLWantWriteError:
pass
drain_outgoing(outgoing, conn)
request = b""
while b"\r\n\r\n" not in request:
try:
request += tls.read(65536)
except ssl.SSLWantReadError:
drain_outgoing(outgoing, conn)
incoming.write(conn.recv(65536))
drain_outgoing(outgoing, conn)
response = (
b"HTTP/1.1 200 OK\r\n"
b"Connection: close\r\n"
+ b"Content-Length: "
+ str(len(BODY)).encode()
+ b"\r\n"
+ b"Content-Type: application/json\r\n"
+ b"\r\n"
+ BODY
)
plaintext_sizes = [max(1, n - 17) for n in TLS_RECORD_BODY_SIZES]
pos = 0
while pos < len(response):
size = plaintext_sizes.pop(0) if plaintext_sizes else 16384
end = min(len(response), pos + size)
while pos < end:
try:
pos += tls.write(response[pos:end])
except ssl.SSLWantWriteError:
pass
drain_outgoing(outgoing, conn)
conn.close()
except BaseException as exc:
server_errors.append(exc)
finally:
listener.close()
thread = threading.Thread(target=run_server)
thread.start()
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.check_hostname = False
client_context.verify_mode = ssl.CERT_NONE
opener = urllib.request.build_opener(
urllib.request.ProxyHandler({}),
urllib.request.HTTPSHandler(context=client_context),
)
try:
with opener.open(f"https://{addr}:{port}/", timeout=5.0) as response:
body = response.read()
thread.join(10.0)
assert not thread.is_alive(), "server thread did not stop"
assert not server_errors, server_errors
assert body == BODY
finally:
finished = True