Skip to content

process::exit() + thread-locals can deadlock due to unique_thread_exit() #164273

Open

Description

@jsnell

In this code:

// `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;

static LOCK: Mutex<u32> = Mutex::new(0);

struct Waits;
impl Drop for Waits {
    fn drop(&mut self) {
        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! {
    static VALUE: Waits = Waits;
}

pub fn main() {
    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).

Nightly: https://rust.godbolt.org/z/93a4s1e3h

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 13, 2026
  2. maxdexh commented on Oct 13, 2026

    @maxdexh
    Member

    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

  3. maxdexh commented on Oct 13, 2026

    @maxdexh
    Member

    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

  4. jsnell commented on Oct 13, 2026

    @jsnell
    Author

    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.

  5. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    on Oct 14, 2026
  6. maxdexh commented on Oct 14, 2026

    @maxdexh
    Member

    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.

  7. added
    O-linuxOperating system: Linux
    A-thread-localsArea: Thread local storage (TLS)
    C-discussionCategory: Discussion or questions that doesn't represent real issues.
    O-linux-gnuOperating system: Linux with glibc (i.e. a bug that can't happen with musl)
    and removed
    C-bugCategory: This is a bug.
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    O-linuxOperating system: Linux
    on Oct 15, 2026
  8. ds84182 commented on Oct 15, 2026

    @ds84182

    Somewhat related lint: #158761

    The call to exit would trigger a lint on _held since execution diverges and won't be dropped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-thread-localsArea: Thread local storage (TLS)C-discussionCategory: Discussion or questions that doesn't represent real issues.O-linux-gnuOperating system: Linux with glibc (i.e. a bug that can't happen with musl)T-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions