@@ -605,14 +605,35 @@ pub(crate) mod _thread {
605605 vm. state . thread_count . fetch_sub ( 1 ) ;
606606 }
607607
608+ /// Default stack size for Python threads in **debug builds only**, where
609+ /// Rust stack frames are substantially larger than in release. Rust's
610+ /// `std::thread::Builder` otherwise defaults to 2 MB, which is too small
611+ /// for the call chains the Python stdlib runs on helper threads in debug
612+ /// (e.g. the SSL test server, see #7941). Release builds keep the prior
613+ /// behavior — leave the stack size unset and let Rust's std default apply
614+ /// — to avoid oversized virtual stack mappings when many threads spawn.
615+ #[ cfg( debug_assertions) ]
616+ const DEFAULT_THREAD_STACK_SIZE : usize = 8 * 1024 * 1024 ;
617+
618+ /// Configure a `thread::Builder` with the stack size to use for a new
619+ /// Python thread. Uses the value set via `threading.stack_size(N)` when
620+ /// the user has provided one (non-zero). Otherwise, debug builds fall
621+ /// back to [`DEFAULT_THREAD_STACK_SIZE`] and release builds leave the
622+ /// builder unmodified (Rust's std default applies).
608623 fn apply_thread_stack_size (
609624 thread_builder : thread:: Builder ,
610625 vm : & VirtualMachine ,
611626 ) -> thread:: Builder {
612627 let configured = vm. state . stacksize . load ( ) ;
613628 if configured != 0 {
614- thread_builder. stack_size ( configured)
615- } else {
629+ return thread_builder. stack_size ( configured) ;
630+ }
631+ #[ cfg( debug_assertions) ]
632+ {
633+ thread_builder. stack_size ( DEFAULT_THREAD_STACK_SIZE )
634+ }
635+ #[ cfg( not( debug_assertions) ) ]
636+ {
616637 thread_builder
617638 }
618639 }
@@ -1996,4 +2017,55 @@ pub(crate) mod _thread {
19962017
19972018 Ok ( handle_clone)
19982019 }
2020+
2021+ #[ cfg( test) ]
2022+ mod tests {
2023+ #[ cfg( all( debug_assertions, any( target_os = "linux" , target_os = "macos" ) ) ) ]
2024+ use super :: * ;
2025+ #[ cfg( all( debug_assertions, any( target_os = "linux" , target_os = "macos" ) ) ) ]
2026+ use crate :: Interpreter ;
2027+
2028+ /// Regression test for #7941: a Python thread started without an
2029+ /// explicit `threading.stack_size()` must not run on Rust's 2 MiB
2030+ /// std default in debug builds, where the call chains the stdlib
2031+ /// runs on helper threads (e.g. the SSL test server) overflowed it.
2032+ #[ test]
2033+ #[ cfg( all( debug_assertions, any( target_os = "linux" , target_os = "macos" ) ) ) ]
2034+ fn default_python_thread_stack_size_debug ( ) {
2035+ Interpreter :: without_stdlib ( Default :: default ( ) ) . enter ( |vm| {
2036+ assert_eq ! ( vm. state. stacksize. load( ) , 0 ) ;
2037+ let builder = apply_thread_stack_size ( thread:: Builder :: new ( ) , vm) ;
2038+ let stack_size = builder
2039+ . spawn ( current_thread_stack_size)
2040+ . expect ( "failed to spawn thread" )
2041+ . join ( )
2042+ . expect ( "thread panicked" ) ;
2043+ assert ! (
2044+ stack_size >= DEFAULT_THREAD_STACK_SIZE ,
2045+ "Python thread stack size is {stack_size} bytes, expected at least {DEFAULT_THREAD_STACK_SIZE}"
2046+ ) ;
2047+ } ) ;
2048+ }
2049+
2050+ #[ cfg( all( debug_assertions, target_os = "linux" ) ) ]
2051+ fn current_thread_stack_size ( ) -> usize {
2052+ use libc:: {
2053+ pthread_attr_destroy, pthread_attr_getstacksize, pthread_attr_t,
2054+ pthread_getattr_np, pthread_self,
2055+ } ;
2056+ let mut attr: pthread_attr_t = unsafe { core:: mem:: zeroed ( ) } ;
2057+ unsafe {
2058+ assert_eq ! ( pthread_getattr_np( pthread_self( ) , & mut attr) , 0 ) ;
2059+ let mut size = 0 ;
2060+ assert_eq ! ( pthread_attr_getstacksize( & attr, & mut size) , 0 ) ;
2061+ pthread_attr_destroy ( & mut attr) ;
2062+ size
2063+ }
2064+ }
2065+
2066+ #[ cfg( all( debug_assertions, target_os = "macos" ) ) ]
2067+ fn current_thread_stack_size ( ) -> usize {
2068+ unsafe { libc:: pthread_get_stacksize_np ( libc:: pthread_self ( ) ) }
2069+ }
2070+ }
19992071}
0 commit comments