You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
process::exit() + thread-locals can deadlock due to unique_thread_exit() #164273
// `main` has returned, and the destructor of a `thread_local!` of the main thread waits for a `Mutex`. The thread that holds the `Mutex` calls `process::exit`. The process never ends.use std::sync::Mutex;use std::thread;use std::time::Duration;staticLOCK:Mutex<u32> = Mutex::new(0);structWaits;implDropforWaits{fndrop(&mutself){println!("2. the destructor of the main thread's value waits for the lock");let _held = LOCK.lock();println!("the destructor has the lock");}}thread_local!{staticVALUE:Waits = Waits;}pubfnmain(){VALUE.with(|_| {});
thread::spawn(|| {let _held = LOCK.lock().unwrap();
thread::sleep(Duration::from_millis(300));println!("3. the second thread, which holds the lock, calls exit(3)");
std::process::exit(3);});
thread::sleep(Duration::from_millis(100));println!("1. main returns");}
I expected to see this happen: the process prints 4 lines and exits
Instead, this happened: the process prints 3 lines and hangs
Meta
I'm filing this separately from #164243 since it seems like a distinct problem (IIUC in this case the actual problem is lock ordering with the implicit exit-guard spinlock from #126606).
Also what do you mean by "due to unique_thread_exit"? AFAICT, this is just caused by exit being called for the main thread first, which causes it to try to lock the mutex being held by the sleeping other thread.
Note that calling libc::exit on both threads is not an option, as that function is not thread-safe. After main has entered libc::exit (which is what calls the destructor), we cannot call it on another thread anymore
I don't think that's the case. If you remove the std::process::exit(3); call from the other thread, there's no deadlock, so the main thread wanting the lock isn't the sole contributor.
So my read of this is that main holds unique_thread_exit and wants LOCK, the other thread holds LOCK and wants unique_thread_exit.
Yes, you have two sources of mutual exclusion here.
On the one hand, the other thread is holding the mutex and the main thread is waiting for it.
On the other hand, the main thread has entered libc::exit, which means the other thread cannot enter it and has to wait (this is what unique_thread_exit is protecting).
Nothing can be done to resolve this, other than to not call destructors on exit. Since calling thread local destructors on exit is the normal behavior on this platform, this is expected behavior.
Activity
maxdexh commentedon Oct 13, 2026
I don't think this is an issue. The code relies on TLS destructors not being called on exit for correctness.
Equivalent code in C using glibc would also deadlock here
maxdexh commentedon Oct 13, 2026
Also what do you mean by "due to
unique_thread_exit"? AFAICT, this is just caused byexitbeing called for the main thread first, which causes it to try to lock the mutex being held by the sleeping other thread.Note that calling
libc::exiton both threads is not an option, as that function is not thread-safe. After main has enteredlibc::exit(which is what calls the destructor), we cannot call it on another thread anymorejsnell commentedon Oct 13, 2026
I don't think that's the case. If you remove the
std::process::exit(3);call from the other thread, there's no deadlock, so the main thread wanting the lock isn't the sole contributor.So my read of this is that main holds
unique_thread_exitand wantsLOCK, the other thread holdsLOCKand wantsunique_thread_exit.maxdexh commentedon Oct 14, 2026
Yes, you have two sources of mutual exclusion here.
On the one hand, the other thread is holding the mutex and the main thread is waiting for it.
On the other hand, the main thread has entered
libc::exit, which means the other thread cannot enter it and has to wait (this is whatunique_thread_exitis protecting).Nothing can be done to resolve this, other than to not call destructors on exit. Since calling thread local destructors on exit is the normal behavior on this platform, this is expected behavior.
ds84182 commentedon Oct 15, 2026
Somewhat related lint: #158761
The call to exit would trigger a lint on
_heldsince execution diverges and won't be dropped.