diff --git a/library/std/src/rt.rs b/library/std/src/rt.rs index 0a841f07e3be7..3c2a447cf0655 100644 --- a/library/std/src/rt.rs +++ b/library/std/src/rt.rs @@ -117,6 +117,7 @@ pub(crate) fn thread_cleanup() { // print a nice message. panic::catch_unwind(|| { crate::thread::drop_current(); + crate::sys::thread_cleanup(); }) .unwrap_or_else(handle_rt_panic); } diff --git a/library/std/src/sys/pal/hermit/mod.rs b/library/std/src/sys/pal/hermit/mod.rs index b62afb40a615f..3f25cf937f15e 100644 --- a/library/std/src/sys/pal/hermit/mod.rs +++ b/library/std/src/sys/pal/hermit/mod.rs @@ -69,6 +69,8 @@ pub unsafe fn init(argc: isize, argv: *const *const u8, _sigpipe: u8) { } } +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // NOTE: this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() {} diff --git a/library/std/src/sys/pal/sgx/mod.rs b/library/std/src/sys/pal/sgx/mod.rs index 586ccd18c2f57..47d68ecd3485a 100644 --- a/library/std/src/sys/pal/sgx/mod.rs +++ b/library/std/src/sys/pal/sgx/mod.rs @@ -37,6 +37,8 @@ pub unsafe fn init(argc: isize, argv: *const *const u8, _sigpipe: u8) { } } +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // NOTE: this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() {} diff --git a/library/std/src/sys/pal/solid/mod.rs b/library/std/src/sys/pal/solid/mod.rs index d41042be51844..70099cfaa862e 100644 --- a/library/std/src/sys/pal/solid/mod.rs +++ b/library/std/src/sys/pal/solid/mod.rs @@ -38,6 +38,8 @@ pub mod time; // NOTE: this is not guaranteed to run, for example when Rust code is called externally. pub unsafe fn init(_argc: isize, _argv: *const *const u8, _sigpipe: u8) {} +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. pub unsafe fn cleanup() {} diff --git a/library/std/src/sys/pal/teeos/mod.rs b/library/std/src/sys/pal/teeos/mod.rs index 60a227afb84e3..3d785ff12b3fa 100644 --- a/library/std/src/sys/pal/teeos/mod.rs +++ b/library/std/src/sys/pal/teeos/mod.rs @@ -37,6 +37,8 @@ pub fn abort_internal() -> ! { // so this should never be called. pub fn init(argc: isize, argv: *const *const u8, sigpipe: u8) {} +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() { diff --git a/library/std/src/sys/pal/uefi/mod.rs b/library/std/src/sys/pal/uefi/mod.rs index c0ab52f650aa5..226765cc159d9 100644 --- a/library/std/src/sys/pal/uefi/mod.rs +++ b/library/std/src/sys/pal/uefi/mod.rs @@ -77,6 +77,8 @@ pub(crate) unsafe fn init(argc: isize, argv: *const *const u8, _sigpipe: u8) { } } +pub fn thread_cleanup() {} + /// # SAFETY /// this is not guaranteed to run, for example when the program aborts. /// - must be called only once during runtime cleanup. diff --git a/library/std/src/sys/pal/unix/mod.rs b/library/std/src/sys/pal/unix/mod.rs index 4fe18daa2040f..a5cf62c43105e 100644 --- a/library/std/src/sys/pal/unix/mod.rs +++ b/library/std/src/sys/pal/unix/mod.rs @@ -54,7 +54,7 @@ pub unsafe fn init(argc: isize, argv: *const *const u8, sigpipe: u8) { // behavior. reset_sigpipe(sigpipe); - stack_overflow::init(); + stack_overflow::protect(true); args::init(argc, argv); // Normally, `thread::spawn` will call `Thread::set_name` but since this thread @@ -228,12 +228,14 @@ pub(crate) fn on_broken_pipe_flag_used() -> bool { ON_BROKEN_PIPE_FLAG_USED.load(crate::sync::atomic::Ordering::Relaxed) } -// SAFETY: must be called only once during runtime cleanup. -// NOTE: this is not guaranteed to run, for example when the program aborts. -pub unsafe fn cleanup() { +pub fn thread_cleanup() { stack_overflow::cleanup(); } +// SAFETY: must be called only once during runtime cleanup. +// NOTE: this is not guaranteed to run, for example when the program aborts. +pub unsafe fn cleanup() {} + #[allow(unused_imports)] pub use libc::signal; diff --git a/library/std/src/sys/pal/unix/stack_overflow.rs b/library/std/src/sys/pal/unix/stack_overflow.rs index ac0858e1de876..076152e9ba482 100644 --- a/library/std/src/sys/pal/unix/stack_overflow.rs +++ b/library/std/src/sys/pal/unix/stack_overflow.rs @@ -1,29 +1,6 @@ #![cfg_attr(test, allow(dead_code))] -pub use self::imp::{cleanup, init}; -use self::imp::{drop_handler, make_handler}; - -pub struct Handler { - data: *mut libc::c_void, -} - -impl Handler { - pub unsafe fn new() -> Handler { - make_handler(false) - } - - fn null() -> Handler { - Handler { data: crate::ptr::null_mut() } - } -} - -impl Drop for Handler { - fn drop(&mut self) { - unsafe { - drop_handler(self.data); - } - } -} +pub use self::imp::{cleanup, protect}; #[cfg(any( target_os = "linux", @@ -45,12 +22,12 @@ mod imp { #[cfg(all(target_os = "linux", target_env = "gnu"))] use libc::{mmap64, mprotect, munmap}; - use super::Handler; - use crate::cell::Cell; use crate::ops::Range; use crate::sync::OnceLock; - use crate::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize, Ordering}; + use crate::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use crate::sys::pal::unix::os; + use crate::sys::thread_local::{guard, local_pointer}; + use crate::thread::Thread; use crate::{io, mem, ptr, thread}; // We use a TLS variable to store the address of the guard page. While TLS @@ -58,9 +35,11 @@ mod imp { // since we make sure to write to the variable before the signal stack is // installed, thereby ensuring that the variable is always allocated when // the signal handler is called. - thread_local! { - // FIXME: use `Range` once that implements `Copy`. - static GUARD: Cell<(usize, usize)> = const { Cell::new((0, 0)) }; + local_pointer! { + static GUARD_START; + static GUARD_END; + + static SIGALTSTACK; } // Signal handler for the SIGSEGV and SIGBUS handlers. We've got guard pages @@ -93,17 +72,18 @@ mod imp { info: *mut libc::siginfo_t, _data: *mut libc::c_void, ) { - let (start, end) = GUARD.get(); + let start = GUARD_START.get().addr(); + let end = GUARD_END.get().addr(); + // SAFETY: this pointer is provided by the system and will always point to a valid `siginfo_t`. let addr = unsafe { (*info).si_addr().addr() }; // If the faulting address is within the guard page, then we print a // message saying so and abort. if start <= addr && addr < end { - rtprintpanic!( - "\nthread '{}' has overflowed its stack\n", - thread::current().name().unwrap_or("") - ); + let thread = thread::try_current(); + let name = thread.as_ref().and_then(Thread::name).unwrap_or(""); + rtprintpanic!("\nthread '{name}' has overflowed its stack\n",); rtabort!("stack overflow"); } else { // Unregister ourselves by reverting back to the default behavior. @@ -118,51 +98,72 @@ mod imp { } static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0); - static MAIN_ALTSTACK: AtomicPtr = AtomicPtr::new(ptr::null_mut()); static NEED_ALTSTACK: AtomicBool = AtomicBool::new(false); + /// Set up stack overflow protection for the current thread + /// /// # Safety - /// Must be called only once + /// May only be called once per thread. #[forbid(unsafe_op_in_unsafe_fn)] - pub unsafe fn init() { - PAGE_SIZE.store(os::page_size(), Ordering::Relaxed); - - // Always write to GUARD to ensure the TLS variable is allocated. - let guard = unsafe { install_main_guard().unwrap_or(0..0) }; - GUARD.set((guard.start, guard.end)); - - // SAFETY: assuming all platforms define struct sigaction as "zero-initializable" - let mut action: sigaction = unsafe { mem::zeroed() }; - for &signal in &[SIGSEGV, SIGBUS] { - // SAFETY: just fetches the current signal handler into action - unsafe { sigaction(signal, ptr::null_mut(), &mut action) }; - // Configure our signal handler if one is not already set. - if action.sa_sigaction == SIG_DFL { - if !NEED_ALTSTACK.load(Ordering::Relaxed) { - // haven't set up our sigaltstack yet - NEED_ALTSTACK.store(true, Ordering::Release); - let handler = unsafe { make_handler(true) }; - MAIN_ALTSTACK.store(handler.data, Ordering::Relaxed); - mem::forget(handler); + pub unsafe fn protect(main_thread: bool) { + if main_thread { + PAGE_SIZE.store(os::page_size(), Ordering::Relaxed); + // Use acquire ordering to observe the page size store above, + // which is propagated by a release store to NEED_ALTSTACK. + } else if !NEED_ALTSTACK.load(Ordering::Acquire) { + return; + } + + let guard = if main_thread { + unsafe { install_main_guard().unwrap_or(0..0) } + } else { + unsafe { current_guard().unwrap_or(0..0) } + }; + + // Always store the guard range to ensure the TLS variables are allocated. + GUARD_START.set(ptr::without_provenance_mut(guard.start)); + GUARD_END.set(ptr::without_provenance_mut(guard.end)); + + if main_thread { + // SAFETY: assuming all platforms define struct sigaction as "zero-initializable" + let mut action: sigaction = unsafe { mem::zeroed() }; + for &signal in &[SIGSEGV, SIGBUS] { + // SAFETY: just fetches the current signal handler into action + unsafe { sigaction(signal, ptr::null_mut(), &mut action) }; + // Configure our signal handler if one is not already set. + if action.sa_sigaction == SIG_DFL { + if !NEED_ALTSTACK.load(Ordering::Relaxed) { + // Set up the signal stack and tell other threads to set + // up their own. This uses a release store to propagate + // the store to PAGE_SIZE above. + NEED_ALTSTACK.store(true, Ordering::Release); + unsafe { setup_sigaltstack() }; + } + + action.sa_flags = SA_SIGINFO | SA_ONSTACK; + action.sa_sigaction = signal_handler as sighandler_t; + // SAFETY: only overriding signals if the default is set + unsafe { sigaction(signal, &action, ptr::null_mut()) }; } - action.sa_flags = SA_SIGINFO | SA_ONSTACK; - action.sa_sigaction = signal_handler as sighandler_t; - // SAFETY: only overriding signals if the default is set - unsafe { sigaction(signal, &action, ptr::null_mut()) }; } + } else { + unsafe { setup_sigaltstack() }; } } /// # Safety - /// Must be called only once + /// Mutates the alternate signal stack #[forbid(unsafe_op_in_unsafe_fn)] - pub unsafe fn cleanup() { - // FIXME: I probably cause more bugs than I'm worth! - // see https://github.com/rust-lang/rust/issues/111272 - unsafe { drop_handler(MAIN_ALTSTACK.load(Ordering::Relaxed)) }; - } + unsafe fn setup_sigaltstack() { + // SAFETY: assuming stack_t is zero-initializable + let mut stack = unsafe { mem::zeroed() }; + // SAFETY: reads current stack_t into stack + unsafe { sigaltstack(ptr::null(), &mut stack) }; + // Do not overwrite the stack if one is already set. + if stack.ss_flags & SS_DISABLE == 0 { + return; + } - unsafe fn get_stack() -> libc::stack_t { // OpenBSD requires this flag for stack mapping // otherwise the said mapping will fail as a no-op on most systems // and has a different meaning on FreeBSD @@ -184,82 +185,60 @@ mod imp { let sigstack_size = sigstack_size(); let page_size = PAGE_SIZE.load(Ordering::Relaxed); - let stackp = mmap64( - ptr::null_mut(), - sigstack_size + page_size, - PROT_READ | PROT_WRITE, - flags, - -1, - 0, - ); - if stackp == MAP_FAILED { + let allocation = unsafe { + mmap64(ptr::null_mut(), sigstack_size + page_size, PROT_READ | PROT_WRITE, flags, -1, 0) + }; + if allocation == MAP_FAILED { panic!("failed to allocate an alternative stack: {}", io::Error::last_os_error()); } - let guard_result = libc::mprotect(stackp, page_size, PROT_NONE); + + let guard_result = unsafe { libc::mprotect(allocation, page_size, PROT_NONE) }; if guard_result != 0 { panic!("failed to set up alternative stack guard page: {}", io::Error::last_os_error()); } - let stackp = stackp.add(page_size); - libc::stack_t { ss_sp: stackp, ss_flags: 0, ss_size: sigstack_size } + let stack = libc::stack_t { + // Reserve a guard page at the bottom of the allocation. + ss_sp: unsafe { allocation.add(page_size) }, + ss_flags: 0, + ss_size: sigstack_size, + }; + // SAFETY: We warned our caller this would happen! + unsafe { + sigaltstack(&stack, ptr::null_mut()); + } + + // Ensure that `rt::thread_cleanup` gets called, which will in turn call + // cleanup, where this signal stack will be freed. + guard::enable(); + SIGALTSTACK.set(allocation.cast()); } - /// # Safety - /// Mutates the alternate signal stack - #[forbid(unsafe_op_in_unsafe_fn)] - pub unsafe fn make_handler(main_thread: bool) -> Handler { - if !NEED_ALTSTACK.load(Ordering::Acquire) { - return Handler::null(); + pub fn cleanup() { + let allocation = SIGALTSTACK.get(); + if allocation.is_null() { + return; } - if !main_thread { - // Always write to GUARD to ensure the TLS variable is allocated. - let guard = unsafe { current_guard() }.unwrap_or(0..0); - GUARD.set((guard.start, guard.end)); - } + SIGALTSTACK.set(ptr::null_mut()); - // SAFETY: assuming stack_t is zero-initializable - let mut stack = unsafe { mem::zeroed() }; - // SAFETY: reads current stack_t into stack - unsafe { sigaltstack(ptr::null(), &mut stack) }; - // Configure alternate signal stack, if one is not already set. - if stack.ss_flags & SS_DISABLE != 0 { - // SAFETY: We warned our caller this would happen! - unsafe { - stack = get_stack(); - sigaltstack(&stack, ptr::null_mut()); - } - Handler { data: stack.ss_sp as *mut libc::c_void } - } else { - Handler::null() - } - } + let sigstack_size = sigstack_size(); + let page_size = PAGE_SIZE.load(Ordering::Relaxed); - /// # Safety - /// Must be called - /// - only with our handler or nullptr - /// - only when done with our altstack - /// This disables the alternate signal stack! - #[forbid(unsafe_op_in_unsafe_fn)] - pub unsafe fn drop_handler(data: *mut libc::c_void) { - if !data.is_null() { - let sigstack_size = sigstack_size(); - let page_size = PAGE_SIZE.load(Ordering::Relaxed); - let disabling_stack = libc::stack_t { - ss_sp: ptr::null_mut(), - ss_flags: SS_DISABLE, - // Workaround for bug in macOS implementation of sigaltstack - // UNIX2003 which returns ENOMEM when disabling a stack while - // passing ss_size smaller than MINSIGSTKSZ. According to POSIX - // both ss_sp and ss_size should be ignored in this case. - ss_size: sigstack_size, - }; - // SAFETY: we warned the caller this disables the alternate signal stack! - unsafe { sigaltstack(&disabling_stack, ptr::null_mut()) }; - // SAFETY: We know from `get_stackp` that the alternate stack we installed is part of - // a mapping that started one page earlier, so walk back a page and unmap from there. - unsafe { munmap(data.sub(page_size), sigstack_size + page_size) }; - } + let disabling_stack = libc::stack_t { + ss_sp: ptr::null_mut(), + ss_flags: SS_DISABLE, + // Workaround for bug in macOS implementation of sigaltstack + // UNIX2003 which returns ENOMEM when disabling a stack while + // passing ss_size smaller than MINSIGSTKSZ. According to POSIX + // both ss_sp and ss_size should be ignored in this case. + ss_size: sigstack_size, + }; + unsafe { sigaltstack(&disabling_stack, ptr::null_mut()) }; + + // SAFETY: we created this mapping in `setup_sigaltstack` above with + // this exact size. + unsafe { munmap(allocation.cast(), sigstack_size + page_size) }; } /// Modern kernels on modern hardware can have dynamic signal stack sizes. @@ -578,13 +557,6 @@ mod imp { target_os = "illumos", )))] mod imp { - pub unsafe fn init() {} - - pub unsafe fn cleanup() {} - - pub unsafe fn make_handler(_main_thread: bool) -> super::Handler { - super::Handler::null() - } - - pub unsafe fn drop_handler(_data: *mut libc::c_void) {} + pub unsafe fn protect(_main_thread: bool) {} + pub fn cleanup() {} } diff --git a/library/std/src/sys/pal/unix/thread.rs b/library/std/src/sys/pal/unix/thread.rs index 2f2d6e6add396..399f67c4de044 100644 --- a/library/std/src/sys/pal/unix/thread.rs +++ b/library/std/src/sys/pal/unix/thread.rs @@ -98,10 +98,9 @@ impl Thread { extern "C" fn thread_start(main: *mut libc::c_void) -> *mut libc::c_void { unsafe { - // Next, set up our stack overflow handler which may get triggered if we run - // out of stack. - let _handler = stack_overflow::Handler::new(); - // Finally, let's run some code. + // Protect this thread against stack overflows + stack_overflow::protect(false); + // and run its main function. Box::from_raw(main as *mut Box)(); } ptr::null_mut() diff --git a/library/std/src/sys/pal/unsupported/common.rs b/library/std/src/sys/pal/unsupported/common.rs index 34a766683830d..ec046e226659a 100644 --- a/library/std/src/sys/pal/unsupported/common.rs +++ b/library/std/src/sys/pal/unsupported/common.rs @@ -4,6 +4,8 @@ use crate::io as std_io; // NOTE: this is not guaranteed to run, for example when Rust code is called externally. pub unsafe fn init(_argc: isize, _argv: *const *const u8, _sigpipe: u8) {} +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // NOTE: this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() {} diff --git a/library/std/src/sys/pal/windows/mod.rs b/library/std/src/sys/pal/windows/mod.rs index 1ea253e5e5263..711a4486809eb 100644 --- a/library/std/src/sys/pal/windows/mod.rs +++ b/library/std/src/sys/pal/windows/mod.rs @@ -60,6 +60,8 @@ pub unsafe fn init(_argc: isize, _argv: *const *const u8, _sigpipe: u8) { } } +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // NOTE: this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() { diff --git a/library/std/src/sys/pal/zkvm/mod.rs b/library/std/src/sys/pal/zkvm/mod.rs index 6ea057720296d..618fb2e3a4120 100644 --- a/library/std/src/sys/pal/zkvm/mod.rs +++ b/library/std/src/sys/pal/zkvm/mod.rs @@ -37,6 +37,8 @@ use crate::io as std_io; // NOTE: this is not guaranteed to run, for example when Rust code is called externally. pub unsafe fn init(_argc: isize, _argv: *const *const u8, _sigpipe: u8) {} +pub fn thread_cleanup() {} + // SAFETY: must be called only once during runtime cleanup. // NOTE: this is not guaranteed to run, for example when the program aborts. pub unsafe fn cleanup() {} diff --git a/tests/ui/runtime/out-of-stack.rs b/tests/ui/runtime/out-of-stack.rs index c5300635ad924..05f2aa44fb709 100644 --- a/tests/ui/runtime/out-of-stack.rs +++ b/tests/ui/runtime/out-of-stack.rs @@ -21,6 +21,7 @@ use std::env; use std::hint::black_box; use std::process::Command; use std::thread; +use std::cell::Cell; fn silent_recurse() { let buf = [0u8; 1000]; @@ -34,13 +35,34 @@ fn loud_recurse() { black_box(()); // don't optimize this into a tail call. please. } +fn in_tls_destructor(f: impl FnOnce() + 'static) { + struct RunOnDrop(Cell>>); + impl Drop for RunOnDrop { + fn drop(&mut self) { + self.0.take().unwrap()() + } + } + + thread_local! { + static RUN: RunOnDrop = RunOnDrop(Cell::new(None)); + } + + RUN.with(|run| run.0.set(Some(Box::new(f)))); +} + #[cfg(unix)] fn check_status(status: std::process::ExitStatus) { use std::os::unix::process::ExitStatusExt; assert!(!status.success()); + #[cfg(not(target_vendor = "apple"))] assert_eq!(status.signal(), Some(libc::SIGABRT)); + + // Apple's libc has a bug where calling abort in a TLS destructor on a thread + // other than the main thread results in a SIGTRAP instead of a SIGABRT. + #[cfg(target_vendor = "apple")] + assert!(matches!(status.signal(), Some(libc::SIGABRT | libc::SIGTRAP))); } #[cfg(not(unix))] @@ -49,40 +71,47 @@ fn check_status(status: std::process::ExitStatus) assert!(!status.success()); } - fn main() { let args: Vec = env::args().collect(); - if args.len() > 1 && args[1] == "silent" { - silent_recurse(); - } else if args.len() > 1 && args[1] == "loud" { - loud_recurse(); - } else if args.len() > 1 && args[1] == "silent-thread" { - thread::spawn(silent_recurse).join(); - } else if args.len() > 1 && args[1] == "loud-thread" { - thread::spawn(loud_recurse).join(); - } else { - let mut modes = vec![ - "silent-thread", - "loud-thread", - ]; - - // On linux it looks like the main thread can sometimes grow its stack - // basically without bounds, so we only test the child thread cases - // there. - if !cfg!(target_os = "linux") { - modes.push("silent"); - modes.push("loud"); + match args.get(1).map(String::as_str) { + Some("silent") => silent_recurse(), + Some("loud") => loud_recurse(), + Some("silent-thread") => thread::spawn(silent_recurse).join().unwrap(), + Some("loud-thread") => thread::spawn(loud_recurse).join().unwrap(), + Some("silent-tls") => in_tls_destructor(silent_recurse), + Some("loud-tls") => in_tls_destructor(loud_recurse), + Some("silent-thread-tls") => { + thread::spawn(|| in_tls_destructor(silent_recurse)).join().unwrap(); } - for mode in modes { - println!("testing: {}", mode); + Some("loud-thread-tls") => { + thread::spawn(|| in_tls_destructor(loud_recurse)).join().unwrap(); + } + _ => { + let mut modes = vec![ + "silent-thread", + "loud-thread", + "silent-thread-tls", + "loud-thread-tls", + ]; + + // On linux it looks like the main thread can sometimes grow its stack + // basically without bounds, so we only test the child thread cases + // there. + if !cfg!(target_os = "linux") { + modes.extend(["silent", "loud", "silent-tls", "loud-tls"]); + } + + for mode in modes { + println!("testing: {}", mode); - let silent = Command::new(&args[0]).arg(mode).output().unwrap(); + let silent = Command::new(&args[0]).arg(mode).output().unwrap(); - check_status(silent.status); + let error = String::from_utf8_lossy(&silent.stderr); + assert!(error.contains("has overflowed its stack"), + "missing overflow message: {}", error); - let error = String::from_utf8_lossy(&silent.stderr); - assert!(error.contains("has overflowed its stack"), - "missing overflow message: {}", error); + check_status(silent.status); + } } } }