Skip to content

std::process::exit is not thread-safe in combination with C code calling exit #126600

Description

@teskje

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:

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.

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jun 17, 2024
  2. teskje commented on Jun 17, 2024

    @teskje
    ContributorAuthor

    This has been discussed previously in #83994. There the decision was to not do anything about the concerns with std::process::exit, mostly based on the reasoning that exit() is thread safe. The above example shows it's clearly not, neither according to the documentation nor in the implementation.

    Given that I just spend the better part of a weekend debugging a segfault that was caused by concurrent std::process::exit invocations I feel quite strongly that this issue deserves reconsideration.

  3. ChrisDenton commented on Jun 17, 2024

    @ChrisDenton
    Member

    On that thread joshtriplett notes:

    The same arguments apply here as they do with environment handling: some types of unsynchronized changes to the environment from C code could race with an otherwise-synchronized one from Rust on some platforms, but we don't mark the corresponding functions in std::env as unsafe.

    We do now mark std::env::set_var as unsafe so maybe thinking on this has changed. Though I'm not sure how you would mark returning from main as being unsafe?

  4. conradludgate commented on Jun 17, 2024

    @conradludgate
    Contributor

    Linux libc documentation does claim that exit() is thread-unsafe: "MT-Unsafe race:exit", although the original thread suggests that all libc implementations introduce appropriate locks. Perhaps introducing a Once before calling libc::exit is sufficient and not a performance blocker?

  5. the8472 commented on Jun 17, 2024

    @the8472
    Member

    Doesn't solve the problem with exit getting called from non-rust code. And no, an atexit handler won't help since it leaves a race-window and if you're in a situation where threads concurrently call exit you're already racing.

    https://github.com/MaterializeInc/database-issues/issues/6528 sounds like it's either a libc bug or the assessment from the previous thread that libc implementations provide the desired behavior - even if the standard language doesn't - needs to be revised.

  6. ChrisDenton commented on Jun 17, 2024

    @ChrisDenton
    Member

    I'd guess the safe thing to do is kill all other threads then call exit.

  7. the8472 commented on Jun 17, 2024

    @the8472
    Member

    Not sure if serious...

    Killing threads would make anything that accesses their stacks UB. Freezing them would work but that's difficult to implement reliably. Libc would be in a position to do it since they control pthreads. But then they might as well fix their locking.

  8. tbu- commented on Jun 17, 2024

    @tbu-
    Contributor

    Adding 32 atexit handlers seems to be essential to make this segfault on my glibc 2.39+r52+gf8e4623421-1 on Arch Linux. With 31 atexit handlers, it doesn't segfault.

  9. tbu- commented on Jun 17, 2024

    @tbu-
    Contributor

    I'd say it makes sense to introduce a lock on the Rust side, since pure-Rust code shouldn't be able to cause UB by calling libc's exit on different threads.

    This also means that Rust cdylibs mustn't call std::process::exit since they might not be the only language running, and applications must ensure that no foreign functions call exit. The latter should be a bug anyway due to the C standard saying that multiple calls to exit are UB.

  10. teskje commented on Jun 17, 2024

    @teskje
    ContributorAuthor

    https://github.com/MaterializeInc/database-issues/issues/6528 sounds like it's either a libc bug or the assessment from the previous thread that libc implementations provide the desired behavior - even if the standard language doesn't - needs to be revised.

    Based on how the glibc code is set up it very much looks like exit isn't intended to be thread safe. It uses a lock, but releases it every time it calls one of the exit handlers. That'd make no sense if it wanted to prevent other threads from modifying the list of exit handlers concurrently. The lock is useful for preventing corruption in the face of other threads calling atexit in parallel. There is a comment that says that this is its intended purpose (though admittedly it could be clearer).

    I think the previous assumption that glibc's exit is thread-safe was a misconception based on the existence of this lock alone.

  11. added
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jun 17, 2024
  12. teskje commented on Jun 17, 2024

    @teskje
    ContributorAuthor

    Adding 32 atexit handlers seems to be essential to make this segfault on my glibc 2.39+r52+gf8e4623421-1 on Arch Linux. With 31 atexit handlers, it doesn't segfault.

    That's explained by the fact that one block in the list of exit handlers contains 32 entries, and the last block is never freed. So to trigger the use-after-free you need at least two blocks in the list. glibc installs one exit handler on its own, so 32 more need to be installed to add a free-able block to the list.

  13. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Jun 17, 2024
  14. 152 remaining items

  15. bstrie commented on Oct 23, 2024

    @bstrie
    Contributor

    This is a rather long thread, can we get a summary of the current status added to the original issue body? What are actionable steps that can be taken to resolve this unsound issue? AFAICT the status appears to be "wait until the rest of the world is fixed", which IMO is tantamount to closing this issue as WONTFIX, given the prospects of that ever happening. And I'm not entirely satisfied by the dismissal of ameliorations on the Rust side (either adding a lock or marking as unsafe). If we can do better, then let's do better, even if preventing people from linking to broken C code is out of our power.

  16. ChrisDenton commented on Oct 23, 2024

    @ChrisDenton
    Member

    The pure Rust fix would be to have some Rust API for adding on exit functions. This could side-step the issue entirely so long as you ignore C.

    Note that we already have a lock to mitigate this issue. However it's by no means a fix.

  17. the8472 commented on Oct 23, 2024

    @the8472
    Member

    AFAICT the status appears to be "wait until the rest of the world is fixed", which IMO is tantamount to closing this issue as WONTFIX, given the prospects of that ever happening.

    Things are looking rosier than that. Posix, musl and glibc agreed to improve things and if I read the comments on the posix issue correctly they'll also bring it to the appropriate C committee.

  18. the8472 commented on Oct 23, 2024

    @the8472
    Member

    I have gathered a short summary and a few links from other comments into the top comment. Edit away if there's anything to correct.

  19. changed the title [-]`std::process::exit` is not thread-safe[/-] [+]`std::process::exit` is not thread-safe in combination with C code calling `exit`[/+] on Oct 24, 2024
  20. safinaskar commented on Jan 29, 2025

    @safinaskar
    Contributor

    Please, document in https://doc.rust-lang.org/nightly/std/process/fn.exit.html , on which platforms exit is fully thread-safe (even if some C code calls it), and on which platforms it is not. As well as I understand, exit is fully safe on musl. This should be documented on that page.

    Also, as well as I understand, musl is shipped with Rust itself. This means nothing prevents us from patching it! So, I suggest patching all remaining similarly unsafe C functions (setenv?) in musl implementation we ship. This will allow us fully solve all these remaining Rust unsoundness problems caused by system libc once and for all (at least for system libraries we ship).

    Also, we can create alternative target with static glibc. We will patch that glibc however we want, we will fix all exit, setenv, etc problems there. (Unfortunately, if we really want static glibc, we should build it with --enable-static-nss, but this is blocked by https://sourceware.org/bugzilla/show_bug.cgi?id=27959 )

  21. RalfJung commented on Jan 29, 2025

    @RalfJung
    Member

    Please, document in https://doc.rust-lang.org/nightly/std/process/fn.exit.html , on which platforms exit is fully thread-safe (even if some C code calls it), and on which platforms it is not. As well as I understand, exit is fully safe on musl. This should be documented on that page.

    That is really something the respective platforms need to document. We shouldn't be in the business of providing a central repository of all the ways in which various C implementations promise more than the C standard. We track this for our own needs here, but that is not the same as making it part of the official docs.

    And we are also not interested in maintaining a patched fork of musl or glibc.

  22. RalfJung commented on Jan 29, 2025

    @RalfJung
    Member

    What we should do, however, is document that it is UB to call this exit concurrently with C code calling exit.

  23. added 2 commits that reference this issue on Mar 18, 2025
  24. added a commit that references this issue on Mar 18, 2025
  25. added a commit that references this issue on Apr 2, 2025
  26. added
    A-processArea: `std::process` (note: `std::env` is A-env)
    on Jun 21, 2025
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-processArea: `std::process` (note: `std::env` is A-env)C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-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