Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 23 additions & 31 deletions library/std/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand All @@ -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) -> ! {
Expand Down
5 changes: 0 additions & 5 deletions library/std/src/rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
98 changes: 16 additions & 82 deletions library/std/src/sys/exit.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,3 @@
cfg_select! {
target_os = "linux" => {
/// Mitigation for <https://github.com/rust-lang/rust/issues/126600>
///
/// 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<c_int> = 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 <https://github.com/rust-lang/rust/issues/126600>
///
/// 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") => {
Expand All @@ -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 _)
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/tools/miri/src/shims/unix/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {

@RalfJung RalfJung Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still do the "yield 100 times" thing here that exit does, by invoking replace_active_thread_stack. Looks like ExitType::ExitCalled needs a field saying whether atexit handlers should be invoked.

I can handle that if you prefer.

View changes since the review

exit_code,
leak_check: false,
});
}
}

// Platform-specific shims
_ => {
let target_os = &this.tcx.sess.target.os;
Expand Down
3 changes: 0 additions & 3 deletions src/tools/miri/tests/pass/tls/thread_atexit.with_exit.stdout

@RalfJung RalfJung Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the test call libc::exit directly so that we keep testing the same codepath as before here (ensuring that atexit handlers get invoked on exit).

View changes since the review

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,3 @@ exiting thread
dtor 1
dtor 2
exiting main
registering dtor in dtor 3
dtor 4
dtor 5

@RalfJung RalfJung Oct 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems worth a comment in the test that exit does not invoke the destructors so we will not get output in that case.

View changes since the review

This file was deleted.

Loading