From b9533635bdffcd5164a52b0756852d9eb5cc3794 Mon Sep 17 00:00:00 2001 From: James David Clarke Date: Wed, 27 May 2026 07:36:37 +0100 Subject: [PATCH 1/5] Fix stack overflow segfault on debug builds for Python threads Rust's std::thread::Builder defaults to a 2 MB stack when no size is set, which is too small for the call chains the Python stdlib runs on helper threads in debug builds (e.g. test.test_ssl's threaded server). CPython on glibc Linux relies on pthread's ~8 MB default instead. Apply 8 MB as the default in apply_thread_stack_size when the user has not explicitly called threading.stack_size(N). This matches CPython's effective default across builds while preserving the existing API: threading.stack_size() still returns 0 by default ("platform default"), and explicit user values still take precedence. Fixes #7941 Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/vm/src/stdlib/_thread.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 0af8d7add38..62ed615aba6 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -560,16 +560,25 @@ pub(crate) mod _thread { vm.state.thread_count.fetch_sub(1); } + /// Default stack size for Python threads when the user has not explicitly + /// called `threading.stack_size(N)`. Matches CPython's effective default + /// on glibc Linux (the pthread default), which is what CPython relies on + /// across builds. Rust's `std::thread::Builder` would otherwise default + /// to 2 MB, which is too small for the kinds of call chains the Python + /// stdlib runs on helper threads (e.g. the SSL test server). + const DEFAULT_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024; + fn apply_thread_stack_size( thread_builder: thread::Builder, vm: &VirtualMachine, ) -> thread::Builder { let configured = vm.state.stacksize.load(); - if configured != 0 { - thread_builder.stack_size(configured) + let size = if configured != 0 { + configured } else { - thread_builder - } + DEFAULT_THREAD_STACK_SIZE + }; + thread_builder.stack_size(size) } /// Clean up thread-local data for the current thread. From e4011c17564cd6083a833b91eda3f9a622ad92f7 Mon Sep 17 00:00:00 2001 From: James David Clarke Date: Wed, 27 May 2026 09:47:29 +0100 Subject: [PATCH 2/5] Add doc comment to apply_thread_stack_size Satisfy docstring coverage check on the PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/vm/src/stdlib/_thread.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 62ed615aba6..945ea36da73 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -568,6 +568,10 @@ pub(crate) mod _thread { /// stdlib runs on helper threads (e.g. the SSL test server). const DEFAULT_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024; + /// Configure a `thread::Builder` with the stack size to use for a new + /// Python thread. Uses the value set via `threading.stack_size(N)` when + /// the user has provided one (non-zero) and falls back to + /// [`DEFAULT_THREAD_STACK_SIZE`] otherwise. fn apply_thread_stack_size( thread_builder: thread::Builder, vm: &VirtualMachine, From 2a5e734a90a9c7e6c1d0128b1a99ed4c4245d22a Mon Sep 17 00:00:00 2001 From: James David Clarke Date: Wed, 27 May 2026 14:23:38 +0100 Subject: [PATCH 3/5] Retrigger CI for flaky multiprocessing tests Co-Authored-By: Claude Opus 4.7 (1M context) From c8b134e6134b51ebb287940bcdee465f55eca946 Mon Sep 17 00:00:00 2001 From: James David Clarke Date: Wed, 27 May 2026 16:57:22 +0100 Subject: [PATCH 4/5] Scope thread stack size override to debug builds only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit applied an 8 MB default unconditionally, which slowed down multiprocessing tests on release: many forked children each spawning many threads with oversized virtual stack mappings caused the flaky MP CI step to time out (60 min, vs ~9 min on main). Issue #7941 only manifested in debug builds — release builds were already fine on Rust's 2 MB std default. Restrict the 8 MB override to `#[cfg(debug_assertions)]` so release behavior is unchanged from before the fix, while the original SSL segfault on debug stays fixed. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/vm/src/stdlib/_thread.rs | 37 ++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 945ea36da73..7e18f8ad30f 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -560,29 +560,38 @@ pub(crate) mod _thread { vm.state.thread_count.fetch_sub(1); } - /// Default stack size for Python threads when the user has not explicitly - /// called `threading.stack_size(N)`. Matches CPython's effective default - /// on glibc Linux (the pthread default), which is what CPython relies on - /// across builds. Rust's `std::thread::Builder` would otherwise default - /// to 2 MB, which is too small for the kinds of call chains the Python - /// stdlib runs on helper threads (e.g. the SSL test server). + /// Default stack size for Python threads in **debug builds only**, where + /// Rust stack frames are substantially larger than in release. Rust's + /// `std::thread::Builder` otherwise defaults to 2 MB, which is too small + /// for the call chains the Python stdlib runs on helper threads in debug + /// (e.g. the SSL test server, see #7941). Release builds keep the prior + /// behavior — leave the stack size unset and let Rust's std default apply + /// — to avoid the multiprocessing slowdown observed when many forked + /// children spawn many threads with oversized virtual stack mappings. + #[cfg(debug_assertions)] const DEFAULT_THREAD_STACK_SIZE: usize = 8 * 1024 * 1024; /// Configure a `thread::Builder` with the stack size to use for a new /// Python thread. Uses the value set via `threading.stack_size(N)` when - /// the user has provided one (non-zero) and falls back to - /// [`DEFAULT_THREAD_STACK_SIZE`] otherwise. + /// the user has provided one (non-zero). Otherwise, debug builds fall + /// back to [`DEFAULT_THREAD_STACK_SIZE`] and release builds leave the + /// builder unmodified (Rust's std default applies). fn apply_thread_stack_size( thread_builder: thread::Builder, vm: &VirtualMachine, ) -> thread::Builder { let configured = vm.state.stacksize.load(); - let size = if configured != 0 { - configured - } else { - DEFAULT_THREAD_STACK_SIZE - }; - thread_builder.stack_size(size) + if configured != 0 { + return thread_builder.stack_size(configured); + } + #[cfg(debug_assertions)] + { + thread_builder.stack_size(DEFAULT_THREAD_STACK_SIZE) + } + #[cfg(not(debug_assertions))] + { + thread_builder + } } /// Clean up thread-local data for the current thread. From 54f5615155073ead9068baa57830f5e03172ec10 Mon Sep 17 00:00:00 2001 From: James David Clarke Date: Sun, 9 Aug 2026 19:43:53 +0100 Subject: [PATCH 5/5] Add regression test for debug thread stack size Verifies that a Python thread started without an explicit threading.stack_size() gets DEFAULT_THREAD_STACK_SIZE (8 MiB) in debug builds instead of Rust's 2 MiB std default, which overflowed and crashed in the SSL test server (#7941). The test reads back the spawned thread's actual stack size via pthread, fails before the fix and passes after it. --- crates/vm/src/stdlib/_thread.rs | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/crates/vm/src/stdlib/_thread.rs b/crates/vm/src/stdlib/_thread.rs index 27a30f1fa61..75ccdb40b98 100644 --- a/crates/vm/src/stdlib/_thread.rs +++ b/crates/vm/src/stdlib/_thread.rs @@ -1790,4 +1790,59 @@ pub(crate) mod _thread { Ok(handle_clone) } + + #[cfg(test)] + mod tests { + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + use super::*; + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + use crate::Interpreter; + + /// Regression test for #7941: a Python thread started without an + /// explicit `threading.stack_size()` must not run on Rust's 2 MiB + /// std default in debug builds, where the call chains the stdlib + /// runs on helper threads (e.g. the SSL test server) overflowed it + /// and crashed the process. Before the patch this observed the + /// Rust default and failed; after the patch it observes + /// [`DEFAULT_THREAD_STACK_SIZE`]. + #[test] + #[cfg(all(debug_assertions, any(target_os = "linux", target_os = "macos")))] + fn default_python_thread_stack_size_debug() { + Interpreter::without_stdlib(Default::default()).enter(|vm| { + // No user-configured stack size: the debug default must apply. + assert_eq!(vm.state.stacksize.load(), 0); + let builder = apply_thread_stack_size(thread::Builder::new(), vm); + let stack_size = builder + .spawn(current_thread_stack_size) + .expect("failed to spawn thread") + .join() + .expect("thread panicked"); + assert!( + stack_size >= DEFAULT_THREAD_STACK_SIZE, + "Python thread stack size is {stack_size} bytes, expected at least {DEFAULT_THREAD_STACK_SIZE}" + ); + }); + } + + #[cfg(all(debug_assertions, target_os = "linux"))] + fn current_thread_stack_size() -> usize { + use libc::{ + pthread_attr_destroy, pthread_attr_getstacksize, pthread_attr_t, + pthread_getattr_np, pthread_self, + }; + let mut attr: pthread_attr_t = unsafe { core::mem::zeroed() }; + unsafe { + assert_eq!(pthread_getattr_np(pthread_self(), &mut attr), 0); + let mut size = 0; + assert_eq!(pthread_attr_getstacksize(&attr, &mut size), 0); + pthread_attr_destroy(&mut attr); + size + } + } + + #[cfg(all(debug_assertions, target_os = "macos"))] + fn current_thread_stack_size() -> usize { + unsafe { libc::pthread_get_stacksize_np(libc::pthread_self()) } + } + } }