-
Notifications
You must be signed in to change notification settings - Fork 48
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
Never allow unwinding from Drop impls #97
Comments
To clarify something, I'm not proposing making a backwards-incompatible change here; there may well be code relying on the ability to do this. But there are steps we could take to deprecate this for new code, such as a new panic variant that allows unwind but not from Drop, which could eventually become the default. And eventually, new code may be able to safely say "this crate just doesn't support unwind-on-Drop at all". |
Making a behavior change a property of the "current" crate is convenient to have opt-in through an edition, but what if multiple crates are involved? For example a generic function in edition A contains code expanded by a (proc-)macro in edition B that drops a value of a type parameter, which for a given instantiation is a type (and |
Current panic behavior is universal, determined by the executable. There's never some crates with unwind and some with abort. This would have to be a third variant: unwind-but-not-in-drop. |
Notes from today's @rust-lang/lang design planning meeting:
Current plan: defer this topic until a design meeting in August, plan for summarizing proposals and usage information from Zulip by that meeting. |
I opened a PR which implements |
Initial perf results: up to 10% reduction in compilation time (perf) and a 5MB (3%) reduction in the size of |
Should there be finer-grain controls (e.g. type-level)? |
This would prevent us from optimizing drops of I feel that this is best left as a global setting and that we eventually should enable it by default (and even remove the option of disabling it). This way authors of unsafe code can rely on stronger guarantees that dropping an object never unwinds. |
It's limited, but we can still perform some optimizations. E.g. if the drop to
I believe there are still legit uses of panicking within drop. The stronger guarantee is usually about calling drop within drops; aborting in such scenario is more reasonable than a blanket "panic in drop will abort". |
I disagree: there are very few cases where panicking within drop is useful and there are huge benefits to removing these: not only do we improve compile times and binary size, but we also get rid of a footgun from the Rust language. Consider that C++ (which is normally very conservative with breaking changes) made the same change in C++11: destructors were changed to be |
Yes, they are |
I am concerned because it's easy to opt-in the nounwind behaviour, but it is difficult (or even impossible) to opt-out. |
Add -Z panic-in-drop={unwind,abort} command-line option This PR changes `Drop` to abort if an unwinding panic attempts to escape it, making the process abort instead. This has several benefits: - The current behavior when unwinding out of `Drop` is very unintuitive and easy to miss: unwinding continues, but the remaining drops in scope are simply leaked. - A lot of unsafe code doesn't expect drops to unwind, which can lead to unsoundness: - servo/rust-smallvec#14 - bluss/arrayvec#3 - There is a code size and compilation time cost to this: LLVM needs to generate extra landing pads out of all calls in a drop implementation. This can compound when functions are inlined since unwinding will then continue on to process drops in the callee, which can itself unwind, etc. - Initial measurements show a 3% size reduction and up to 10% compilation time reduction on some crates (`syn`). One thing to note about `-Z panic-in-drop=abort` is that *all* crates must be built with this option for it to be sound since it makes the compiler assume that dropping `Box<dyn Any>` will never unwind. cc rust-lang/lang-team#97
C.f. rust-lang/lang-team#97 for a general discussion on why this is undesirable.
It seems that the main concern is whether anyone is relying on the current behavior. I can see two main types of panic sources in drops:
Also I just came across rust-lang/rust#83618 which shows that properly handling panicking drops in unsafe code can be a huge footgun. That issue is about a bug in the standard library could lead to a double-free with a panicking drop. |
@Amanieu if we were to schedule a design meeting for october, would you be available? Available times are: |
I'm available on the 20th and 27th. |
Scheduled for October 27 |
@Amanieu will prepare the write-up doc |
Since I've argued for both positions, my position refinement might be relevant: I'm personally now fine with Alternatively, the testing infrastructure can always use the abort-resilient strategy. It doesn't necessarily need to run every test in a separate process like the current |
|
I'd be wary of automatically changing from |
I feel that the dicussion should be split into two aspects, the safety aspect and the size saving aspect. I feel that we have other good alternatives to the safety aspect, e.g. a function-level toggle to make a function nounwind. Making panics in drop abort won't solve the safety problem as long as we want backward compatibility or type-level opt-outs (e.g. for On the size saving aspect, I don't believe that's good enough reason to switch the default, especially it's a potentially breaking change. We don't just switch the default to |
The size saving aspect is a bonus, not a primary motivation. The primary motivation, I think, is changing the expectation for whether libraries are expected to handle Drop types that panic in drop, or whether libraries can assume that case won't happen. That, I think, is what we ultimately need to make a decision on. |
And notably, there's a significant difference between
My position is that changing the default is fine and perhaps even desirable, but that It's currently somewhat reasonable to write a library that only guarantees functionality/soundness when I'm sympathetic to the soundness reasoning benefits of guaranteeing drop glue is I'll refine my position into a more proper supported argument once the RFC is drafted. Is crater able to instrument crater to determine if programs/tests which currently do not abort (this includes fail by panic before the patch) are made to abort? A crater run classifying not just pass->fail but also panic->abort would do a significant amount for weighing the cost of changing the default. However, it should be noted that generally tests for published code should (hopefully) pass, and thus not exercise the failing paths (which is where panic in dtor is most likely to occur). We might still catch some, though, by tests for testing harnesses which |
This almost seems to argue for a way for libraries to declare which panic strategies they are compatible with? |
If we do decide to have a third panic strategy, it does seem useful. With the current split just being unwind/abort, the social pressure to support unwinding (or at the very least not cause UB on unwinding) in public libraries is probably enough, especially since the difference to conformance is mostly just sprinkling |
Are there known footguns other than |
I'd like to give my own two cents, even if this is change already a fait accompli. I've gathered that two main issues with the status quo are relevant here:
Since this change doesn't resolve the second issue (personally speaking, it would be nice if we had a flag to allow stacking panics when There have been several comparisons between this proposed behavior and C++11's default- The most major difference is Rust's move semantics. In Rust, you can move a value from a binding into a function, making that function responsible for dropping (or storing) the value. But in C++, every local variable will always have its destructor implicitly called at the end of its scope. There's absolutely no way to avoid the implicit destruction, apart from wrapping the value in an In other words, C++ functions must implicitly destroy all their owned variables, whereas Rust functions only implicitly drop those variables that have not been moved somewhere else. In particular, Rust allows users to drop every significant value explicitly, by moving them into the appropriately named Another difference is between the meaning of C++'s exceptions and Rust's panics on a semantic level. In Rust, a panic is considered an irrecoverable error: something happened which you think should never happen, or which you have no way of ignoring or correcting, so the current task (perhaps the whole process, or perhaps only the current thread) must be canceled without further ado. But C++ primarily uses exceptions to express ordinary error conditions, which would just be From this perspective, it makes sense why destructors in C++ should be Together, I believe these differences give us more freedom than C++ to allow panicking Therefore, I am in favor of a solution like one proposed by @danielhenrymantilla on the community Discord server: Create an allow-by-default lint that triggers on every significant drop from variables falling out of scope. Then, users writing unsafe code can |
I think every drop is too much (unless I misunderstood what you mean by "significant"). But I would like a lint against running arbitrary user code, including drop. This will be very helpful for verifying correctness of unsafe functions. |
"significant" drop here means "has drop glue," "more than nothing ( Any type which does not have a drop implementation and does not have any field with (transitively a field that has) a drop implementation has a trivial drop. Making std types special as "not user code" and therefore "safe" to drop (if they only contain std types) is unfair to third party libraries and further privileges std. The alternative would be some way of specifying It's also worth reiterating that |
My apologies; by a "significant drop", I mean any drop that calls a Your idea does sound attractive, though, if it could be implemented usefully; writing generic unsafe code can be difficult when every user function or
The problem would be indirection; does dropping a |
I think I should also point out explicitly (I think @CAD97 mentioned about this as struct PanicGuard;
impl Drop for PanicGuard {
fn drop(&mut self) {
std::process::abort();
// panic!("unwind escapes nounwind context"); // This also works, as it causes double-unwind, so trigger abort
// core::intrinsics::abort(); // Or this with unstable
// rtabort!("unwind escapes nounwind context"); // Or this when used inside std
}
}
let guard = PanicGuard;
// Your code that does not allow unwinding
std::mem::forget(guard); When |
This is what I thought; I just wanted to make sure you didn't mean something like "runs user code on drop".
I didn't mean to implement this only for std types. I want an (allow-by-default) lint that warns when you run untrusted code, i.e. user code. As long as you know the types everything is fine (unsafe Rust can trust known safe code). Generics are indeed a problem, but even using heuristics will be a big help. |
Rather than specifying "user code" (from a compiler perspective, that's any Rust code not emitted by the compiler), it's better to say "upstream" or "downstream" for the relationship of the library dependency graph. There should still be a lint for implicit drop glue of upstream types, but I do agree that if such a lint is added, splitting it into two tiers of downstream and all types would indeed be useful. Also, @nbdd0121: abort on unwind can be made even simpler, skipping the need to impl Drop for AbortOnUnwind {
fn drop(&mut self) {
if std::thread::panicking() { panic!("unwound through nounwind context") }
}
} |
I think it's desirable in the future to allow multiple concurrent panics -- as long as the inner panic is caught and does not escape. This code will disallow nested panics. Also, checking |
C++ exceptions crossing Rust frames with drop glue are already UB, and there is no current plan to make them not UB. But this is off topic noise in an already noisy thread. |
That's not true with
No, I mean your code would abort when it is used while unwinding is already in place, not only when a new unwind propagates out from the function body.
Well, I think this is on-topic, since other ways to prevent the footgun would be alternative solutions to the proposed approach. |
Ah, apologies, I misremembered the section about forced unwinds as applying to all foreign unwinds. |
I wrote an RFC to argue for turning |
I assume you mean "=abort"?
|
Oops, fixed. |
There is now an RFC, I don't think we ever had the meeting, going to close this meeting request. |
Summary
Code using
catch_unwind
is not typically prepared to handle an object that panics in itsDrop
impl. Even the standard library has had various bugs in this regard, and if the standard library doesn't consistently get it right, we can hardly expect others to do so.This came up in @rust-lang/libs discussion.
We discussed various ways to handle this, including potential tweaks to
panic_any
orcatch_unwind
to add special handling of types that implementDrop
, but on balance we felt like it would be preferable to decide at the language level to generally not allow unwind fromDrop
impls. (We may not be able to universally prohibit this, but we could work towards transitioning there.)Background reading
rust-lang/rust#86027
About this issue
This issue corresponds to a lang-team design meeting proposal. It corresponds
to a possible topic of discussion that may be scheduled for deeper discussion
during one of our design meetings.
The text was updated successfully, but these errors were encountered: