Rust's exit() forwards to C's exit() but doesn't warn about UB

I was reading https://wiki.musl-libc.org/functional-differences-from-glibc and I noticed that it says that (in any libc) "Calling exit more than once invokes undefined behavior". CPP Reference confirms this: https://en.cppreference.com/w/c/program/exit

On unix targets, the Rust standard library's exit function forwards to libc's exit, but there is no warning about the C UB in the documentation. Wouldn't it be helpful for the Rust documentation to include a note about this?

2 Likes

When is it even possible to call exit more than once? Not destructors. Maybe multiple threads?

I think it would probably be better to use an atomic lock to make the function safe in any case. Nop or abort if it's called more than once. Safe functions should not be able to cause UB.

1 Like

My guess is is, there is no "UB in the Rust sense" because

  • there are no atexit functions except those that are registered with unsafe. In this case, the atexit function should have this warning
  • of course, two threads could race towards exit, one calling exit(0), the other calling exit(1). It is not surprising to me that C++ calls this UB because the exit code is part of the abstract machine. I think that this is not the case for Rust and this is a perfectly fine Race condition.

Rust has decided to ignore this type of UB in favor of being more pragmatic around this: https://github.com/rust-lang/rust/issues/83994

We've been bitten by this before (ahem set_var), but I think in this case it's a fine solution.

7 Likes

You say pragmatic, but the report was an use after free. That's totally counter to Rust purpose.

The justification to that was this comment

exit is thread-safe. Installing atexit handlers which free resources other threads might still be using is not thread-safe and is a bug in whatever code is installing those atexit handlers, and should be fixed there.

But it seems libc::exit is not thread safe, as defined by the C standard. If Rust wants to call libc, it must adhere to libc's contract. It's the C standard that is in charge of defining what is and isn't UB when calling C APIs. Indeed, per the comment that proposed to close the issue

  • We feel that if a library has atexit handlers that aren't thread-safe, that's an issue in that library. We can't do anything to fix that; that requires appropriate locking in the library. We shouldn't call quick_exit, because such a race could just as easily happen in an at_quick_exit handler. And we shouldn't call _Exit or _exit and bypass the atexit handlers, because people may legitimately be relying on those atexit handlers, and there's no issue if your atexit handlers are thread-safe.
  • We do understand that exit is not marked as MT-safe; however, our understanding is that in practice every major C library seems to use an appropriate lock around the atexit array. If there's a specific C library that doesn't, we'd be willing to consider a workaround on that platform (e.g. wrapping it with our own lock), but otherwise we'd propose to close this.

"Is not marked as MT-safe" means "it's not MT-safe", or, it's UB to call concurrently from other threads, without a lock. The current Rust behavior is like, knowingly doing something that is UB, but in the opinion of Rust developers shouldn't be.

The only solution I see here is to simply not call libc, at all.

It's not UB, it's unsound. UB is a property of a specific execution, which can rely on implementation details. Your quote says it is known that all currently used implementations are actually MT-safe, it's just technically std is violating a safety-precondition of the generic libc API which could lead to UB on some other implementations.

You could interpret the current policy as

  • we will call libc::exit in a thread-safe fashion
  • unless the specific libc we are interfacing with does that for us (no reason to duplicate locking)

Ok. So this means that Rust currently has an unwritten rule that before adding a new target with a new libc, Rust developers must make sure that the libc does the expected thing here (even though the expected thing isn't required by any C standard)

It has lots of those. We're writing them down as we notice them, like

5 Likes

Is it even "unsound" in the Rust sense? No safe Rust can cause this UB. It probably is a practical issue regarding interop: in C/C++ it is UB to race to exit and if a program does not do that, nonthreadsafe atexit-handlers are fine, but are not fine in safe Rust... Unless I got something backwards.

1 Like

Is this libc exit thing documented somewhere as well?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.