rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
96.87k stars 12.51k forks source link

`LocalKey` dropped after calling `std::process::exit()` #127637

Open SteveBeeblebrox opened 2 months ago

SteveBeeblebrox commented 2 months ago

Unlike normal static variables (or the differently implemented unstable #[thread_local] statics), thread_local!/LocalKey statics are still dropped after calling std::process::exit().

I tried this code:

#![feature(thread_local)]
use std::sync::LazyLock;

#[derive(Debug)]
struct Droppable {
    name: &'static str
}

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("Dropping {}", self.name);
        println!("{}", std::backtrace::Backtrace::force_capture());
    }
}

impl Droppable {
    fn new(name: &'static str) -> Self {
        return Self {
            name
        };
    }
}

static SHARED_STATIC: LazyLock<Droppable> = LazyLock::new(|| Droppable::new("SHARED_STATIC"));

thread_local! {
    static TLS_STATIC: Droppable = Droppable::new("TLS_STATIC");
}

#[thread_local]
static UNSTABLE_THREAD_LOCAL: LazyLock<Droppable> = LazyLock::new(|| Droppable::new("UNSTABLE_THREAD_LOCAL"));

fn main() {
    // Force initialize
    LazyLock::force(&SHARED_STATIC);
    LazyLock::force(&UNSTABLE_THREAD_LOCAL);
    TLS_STATIC.with(|_| {});

    println!("std::process::exit(1)");
    std::process::exit(1);
}

Link to playground

I expected to see this happen: No output from drop() since the documentation for std::process::exit() makes a point of saying no destructors will be called and the program will exit immediately. When calling exit, I assumed that almost nothing happens after that.

Instead, this happened: The static variable wrapped with thread_local! had its drop implementation called after calling std::process::exit() meanwhile a normal static variable and one with the unstable #[thread_local] attribute did not.

Other notes: The same behavior occurs where only the thread_local! value is dropped when normally exiting from main().

LocalKey's documentation sort of makes it sound like this is intentional behavior that the destructors are run even when exiting the main thread (See "Platform-specific behavior" 1.). If this is working as intended, it would be nice if the documentation were slightly clearer.

Meta

rustc --version --verbose:

rustc 1.81.0-nightly (20ae37c18 2024-07-07)
binary: rustc
commit-hash: 20ae37c18df95f9246c019b04957d23b4164bf7a
commit-date: 2024-07-07
host: x86_64-unknown-linux-gnu
release: 1.81.0-nightly
LLVM version: 18.1.7
Backtrace

``` 0: ::drop at ./src/main.rs:12:24 1: core::ptr::drop_in_place at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/core/src/ptr/mod.rs:542:1 2: core::ptr::drop_in_place> at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/core/src/ptr/mod.rs:542:1 3: core::mem::drop at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/core/src/mem/mod.rs:938:24 4: std::sys::thread_local::native::lazy::destroy::{{closure}} at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/sys/thread_local/native/lazy.rs:99:9 5: std::sys::thread_local::abort_on_dtor_unwind at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/sys/thread_local/mod.rs:168:5 6: std::sys::thread_local::native::lazy::destroy at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/sys/thread_local/native/lazy.rs:94:5 7: __call_tls_dtors 8: 9: exit 10: std::sys::pal::unix::os::exit at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/sys/pal/unix/os.rs:761:14 11: std::process::exit at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/process.rs:2320:5 12: playground::main at ./src/main.rs:40:5 13: core::ops::function::FnOnce::call_once at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/core/src/ops/function.rs:250:5 14: std::sys::backtrace::__rust_begin_short_backtrace at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/sys/backtrace.rs:155:18 15: std::rt::lang_start::{{closure}} at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/rt.rs:159:18 16: core::ops::function::impls:: for &F>::call_once at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/core/src/ops/function.rs:284:13 17: std::panicking::try::do_call at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panicking.rs:553:40 18: std::panicking::try at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panicking.rs:517:19 19: std::panic::catch_unwind at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panic.rs:350:14 20: std::rt::lang_start_internal::{{closure}} at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/rt.rs:141:48 21: std::panicking::try::do_call at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panicking.rs:553:40 22: std::panicking::try at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panicking.rs:517:19 23: std::panic::catch_unwind at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/panic.rs:350:14 24: std::rt::lang_start_internal at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/rt.rs:141:20 25: std::rt::lang_start at /rustc/5315cbe15b79533f380bbb6685aa5480d5ff4ef5/library/std/src/rt.rs:158:17 26: main 27: __libc_start_main 28: _start ```

ChrisDenton commented 2 months ago

since the documentation for std::process::exit() makes a point of saying no destructors will be called and the program will exit immediately

To be clear, the documentation says this:

Note that because this function never returns, and that it terminates the process, no destructors on the current stack or any other thread’s stack will be run

The "on the current stack" part is important. atexit handlers are not on the stack and are invoked by the platform's exit routine (or equivalent), not by Rust.

workingjubilee commented 2 months ago

please feel free to send a PR improving the documentation!

Noratrieb commented 2 months ago

this relates to #126600, where it was clearly decided to have this function be C exit() and nothing more or less.

RalfJung commented 2 months ago

Seems like behavior matches documentation -- thread-local variables are not on the stack. What more could the docs say?

correabuscar commented 2 months ago

Seems like behavior matches documentation -- thread-local variables are not on the stack. What more could the docs say?

Would definitely be useful to know[^1] that atexit and on_exit handlers will be executed and that something that already makes use of them(as per the above comment) is the thread local vars and thus their destructors will be executed which means any drop() of a thread local static will execute.

But I'm pretty bad at expressing myself, else I would've made a PR to update the doc for it myself.

related: https://github.com/rust-lang/rust/issues/110897

Actually nevermind the following isn't right: and it only works due to the _exit(0) at the end. Ugly workaround which doesn't let TLS destructors run because it immediately exits(playground):

#![feature(thread_local)]

use libc::{_exit, atexit};
use std::sync::LazyLock;

#[derive(Debug)]
struct Droppable {
    name: &'static str,
}

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("Dropping {}", self.name);
        println!("{}", std::backtrace::Backtrace::force_capture());
    }
}

impl Droppable {
    fn new(name: &'static str) -> Self {
        Self { name }
    }
}

static SHARED_STATIC: LazyLock<Droppable> = LazyLock::new(|| Droppable::new("SHARED_STATIC"));

thread_local! {
    static TLS_STATIC: Droppable = Droppable::new("TLS_STATIC");
}

#[thread_local]
static UNSTABLE_THREAD_LOCAL: LazyLock<Droppable> =
    LazyLock::new(|| Droppable::new("UNSTABLE_THREAD_LOCAL"));

extern "C" fn exit_handler() {
    println!("Calling exit handler");
    unsafe {
        _exit(1); // The _exit function terminates the process immediately without calling any destructors.
    }
}

fn main() {
    // Register the exit handler
    unsafe {
        atexit(exit_handler as extern "C" fn());
    }

    // Force initialize
    LazyLock::force(&SHARED_STATIC);
    LazyLock::force(&UNSTABLE_THREAD_LOCAL);
    TLS_STATIC.with(|_| {});

    println!("Main function end");
    // The program will terminate here using _exit, bypassing destructors
    unsafe {
        _exit(0);
    }
}

[^1]: because it may not even cross some people's minds, like me

RalfJung commented 2 months ago

Thread locals do not use atexit handlers, they use yet another handler mechanism.

I don't know if we guarantee that the current thread's thread-locals will be dropped on exit; this might differ from platform to platform.