Description
Calling exit
concurrently from Rust and C (or from 2 different copies of the Rust runtime in the same binary) is UB. This is permitted by the C standard, and can actually cause trouble in practice since not all versions of all libc implementations do proper locking when traversing the list of atexit
functions. For programs only using Rust, we mitigated this by adding a lock, but this only helps when all calls to exit
come from Rust (and they must come from the same Rust runtime). Note that returning from main
is equivalent to calling exit
so it can also cause this issue.
Current status
Mitigation on our end (only covers pure Rust programs): #126606
The current status is that the specification language stems from a time when C did not specify multithreading and the intent was to forbid reentrancy. External discussion indicates that there's intent to fix it both in libc implementations and the specs:
- glibc: https://sourceware.org/bugzilla/show_bug.cgi?id=31997
- POSIX: https://austingroupbugs.net/view.php?id=1845
- also a comment mentioning taking it to the C committee.
- libc-coord (including musl author): https://www.openwall.com/lists/libc-coord/2024/07/24/4
- musl patch: https://git.musl-libc.org/cgit/musl/commit/?id=8cca79a72cccbdb54726125d690d7d0095fc2409
- freebsd patch: https://reviews.freebsd.org/D46108
- bionic: https://android.googlesource.com/platform/bionic.git/+/089f4d17265480a5ad9311bcbd8890bc9f361801%5E%21/
Original bugreport (partially outdated)
The current implementation of std::process::exit
is unsound on Linux and possibly related platforms where it defers to libc's exit()
function. exit()
is documented to be not thread-safe, and hence std::process::exit
is not thread-safe either, despite being a non-unsafe
function.
To show that this isn't just a theoretical problem, here is a minimal example that segfaults on my machine (Ubuntu with glibc 2.37):
use std::thread;
fn main() {
for _ in 0..32 {
unsafe { libc::atexit(exit_handler) };
}
for _ in 0..2 {
thread::spawn(|| std::process::exit(0));
}
}
extern "C" fn exit_handler() {
thread::sleep_ms(1000);
}
The example contains unsafe
code, but only to install exit handlers. AFAICT nothing about the libc::atexit
call is unsafe. The UB is introduced by calling std::process::exit
concurrently afterwards.
If you are curious, https://github.com/MaterializeInc/database-issues/issues/6528 lays out what causes the segfault (it's a use-after-free). That's not terribly relevant though, given that glibc has no obligation to ensure exit()
is thread safe when it's clearly documented not to be. Instead, Rust should either mark std::process::exit
as unsafe
(which is something it could do for the 2024 edition), or introduce locking to ensure only a single thread gets to call exit()
at a time.