From 0b46934057c0fca3682b59a40f60edaec101c1fa Mon Sep 17 00:00:00 2001 From: joboet Date: Wed, 28 Oct 2026 17:20:27 +0100 Subject: [PATCH 1/2] std: use `_exit` for `process::exit` --- library/std/src/process.rs | 54 +++++++++----------- library/std/src/rt.rs | 5 -- library/std/src/sys/exit.rs | 98 ++++++------------------------------- 3 files changed, 39 insertions(+), 118 deletions(-) diff --git a/library/std/src/process.rs b/library/std/src/process.rs index 7afda60d019ec..6d9f230216d2c 100644 --- a/library/std/src/process.rs +++ b/library/std/src/process.rs @@ -2516,14 +2516,15 @@ impl Child { /// } /// ``` /// -/// In its current implementation, this function will execute exit handlers registered with `atexit` -/// as well as other platform-specific exit handlers (e.g. `fini` sections of ELF shared objects). -/// This means that Rust requires that all exit handlers are safe to execute at any time. In -/// particular, if an exit handler cleans up some state that might be concurrently accessed by other -/// threads, it is required that the exit handler performs suitable synchronization with those -/// threads. (The alternative to this requirement would be to not run exit handlers at all, which is -/// considered undesirable. Note that returning from `main` also calls `exit`, so making `exit` an -/// unsafe operation is not an option.) +/// In its current implementation, this function will *not* execute exit handlers +/// registered with `atexit` as well as other platform-specific exit handlers +/// (e.g. `fini` sections of ELF shared objects). Note however that this behavior +/// is subject to change. Previous Rust versions did in fact run exit handler, +/// but this was disabled for soundness reasons. Nevertheless, it is prudent to +/// ensure that all exit handlers are safe to execute at any time. In particular, +/// if an exit handler cleans up some state that might be concurrently accessed +/// by other threads it should perform suitable synchronization with those +/// threads. /// /// ## Platform-specific behavior /// @@ -2542,30 +2543,21 @@ impl Child { /// /// ### Safe interop with C code /// -/// On Unix, this function is currently implemented using the `exit` C function [`exit`][C-exit]. As -/// of C23, the C standard does not permit multiple threads to call `exit` concurrently. Rust -/// mitigates this with a lock, but if C code calls `exit`, that can still cause undefined behavior. -/// Note that returning from `main` is equivalent to calling `exit`. -/// -/// Therefore, it is undefined behavior to have two concurrent threads perform the following -/// without synchronization: -/// - One thread calls Rust's `exit` function or returns from Rust's `main` function -/// - Another thread calls the C function `exit` or `quick_exit`, or returns from C's `main` function -/// -/// Note that if a binary contains multiple copies of the Rust runtime (e.g., when combining -/// multiple `cdylib` or `staticlib`), they each have their own separate lock, so from the -/// perspective of code running in one of the Rust runtimes, the "outside" Rust code is basically C -/// code, and concurrent `exit` again causes undefined behavior. -/// -/// Individual C implementations might provide more guarantees than the standard and permit concurrent -/// calls to `exit`; consult the documentation of your C implementation for details. -/// -/// For some of the on-going discussion to make `exit` thread-safe in C, see: -/// - [Rust issue #126600](https://github.com/rust-lang/rust/issues/126600) -/// - [Austin Group Bugzilla (for POSIX)](https://austingroupbugs.net/view.php?id=1845) -/// - [GNU C library Bugzilla](https://sourceware.org/bugzilla/show_bug.cgi?id=31997) -/// +/// On Unix, this function is currently implemented using the [`_exit`] function. +/// Previous Rust version called the [`exit`][C-exit] C function, but this +/// resulted in multiple soundness issues as `exit` is +/// [not guaranteed to be thread-safe][issue-126600] and – depending on how they +/// are implemented – runs the destructors of thread-local variables, leading to +/// [double-free issues][issue-164243]. C's [`exit`][C-exit] function may only +/// soundly be called if both of these conditions are fulfilled: +/// - no other thread is currently exiting, be that through returning from `main` +/// or by calling `exit`. +/// - no thread-local variables are currently being referenced. +/// +/// [`_exit`]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/_Exit.html /// [C-exit]: https://en.cppreference.com/w/c/program/exit +/// [issue-126600]: https://github.com/rust-lang/rust/issues/126600 +/// [issue-164243]: https://github.com/rust-lang/rust/issues/164243 #[stable(feature = "rust1", since = "1.0.0")] #[cfg_attr(not(test), rustc_diagnostic_item = "process_exit")] pub fn exit(code: i32) -> ! { diff --git a/library/std/src/rt.rs b/library/std/src/rt.rs index 35d17d56ddd49..22edc22360241 100644 --- a/library/std/src/rt.rs +++ b/library/std/src/rt.rs @@ -183,12 +183,7 @@ fn lang_start_internal( 101 }); let ret_code = ret_code as isize; - cleanup(); - // Guard against multiple threads calling `libc::exit` concurrently. - // See the documentation for `unique_thread_exit` for more information. - crate::sys::exit::unique_thread_exit(); - ret_code }) .unwrap_or_else(handle_rt_panic) diff --git a/library/std/src/sys/exit.rs b/library/std/src/sys/exit.rs index 1ca2bd8f6b514..d9d2945dad77c 100644 --- a/library/std/src/sys/exit.rs +++ b/library/std/src/sys/exit.rs @@ -1,80 +1,3 @@ -cfg_select! { - target_os = "linux" => { - /// Mitigation for - /// - /// On glibc, `libc::exit` has been observed to not always be thread-safe. - /// It is currently unclear whether that is a glibc bug or allowed by the standard. - /// To mitigate this problem, we ensure that only one - /// Rust thread calls `libc::exit` (or returns from `main`) by calling this function before - /// calling `libc::exit` (or returning from `main`). - /// - /// Technically, this is not enough to ensure soundness, since other code directly calling - /// `libc::exit` will still race with this. - /// - /// *This function does not itself call `libc::exit`.* This is so it can also be used - /// to guard returning from `main`. - /// - /// This function will return only the first time it is called in a process. - /// - /// * If it is called again on the same thread as the first call, it will abort. - /// * If it is called again on a different thread, it will wait in a loop - /// (waiting for the process to exit). - pub fn unique_thread_exit() { - use crate::ffi::c_int; - use crate::ptr; - use crate::sync::atomic::AtomicPtr; - use crate::sync::atomic::Ordering::{Acquire, Relaxed}; - - static EXITING_THREAD_ID: AtomicPtr = AtomicPtr::new(ptr::null_mut()); - - // We use the address of `errno` as a cheap and safe way to identify - // threads. As the C standard mandates that `errno` must have thread - // storage duration, we can rely on its address not changing over the - // lifetime of the thread. Additionally, accesses to `errno` are - // async-signal-safe, so this function is available in all imaginable - // circumstances. - let this_thread_id = crate::sys::io::errno_location(); - match EXITING_THREAD_ID.compare_exchange( - ptr::null_mut(), - this_thread_id, - Acquire, - Relaxed, - ) { - Ok(_) => { - // This is the first thread to call `unique_thread_exit`, - // and this is the first time it is called. Continue exiting. - } - Err(exiting_thread_id) if exiting_thread_id == this_thread_id => { - // This is the first thread to call `unique_thread_exit`, - // but this is the second time it is called. - // Abort the process. - core::panicking::panic_nounwind("std::process::exit called re-entrantly") - } - Err(_) => { - // This is not the first thread to call `unique_thread_exit`. - // Pause until the process exits. - loop { - // Safety: libc::pause is safe to call. - unsafe { - libc::pause(); - } - } - } - } - } - } - _ => { - /// Mitigation for - /// - /// Mitigation is ***NOT*** implemented on this platform, either because this platform - /// is not affected, or because mitigation is not yet implemented for this platform. - #[cfg_attr(any(test, doctest), expect(dead_code))] - pub fn unique_thread_exit() { - // Mitigation not required on platforms where `exit` is thread-safe. - } - } -} - #[cfg(not(test))] cfg_select! { any(target_family = "unix", target_os = "wasi") => { @@ -94,10 +17,6 @@ cfg_select! { pub fn exit(code: i32) -> ! { cfg_select! { target_os = "hermit" => unsafe { hermit_abi::exit(code) }, - target_os = "linux" => unsafe { - unique_thread_exit(); - libc::exit(code) - }, target_os = "motor" => moto_rt::process::exit(code), all(target_vendor = "fortanix", target_env = "sgx") => { crate::sys::pal::abi::exit_with_code(code as _) @@ -129,8 +48,23 @@ pub fn exit(code: i32) -> ! { } crate::intrinsics::abort_immediate() } + // `exit` causes multiple soundness issues: + // 1. On glibc, `exit` has been observed to not always be thread-safe and + // it is currently unclear whether that is a glibc bug or allowed by + // the standard. See #126600 for more. + // 2. `exit` causes the destructors of C++ thread locals to be run. Since + // we use the same mechanisms of destructor registration on platforms + // like Linux (`__cxa_thread_atexit_impl`) and macOS (`_tlv_atexit`), + // this also applies to our `thread_local!` variables. Unfortunately, + // this behaviour can lead to double-frees if the destructor of a TLS + // variable being overwritten calls `exit`. See #164243 for more. + // + // Thus we use `_exit`, which does not have these problems since it does + // not perform any cleanup. That's unfortunate, but pretty much + // unavoidable. A safe `process::exit` function cannot soundly use `exit` + // while 1. is not resolved. any(target_family = "unix", target_os = "wasi") => unsafe { - libc::exit(code as crate::ffi::c_int) + libc::_exit(code as crate::ffi::c_int) }, target_os = "vexos" => { let _ = code; From a73394e4ed52d71f3f8b0e23a60a2a2f19b56ba4 Mon Sep 17 00:00:00 2001 From: joboet Date: Thu, 29 Oct 2026 13:54:24 +0100 Subject: [PATCH 2/2] miri: add `_exit` shim --- .../miri/src/shims/unix/foreign_items.rs | 24 +++++++++++++++++++ .../pass/tls/thread_atexit.with_exit.stdout | 3 --- ...ls_macro_drop_main_thread.with_exit.stdout | 2 -- 3 files changed, 24 insertions(+), 5 deletions(-) delete mode 100644 src/tools/miri/tests/pass/tls/tls_macro_drop_main_thread.with_exit.stdout diff --git a/src/tools/miri/src/shims/unix/foreign_items.rs b/src/tools/miri/src/shims/unix/foreign_items.rs index 838e651a8a444..380f537375e3b 100644 --- a/src/tools/miri/src/shims/unix/foreign_items.rs +++ b/src/tools/miri/src/shims/unix/foreign_items.rs @@ -18,6 +18,7 @@ use crate::shims::alloc::EvalContextExt as _; use crate::shims::cpu_affinity::EvalContextExt as _; use crate::shims::unix::*; use crate::{shim_sig, *}; +use crate::eval::ExitType; pub fn is_dyn_sym(name: &str, target: &Target) -> bool { match name { @@ -1446,6 +1447,29 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { } } + // Terminating the process. + "_exit" => { + // FIXME: This does not have a direct test (#3179). + let [exit_code] = &this + .check_shim_sig(shim_sig!(extern "C" fn(i32) -> ()), (link_name, abi, args))?; + let exit_code = this.read_scalar(exit_code)?.to_i32()?; + // In GenMC mode, we do not immediately stop execution on exit. + if let Some(genmc_ctx) = this.machine.data_race.as_genmc_ref() { + // If there's no error, execution will continue (on another thread). + genmc_ctx.handle_exit( + this.machine.threads.active_thread(), + exit_code, + ExitType::ExitCalled, + )?; + } else { + // Stop interpreter loop. + throw_machine_stop!(TerminationInfo::Exit { + exit_code, + leak_check: false, + }); + } + } + // Platform-specific shims _ => { let target_os = &this.tcx.sess.target.os; diff --git a/src/tools/miri/tests/pass/tls/thread_atexit.with_exit.stdout b/src/tools/miri/tests/pass/tls/thread_atexit.with_exit.stdout index 89d6ca2593547..5c27831c338b3 100644 --- a/src/tools/miri/tests/pass/tls/thread_atexit.with_exit.stdout +++ b/src/tools/miri/tests/pass/tls/thread_atexit.with_exit.stdout @@ -2,6 +2,3 @@ exiting thread dtor 1 dtor 2 exiting main -registering dtor in dtor 3 -dtor 4 -dtor 5 diff --git a/src/tools/miri/tests/pass/tls/tls_macro_drop_main_thread.with_exit.stdout b/src/tools/miri/tests/pass/tls/tls_macro_drop_main_thread.with_exit.stdout deleted file mode 100644 index 6160f2726492d..0000000000000 --- a/src/tools/miri/tests/pass/tls/tls_macro_drop_main_thread.with_exit.stdout +++ /dev/null @@ -1,2 +0,0 @@ -Foo dtor -Bar dtor