You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Code using catch_unwind is not typically prepared to handle an object that panics in its Drop 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 or catch_unwind to add special handling of types that implement Drop, but on balance we felt like it would be preferable to decide at the language level to generally not allow unwind from Drop impls. (We may not be able to universally prohibit this, but we could work towards transitioning there.)
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.
Reacted by Ralf Jung, Lukas Wirth, Kevin Reid, qj, Keziah Biermann, Jakub Beránek, Kyle J Strand, Gustav Trede, Cristi Cobzarenco, Donough Liu and 2 more
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".
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 Drop impl) is in edition C.
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:
For this meeting to be a success, we would need some exploration of what the alternatives are and what kinds of backwards compatibility concerns exist in the wild.
Are people relying on the current behavior?
Can we estimate how many latent bugs exist because of the current behavior?
Anecdotally: Rayon for example found it too hard to manage and just aborts.
Libs team has found this painful in the std library.
Current plan: defer this topic until a design meeting in August, plan for summarizing proposals and usage information from Zulip by that meeting.
Should there be finer-grain controls (e.g. type-level)?
This would prevent us from optimizing drops of Box<dyn Any> since we wouldn't know whether they unwind.
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.
This would prevent us from optimizing drops of Box<dyn Any> since we wouldn't know whether they unwind.
It's limited, but we can still perform some optimizations. E.g. if the drop to Box<dyn Any> is itself called from a nounwind function, we'll still abort rather than unwind.
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.
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".
"significant" drop here means "has drop glue," "more than nothing (forget)," or "runs someDrop::drop code."
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 nounwind(nounwind(~T())) for user types to a) generate a landing pad that aborts and b) allow the type to be silently dropped even when #[warn(implicit_could_panic_drops)].
It's also worth reiterating that extern "C"will have an implicit nounwind landing pad once extern "C-unwind" is available. Making drop unwinds always abort does not plug a soundness hole here. It does make code destroying unknown types easier to make correct.
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.
My apologies; by a "significant drop", I mean any drop that calls a Drop impl, or approximately, dropping any type T where core::mem::needs_drop::<T>(). This is somewhat inspired by clippy::significant_drop_in_scrutinee, although I should probably use another name to avoid confusing the concepts.
Your idea does sound attractive, though, if it could be implemented usefully; writing generic unsafe code can be difficult when every user function or Drop impl can panic or give incorrect results. Perhaps such a lint could trigger on a few different things, such as:
Calling a trait method on a type parameter.
Calling a trait method on a dyn Trait.
Dropping a type parameter.
Dropping a dyn Trait.
The problem would be indirection; does dropping a LibraryType<T> drop T values or not? We could add attributes for types like Option<T> and Box<T> in the standard library, but this wouldn't work for third-party libraries. I'd be happy to hear of any good solutions on this front.
I think I should also point out explicitly (I think @CAD97 mentioned about this as AbortOnUnwindBomb but doesn't go into much detail) is that for unsafe code it's fairly easy to prevent unwinding from escaping a function (and works in stable):
structPanicGuard;implDropforPanicGuard{fndrop(&mutself){
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 C-unwind is stablised majority of FFI code will automatically get their free abort-upon-unwind behaviour, and the remaining non-extern "C" ones could use this pattern to explicit prohibit unwind.
"significant" drop here means "has drop glue," "more than nothing (forget)," or "runs someDrop::drop code."
My apologies; by a "significant drop", I mean any drop that calls a Drop impl, or approximately, dropping any type T where core::mem::needs_drop::<T>(). This is somewhat inspired by clippy::significant_drop_in_scrutinee, although I should probably use another name to avoid confusing the concepts.
This is what I thought; I just wanted to make sure you didn't mean something like "runs user code on 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.
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 forget with a cheap runtime check:
implDropforAbortOnUnwind{fndrop(&mutself){if std::thread::panicking(){panic!("unwound through nounwind context")}}}
Also, @nbdd0121: abort on unwind can be made even simpler, skipping the need to forget with a cheap runtime check:
implDropforAbortOnUnwind{fndrop(&mutself){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 std::thread::panicking would not protect your against a foreign exception; std::thread::panicking() would return false when, for example, a C++ exception causes the current in-progress unwind.
C++ exceptions crossing Rust frames with drop glue are already UB, and there is no current plan to make them not UB. panic! was an option you gave and the one I chose for the example; you can just as easily use abort. Also, this code has no impact on whether unwinding inside of a drop while already unwinding needs to immediately abort or only abort when hitting the drop glue edge, as this panic clearly does hit the edge of the destructor.
But this is off topic noise in an already noisy thread.
C++ exceptions crossing Rust frames with drop glue are already UB, and there is no current plan to make them not UB.
That's not true with C-unwind.
panic! was an option you gave and the one I chose for the example; you can just as easily use abort.
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.
But this is off topic noise in an already noisy thread.
Well, I think this is on-topic, since other ways to prevent the footgun would be alternative solutions to the proposed approach.
Activity
joshtriplett commentedon Jun 9, 2021
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".
SimonSapin commentedon Jun 10, 2021
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
Dropimpl) is in edition C.Lokathor commentedon Jun 10, 2021
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.
danielhenrymantilla commentedon Jun 10, 2021
Zulip discussion: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Never.20allow.20unwinding.20from.20Drop.20impls
joshtriplett commentedon Jul 7, 2021
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.
Amanieu commentedon Sep 8, 2021
I opened a PR which implements
-Z panic-in-drop=abort(rust-lang/rust#88759), we should have some perf results soon.Amanieu commentedon Sep 9, 2021
Initial perf results: up to 10% reduction in compilation time (perf) and a 5MB (3%) reduction in the size of
librustc_driver.so.nbdd0121 commentedon Sep 10, 2021
Should there be finer-grain controls (e.g. type-level)?
Amanieu commentedon Sep 10, 2021
This would prevent us from optimizing drops of
Box<dyn Any>since we wouldn't know whether they unwind.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.
nbdd0121 commentedon Sep 10, 2021
It's limited, but we can still perform some optimizations. E.g. if the drop to
Box<dyn Any>is itself called from a nounwind function, we'll still abort rather than unwind.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".
109 remaining items
CAD97 commentedon Jun 23, 2022
"significant" drop here means "has drop glue," "more than nothing (
forget)," or "runs someDrop::dropcode."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
nounwind(nounwind(~T()))for user types to a) generate a landing pad that aborts and b) allow the type to be silently dropped even when#[warn(implicit_could_panic_drops)].It's also worth reiterating that
extern "C"will have an implicit nounwind landing pad onceextern "C-unwind"is available. Making drop unwinds always abort does not plug a soundness hole here. It does make code destroying unknown types easier to make correct.LegionMammal978 commentedon Jun 23, 2022
My apologies; by a "significant drop", I mean any drop that calls a
Dropimpl, or approximately, dropping any typeTwherecore::mem::needs_drop::<T>(). This is somewhat inspired byclippy::significant_drop_in_scrutinee, although I should probably use another name to avoid confusing the concepts.Your idea does sound attractive, though, if it could be implemented usefully; writing generic unsafe code can be difficult when every user function or
Dropimpl can panic or give incorrect results. Perhaps such a lint could trigger on a few different things, such as:dyn Trait.dyn Trait.The problem would be indirection; does dropping a
LibraryType<T>dropTvalues or not? We could add attributes for types likeOption<T>andBox<T>in the standard library, but this wouldn't work for third-party libraries. I'd be happy to hear of any good solutions on this front.nbdd0121 commentedon Jun 23, 2022
I think I should also point out explicitly (I think @CAD97 mentioned about this as
AbortOnUnwindBombbut doesn't go into much detail) is that for unsafe code it's fairly easy to prevent unwinding from escaping a function (and works in stable):When
C-unwindis stablised majority of FFI code will automatically get their free abort-upon-unwind behaviour, and the remaining non-extern "C"ones could use this pattern to explicit prohibit unwind.ChayimFriedman2 commentedon Jun 23, 2022
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.
CAD97 commentedon Jun 23, 2022
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
forgetwith a cheap runtime check:nbdd0121 commentedon Jun 23, 2022
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
std::thread::panickingwould not protect your against a foreign exception;std::thread::panicking()would return false when, for example, a C++ exception causes the current in-progress unwind.CAD97 commentedon Jun 23, 2022
C++ exceptions crossing Rust frames with drop glue are already UB, and there is no current plan to make them not UB.
panic!was an option you gave and the one I chose for the example; you can just as easily useabort. Also, this code has no impact on whether unwinding inside of a drop while already unwinding needs to immediately abort or only abort when hitting the drop glue edge, as this panic clearly does hit the edge of the destructor.But this is off topic noise in an already noisy thread.
nbdd0121 commentedon Jun 23, 2022
That's not true with
C-unwind.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.
CAD97 commentedon Jun 23, 2022
Ah, apologies, I misremembered the section about forced unwinds as applying to all foreign unwinds.
Amanieu commentedon Jul 5, 2022
I wrote an RFC to argue for turning
-Z panic-in-drop=aborton by default: rust-lang/rfcs#3288RalfJung commentedon Jul 5, 2022
Amanieu commentedon Jul 5, 2022
Oops, fixed.
nikomatsakis commentedon Oct 12, 2022
There is now an RFC, I don't think we ever had the meeting, going to close this meeting request.