-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exiting a process calls exit() which isn’t thread-safe #83994
Comments
Hmm, it seems really weird to me for exit() not to call libc::exit. It seems like the issue is libraries shouldn't be registering exit handler that aren't thread safe? |
I've seen a similar issue before: sfackler/rust-openssl#1293 |
I'm going to move this from T-libs-impl to T-libs because not calling libc::exit is user-facing. |
I don't see how would |
The assumption (from an old C++ proposal I read) is that doing so is considered a bug on the part of the caller of |
In Linux man-pages,
|
We discussed this in today's @rust-lang/libs meeting, and came to two conclusions:
@rfcbot close |
Team member @joshtriplett has proposed to close this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
Sadly, this makes safe FFI with some important libraries impossible. In particular, C++ static destructors, which are also called by |
Also see openethereum/parity-ethereum#6213 |
|
What about C++ static destructors? |
It's a programming bug to have code which might access them while (or after) they're being destroyed, or unless they're read-only and immutable, without some kind of synchronization around the access to them. If you have objects that are really supposed to have "whole program lifetime" that needs to include continuing through exit to the moment the process ceases to exist. You cannot deallocate them, ever. This may mean you need a wrapper ctor that just constructs them with dynamic lifetime so that they're not destructed at exit. |
So that means I need to report a bug against pretty much every C++ library that uses statically allocated resources. is there a linker flag I can pass to disable static destruction? Because I guarantee that I will not be the first one to face this problem, and changing the library is often impractical. |
I don't think that's quite accurate. If a C++ library has mutable singletons, there must already be some contract controlling shared access to them, and if the singleton is destructed on exit, then exiting is part of that contract, and the application is obligated not to exit without first obtaining exclusive access to said object - whether by holding a lock, ensuring that any thread potentially accessing the object has exited, or some other means. The immutable singletons situation is somewhat different, e.g. for things like runtime-generated tables of constants (ignore the fact that generating these at runtime is a bug because it prevents sharing them like you could do if they were static const data). However if the dtor actually destroys anything, the data was not actually immutable. Part of real immutability is never being freed. You can disable static destruction with a big hammer by calling
That's what people said with incorrect/nonportable code that broke on musl in the early days, and indeed it was hard to get maintainers to take bug reports seriously, but eventually we mostly did, and the whole ecosystem got better as a result. The issue you've uncovered is a real problem I've been wanting to get projects to fix for a long long time, and despite being difficult I believe it is worthwhile. It does not only affect Rust; the same problem you're seeing was already there with C or C++ code using these libraries. |
That is what this bug is about. As long as |
Is that any different than simply returning from |
No, there is no difference vs returning from
Of course it is not possible to write safe Rust bindings to libraries that have inherent object lifetime bugs internally. This is expected. |
@DemiMarie In principle? But that's quite a process that goes far beyond the scope of this issue. You may be interested in the http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2461.pdf, which touches this area and which has seen some positive interest from the C standards committee. I don't know if or how the author is planning to take that forward. |
I'm going to close this issue since T-libs agreed it isn't an issue with the rust standard library. |
FWIW, it seems increasingly likely that at least |
What rust can do is only call |
I suppose you "could" (if you could even distinguish that case; im general it's not possible), but this would have all sorts of nasty consequences like violating least surprise (different behavior for MT vs ST process), breaking atomicity of stdio operations, leaving exit cleanup functions unexecuted, etc. If there's any proper action to be had here, it's on the C side. The text about calling |
Do musl and glibc behave correctly? |
I guess it depends on what properties you want. I know glibc has or at least had unrelated issues with stdio synchronization at exit being missing, but I believe it safely handles multiple threads calling exit. On the other hand, musl used to have a global exit lock that made it so concurrent callers to exit would just wait forever until the first one finished exiting. That was removed, which was probably a bad idea, on the basis that it was undefined. But as far as I know there has never been any problem with a multithreaded program exiting (either via a call to exit, or main returning) as long as it was the only possible point of exit. This is in accordance with what the C standard already requires (exit is "thread safe" but just has the special, likely unintentional, constraint that you can only call it from one thread). I'll open this for reevaluation in musl; we'll probably add back the big lock. (In any program where atexit has been linked, including all dynamic linked programs, the atexit lock already partly fills this role, but allows successive handler functions to jump around among which thread executes them.) None of this changes my opinion that there is nothing Rust needs to do here, and that exiting already "just works", per the standard and implemntations, as long as you don't call exit from more than one thread. |
That's not an acceptable restriction for us though. This is safe code: fn main() {
std::thread::spawn(|| std::process::exit(1));
std::process::exit(1);
} |
That admits a trivial solution on the Rust side: put a lock on it. |
We can do that in Rust of course, but it has the same issue as the env lock: it doesn't apply to code written in other languages linked via FFI. You are basically saying that any C library that calls |
I don't think it's the same issue as the proposed env lock, which was based on a misunderstanding of the contracts involved. Accessing the environment is specified to be thread-safe and allowed to be done concurrently from arbitrary threads. It's that modifying it is a disallowed operation except in very restrictive contexts. Essentially, in a potentially multithreaded program (and thus in a general programming context), the environment is immutable input state reflecting how the program was invoked. Regarding the disallowance for multiple threads to call I'm still in favor of working to get this "fixed" on the C side, because the undefinedness seems completely gratuitous and potentially problematic. But I don't think it has any significant practical impact on Rust. If you're calling FFI library code that terminates your process behind your back, you have bigger problems. |
Whether or not code has Undefined Behavior is a big deal around here. And yes it makes a huge difference for us whether a library can "just" stop the entire process from running in a very visible way (this is bad style but nobody will be surprised to hear that it is possible), vs cause UB that might have arbitrary bad side-effects (which should be 100% ruled out, that is the entire point of Rust's safety guarantees). |
You're talking about a library written in C, not Rust. A library written in C is inherently not memory-safe and can always blow up your process with arbitrarily-bad side effects in ways Rust cannot rule out. If the library is written in Rust and uses |
Now we're back to this message. ;) It should be possible to safely combine code written in Rust with code written in another safe language, such as Java. But there's no way to coordinate a shared lock between Rust's and Java's
I'm putting my support behind that option then. :D |
This could be fixed for C2y. If a major libc like musl agrees with the new wording, that would help pushing the paper. Am I understanding correctly that you are looking for something along the lines of "If a program calls the Another way would be to make it IB, and hope all major libcs end up documenting a lock. |
I'm not sure I'm reading that proposed text correctly, but as I read it, it's saying I'm not sure the degree to which we could get consensus on something like that, but we can see. This probably calls for some research into what glibc, the BSDs, Apple, etc. do when multiple threads call I still think the extent of the problem is exaggerated. Traditionally and even now, while Java is supposedly memory safe at the specification level, the implementation of the runtime is chock-full of UB around system-interface-level things like this. For example I recall there being multiple versions of the backend for executing child processes, and all of them having UB around misuse of |
While it's certainly not a language that tickles my fancy, taking such a swipe at Java's prior bugs here is not only beneath you, it is a distraction, as Java was just an example and its quality of implementation doesn't matter. The problem in question returns if one introduces runtimes that don't have any UB around system interfaces. Even most purely functional programming languages have functions that can diverge at runtime (and thus call It would seem absurd if, by dint of a system only providing C interfaces to call process-terminating functions, one could not actually confidently run a program made by linking any two purely functional programming languages, even if both had their soundness actually formally proven, independently, and even if both used a "thin runtime" as C and Rust do, without having them both first agree to only allow one to call a diverging function amongst all their threads. |
Indeed feel free to substitute Java for literally any language that provides (That is also the parallel with the env situation: each individual language can actually provide a thread-safe mutable environment by adding a lock. But the only way to have that work in multi-language programs is to make the underlying system lib thread safe for env mutation. Those libs don't seem to consider that a desirable property, and the rest of the ecosystem is beholden to that decision.) We don't necessarily need the C standard to change here, but for targets Rust supports we do need to ensure that their |
Yeah, one could interpret it like that. I moved the "except" part and added a couple commas, that should make it a bit more clear, please take a look. Otherwise, the standard can always be more explicit, like repeating "that called ...". To be clear, the actual wording could be different, but I wanted to see if we were speaking about the same thing. |
For what it's worth we have done some research and glibc does in fact lock things. It apparently sometimes causes issues if you have more than 32 |
@workingjubilee turns out that exactly 32 handlers are enough: #126600 |
No, glibc adds its own, so it is technically more than 32 if you add 32 handlers. |
The issue here is that the other threads need to be quiesced before releasing those resources or do not release those resources. It's a bug in RPM Oxide. |
I avoided the problem by adding an |
This is a closed issue, please move the discussion to a place that is better suited. :) (e.g. #126600) |
On Unix-like platforms,
std::process::exit
callslibc::exit()
. However, this can lead to undefined behavior in a multithreaded process.In my case, this showed up as use-after-free crashes when running the testsuite for RPM Oxide.
librpm
registers anatexit()
handler to clean up global resources, but this causes some of the resources to be freed while other threads are still using them. Other projects have had similar issues, such as the Rust bindings for RocksDB.The best answer I know of is to call
quick_exit
instead, which only calls functions registered withat_quick_exit
. Such functions should be safe to be called while other threads are running.The text was updated successfully, but these errors were encountered: