Skip to content

Don't allow unwinding from Drop impls#3288

Open
Amanieu wants to merge 4 commits into
rust-lang:masterfrom
Amanieu:panic-in-drop
Open

Don't allow unwinding from Drop impls#3288
Amanieu wants to merge 4 commits into
rust-lang:masterfrom
Amanieu:panic-in-drop

Conversation

@Amanieu

@Amanieu Amanieu commented Jul 5, 2022

Copy link
Copy Markdown
Member

View all comments

This RFC proposes to change this behavior to always abort if a panic attempts to escape a drop, even if this drop was invoked as part of normal function execution (as opposed to unwinding which already aborts with a double-panic).

Previous discussion: rust-lang/lang-team#97

Rendered

FCP comment

@scottmcm scottmcm added the T-lang Relevant to the language team, which will review and decide on the RFC. label Jul 5, 2022
Comment thread text/0000-panic-in-drop.md Outdated
Comment thread text/0000-panic-in-drop.md Outdated
Comment thread text/0000-panic-in-drop.md Outdated
Comment thread text/0000-panic-in-drop.md
@Mark-Simulacrum

Copy link
Copy Markdown
Member

I think it would also be good to add an explicit section to the RFC about how users will find out about the change in behavior, just to document that going in - for example, release blog post and compat note would be a good start in my eyes, but we could also consider some softer change (e.g., adjusting the panic logging to say "this will abort starting in X release) to phase this in. AIUI, there's not really much static checking we can do, so that's probably about our limit...

@Gankra

Gankra commented Jul 5, 2022

Copy link
Copy Markdown
Contributor

I'm a huge fan of this proposal. When writing the rustonomicon this was one of the few major details of rust I couldn't really get a straight answer from anyone about. From my perspective it existed in the same grey area as unwinding through FFI, where we just lacked institutional expertise on the subtleties of unwinding corner cases and had punted on it.

This is why I never really adequately documented it. The current behaviour is very subtle and not something even stdlib devs ever had institutional knowledge or consideration of.

Writing exception-safe unsafe code is already enough of a headache, partially because it comes up rarely enough that it's hard to stay actively vigilant for. I think like, BinaryHeap is one of the only places in liballoc that has to do some explicit exception-safety fuckery because it needs to invoke the user-defined comparator repeatedly while doing mutations that put it in a transiently invalid state. All those stars lining up is actually surprisingly rare! Most of the time there are distinct lookup, allocate, then modify phases; so code is trivially exception-safe (assuming your own code doesn't panic, which collections generally only do during the lookup and allocations, not the final modification). Having less places where this rare problem can surprisingly come up, especially from user code, seems like a great thing.

We've never had a good story about this situation and at this point I think we have ample evidence it should just be banished to the shadow realm for being more trouble than it's worth. Especially since the stdlib already has the established precedent of Buf and File both just swallowing errors on Drop. Those cases were going to be my one hand-wring for this change, so it turns out I have nothing to complain about! (My memory was that they panicked, but honestly I never really gave it much consideration).

@LegionMammal978

LegionMammal978 commented Jul 5, 2022

Copy link
Copy Markdown
Contributor

I'd like to rehash my earlier arguments against this change, for those starting from this issue. I'd like to begin with some statements that I think everyone here agrees with:

  • Disregarding code size, this change only assists unsafe code. All safe code remains safe in the presence of unwinding panics.
  • Authors of some applications, such as certain web servers, would strongly prefer that threads unwind on critical failure rather than abort the entire process.
  • Some real-world Rust code can panic on drop today. (For instance, a Drop impl that calls println!() can panic if it fails to write.)
  • Today, a normally-unwinding panic from a Drop impl aborts the process if and only if either -C panic=abort is set, or the thread is already unwinding. With this change, a panic from a Drop impl will abort unconditionally.
  • As a consequence, some code that used to unwind the thread will now abort the process, to the (perhaps minor) detriment of certain web servers and some other applications.

Thus, I believe that since this is a silent change that will harm some current use cases, we should really show that the benefit of this change is greater than the cost, and that there are neither more beneficial nor less costly alternatives. I argue that such an alternative exists: keep panic-in-drop=unwind as the default, and provide better tools for writing drop-sensitive unsafe code.

Currently, the RFC twice compares this change to C++11's noexcept destructors, and only briefly mentions the differences in the "Prior Art" section. However, these differences are very important. In C++, each function parameter and local variable runs its destructor at the end of its scope. There is no way to prevent this, short of performing certain hacks with a union. There is also no way to run the destructor explicitly; even a move operation creates a new value and leaves a sentinel which must be destructed. But in Rust, destruction is determined by ownership semantics. If we pass a variable into a function parameter by value, the function (or data structure it is attached to) will become responsible for dropping it. This allows us to explicitly drop any value at any point in time, by passing it to the aptly-named drop() function.

The ability of Rust code to explicitly control dropping is quite relevant here. In C++, destruction is always invisible and uncontrollable (except if a value is placed into an std::optional which is reset), and so it makes a lot of sense not to mandate exception-safety surrounding it. In Rust, we can make drops clearly visible in unsafe code by using drop() and similar constructions, so the rationale doesn't apply nearly as absolutely.

The problem is, unsafe code today can easily implicitly drop values without realizing there's an issue; just look at the catch_unwind footgun (rust-lang/rust#86027). I believe that the best and most direct way to mitigate this is to provide better tools, both in the compiler and in the standard library, to catch these footguns instead of leaving them for the unsafe code's author to figure out. Importantly, we should clearly recommend using these tools in our docs and our books. To give an example, one crude but effective tool, from a suggestion by @danielhenrymantilla, would be an allow-by-default lint that triggers on every variable that implicitly calls Drop::drop(), and suggests explicitly drop()ping the variable. We could advise library authors to activate this lint on any panic-sensitive code that handles trait objects or user data types.


There are a few different variations on how the panic-in-drop option is to be handled. To summarize my position on each:

  • -Z panic-in-drop=unwind by default, -Z panic-in-drop=abort allowed: The current status quo. Acceptable, given additional tools to help unsafe code authors.
  • -C panic-in-drop=unwind by default, -C panic-in-drop=abort allowed: My preferred option, given additional tools. Users could manually choose panic-in-drop=abort for the improved code size and compile times, without needing the fully-fledged panic=abort.
  • -Z panic-in-drop=abort by default, -Z panic-in-drop=unwind allowed: This proposal, which I am against. It provides the code-size and compile-time improvements, but introduces a silent change that restricts some use cases with no stable workaround.
  • -C panic-in-drop=abort by default, -C panic-in-drop=unwind allowed: I am strongly against this. It would make unwinding drops possible, but it would fracture the package ecosystem: new unsafe container libraries could easily ignore support for the now-special panic-on-drop=unwind configuration, making the option very dangerous to use.

@CAD97

CAD97 commented Jul 5, 2022

Copy link
Copy Markdown

I would like to echo that the RFC as currently written puts too much weight on the code size / compilation benefits of the change. These are a strong argument for making the option available, but distract from the correctness argument for making it mandatory. Presenting this in the main text which is concerned with justifying changing the semantics of existing code serves to distinctly weaken the justification, since the argued position is not necessary for a primary argued benefit.

If the change is to be made, it should be justified on the unwind safety benefits alone, and the other benefits are just a very convenient bonus.

There is additionally a third undiscussed option: panic-in-drop=catch. Effectively, this automatically converts types to the recommended panic-in-drop=abort behavior of attempting as much cleanup as possible but swallowing the error and continuing on if something goes wrong. Of course, this is not without its own annoying problems (e.g. a field relying on the parent destructor to put it back into a state that is safe to drop... though imho that should definitely be using ManuallyDrop already anyway), but it does still have the distinct advantage of providing the nounwind benefit while avoiding introducing new implicit aborts into existing code.

I also need to point out what I think should be addressed, or at least acknowledged as a drawback: it is impossible to drop a structure where dropping its fields may panic without risking a panic-in-drop=abort. Even if you use ManuallyDrop to drop fields within your drop impl rather than after, the field's drop glue will cause an abort, so if you have legacy code that e.g. calls eprintln! in its destructor1, you have no recourse other than to forget to drop that field and leak its resources.

A final really silly option: add ptr::drop_in_place_maybe_unwind. This would explicitly generate and call panic-in-drop=unwind style drop glue, allowing you to lift panickingaborting destructors into a panicking close automatically.


If the compiler had some sort of #[no_panic] support that could be used for drop impls, I would be less worried about a change along these lines, because it would be at least somewhat possible to verify that drops don't cause an abort in normal execution.

Footnotes

  1. ... I have some code to go update to be more resilient against double panics.

Comment thread text/0000-panic-in-drop.md
@afetisov

afetisov commented Jul 5, 2022

Copy link
Copy Markdown

I'll just note that the performance and code size changes are pretty inconsequential, for a break in backwards compatibility. 3-5% decrease in code size is irrelevant for all but the most resource-constrained applications, which will likely use panic=abort anyway.

Also, while the change would possibly simplify the correctness of unsafe code, it would compromise the correctness of the application at large, which may rely on properly handling Drop panics. This is a big issue, both because the application is much harder to audit due to its size and lack of compiler-enforced panic boundaries, and because the application is usually written by less experienced users, including total newbies, while unsafe code is relegated to the experts.

The change in behaviour of Drop would be a breaking change, and a very insidious one, since it is entirely silent and hard to audit. That's the worst kind of breakage that can be introduced. It is also well-known that Rust currently allow panic in Drop, so many crates may rely on it. scopeguard is one mentioned example, Another example is the drop_bomb crate, which is used in 53 dependencies, including rust-analyzer. I'm sure there are Drop impls in the wild which use the same pattern without explicitly using those dependencies.

@Lokathor

Lokathor commented Jul 5, 2022

Copy link
Copy Markdown
Contributor

An attribute that specific unsafe types can put on their own drop impl if they need to be absolutely sure that no unwind can escape their drop is one thing. This is too much.

@kornelski

kornelski commented Jul 5, 2022

Copy link
Copy Markdown
Contributor

I feel this is a throwing baby with the bathwater situation. I'd prefer exploration of alternative designs. For example:

  • Add ability to forbid all panics in a function or a block of code. For unsafe code panics are dangerous, but panic in Drop is only a one of many cases. There could be unexpected panics lurking in Deref and overloaded operators. unsafe needs to be aware of all of them, so this solution is insufficient.

  • catch_unwind is a terrible gotcha that definitely needs addressing, but it also is a very unique case. There is no other panic-catching function, so there is no other place in Rust where code that clearly doesn't want a panic may still get one. I feel that a solution narrowly targeted specifically at catch_unwind would be sufficient to solve this gotcha, e.g. forbid panic in drop only in types used with catch_unwind (and dyn if necessary) or mark only callers of catch_unwind with panic=abort. There could be a safer catch_unwind variant that requires return type to be Copy.

  • For applications concerned about code size overhead there is panic=abort already. If a more granular solution is needed, this could be offered with an explicit function attribute.

@CAD97 CAD97 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about it some more, and I am currently against changing the global default to panic-in-drop=abort. I have a hypothesis that end of scope drops are not the problem to writing unwind-safe code. In most cases, unsafe code is written to have an unsafe precondition but a sound (or at least no-more-unsound) postcondition in a { suspend_safety; defer! { restore_safety; } tricky_code; return; } pattern.

Because of this, most code, even unsafe code, is unwind safe. Of course, the issue making unwind safety difficult is specifically that most code doesn't need to care, so when you do need to care, it's fairly easy to forget to. (This is an inherent limitation of operational pre/post-condition soundness semantics; an axiomatic invariant is just never put into an invalid unsound state in the first place.)

In order to justify changing the default, the RFC should justify the claim that RAII drop glue is an issue, and not just explicit drop_in_place calls unwinding. I would much rather see (and am drafting) a proposal providing APIs for unsafe code to control unwinds more carefully; namely some combination of:

  • drop_unwind which returns Option<R> rather than Result<R, UnwindPayload> to prevent rust-lang/rust#86027 issues.
  • catch_unwind2 that forces explicitly deciding how to deal with payload drop panics.
    • Two main ways to do so: a UnwindPayload type which is ManuallyDrop, or a #[must_use] Result-variant that doesn't offer functionality implicitly dropping the unwind payload.
    • Even better if this can replace catch_unwind in-place. (Doing so in existing editions would require #[deprecated] coercions for use of the current API or a strong argument for being a soundness fix that breaks inference.)
    • Author's current weak preference: suspend_unwind, which returns Result<R, Unwind>, and dropping Unwind calls resume_unwind.
  • #[abort_unwind] to add unwind-to-abort shims to arbitrary functions.
  • #[catch_unwind(return)] to add catch_unwind shims returning some fallback value.
  • Some #[drop_abort_unwind]/#[drop_catch_unwind] to do the above but on the full drop glue including dropping fields (as just shimming Drop::drop will not catch fields' drop glue unwinding).
  • Per-type control between leaking fields on drop glue unwind and suspending the unwind to drop fields.
  • drop_in_place_abort_unwinds which calls drop glue with an unwind-to-abort shim.
  • drop_in_place_catch_unwinds which calls drop glue returning some Result<(), PanicPayload> (see catch_unwind2 for options here).
  • drop_in_place_drop_unwinds which calls drop glue with drop_unwind glue.
  • #[warn(implicit_drop_glue)] to warn where drop glue is inserted. (A good restriction lint candidate, Though perhaps perf sensitive?)
    • Potential refinements involve only warning on drop glue which is not nounwind.

The goal of the above is providing options to unsafe code to allow code to choose their guarantees.


The below is a very interesting alternative to both panic-in-drop=unwind and panic-in-drop=abort.

There's additionally another intriguing middle ground which should be listed as an alternative: drop glue / drop_in_place can still unwind, and a field's drop glue unwinding continues to unwind and drop other fields (siblings and of parent containing types), but unwind is implicitly caught and turned into an abort only when the unwind exits from the drop glue stack into a calling function.

(How this behavior translates to async witness tables is a question in this scheme that still would need to be answered. My personal preferred solution is to treat dropping an async witness table identically to as if it were an unwind (as the sync analogue is exactly an unwind), thus setting thread::panicking() to true to eagerly convert any panics into aborts.)

This scheme has the potential to combine the benefits of both panic-in-drop=abort1 and panic-in-drop=unwind, though with a bit of additional effort. Namely, implicit drop glue unwind points are removed as a thing that unsafe code has to worry about, but drop_in_place still allows drop implementations to unwind at clearly marked points. Also notable is that drop under this scheme can be made to opt-in by-value drops unwinding by reimplementing it as ManuallyDrop::drop(&mut ManuallyDrop::new(it)).

Combined with some of the above controls for managing unwinding, this is a very promising approach to very potentially gain the benefit of this RFC while mitigating the downsides. (For maximum control, make a function-level switch between drop glue unwinds propagating or aborting.)

Footnotes

  1. The unwind-safety benefits, anyway; the code size benefits will be reduced, if not completely eliminated, by drop glue handling suspend-and-continue unwind semantics.

Comment thread text/0000-panic-in-drop.md Outdated

This RFC is already implemented as an unstable compiler flag which controls the behavior of panics escaping from a `Drop` impl.

Performance results from [rust-lang/rust#88759](https://github.com/rust-lang/rust/pull/88759) show up to 10% reduction in compilation time ([perf](https://perf.rust-lang.org/compare.html?start=c9db3e0fbc84d8409285698486375f080d361ef3&end=1f815a30705b7e96c149fc4fa88a98ca04e2deee)) and a 5MB (3%) reduction in the size of `librustc_driver.so`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For comparison, I would like to see results where just std's destructors are nounwind/unwind-in-drop=abort. This is likely a worst-of-both-worlds situation where using std containers turns a drop unwind into an unconditional abort but stack types can still unwind from drop glue, but is well within std's QOI rights to decide that unwinds out of its drop glue is forbidden while still allowed by the language. (Whether this is acceptable QOI is a separate question and depends on the potential benefit.)

QOI
Quality Of Implementation

Comment thread text/0000-panic-in-drop.md
Comment thread text/0000-panic-in-drop.md Outdated
Comment on lines +206 to +249
// Codegen with -Z panic-in-drop=unwind (old behavior)
unsafe fn drop_in_place<T>(ptr: *mut T) {
// Call the Drop impl if there is one.
try {
<T as Drop>::drop(&mut *ptr);
} catch {
// If the Drop impl panics the keep trying to drop fields.
try {
drop_in_place(&mut (*ptr).field1);
drop_in_place(&mut (*ptr).field2);
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}

// Drop the first field.
try {
drop_in_place(&mut (*ptr).field1);
} catch {
// If dropping field1 panics the keep trying to drop the remaining fields.
try {
drop_in_place(&mut (*ptr).field2);
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}

// Drop the second field.
try {
drop_in_place(&mut (*ptr).field2);
} catch {
// If dropping field2 panics the keep trying to drop the remaining fields.
try {
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}

// Drop the third field.
drop_in_place(&mut (*ptr).field3);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize that this is illustrative rather than accurate, but this is also significantly worse than what is necessary and as such exaggerates the benefit of panic-in-drop=abort simplifying drop glue. A better representation of code the compiler could emit would be

unsafe fn drop_in_place<T>(ptr: *mut T) {
    let mut unwind = None;

    // Call the Drop impl if there is one.
    try {
        <T as Drop>::drop(&mut *ptr);
    } catch(payload) {
        std::panicking::panic_count::increase();
        match unwind {
            Some(_) => abort(),
            None => unwind = Some(payload),
        }
    }

    // Drop the first field.
    try {
        drop_in_place(&mut (*ptr).field1);
    } catch {
        std::panicking::panic_count::increase();
        match unwind {
            Some(_) => abort(),
            None => unwind = Some(payload),
        }
    }

    // Drop the second field.
    try {
        drop_in_place(&mut (*ptr).field2);
    } catch {
        std::panicking::panic_count::increase();
        match unwind {
            Some(_) => abort(),
            None => unwind = Some(payload),
        }
    }

    // Drop the third field.
    try {
        drop_in_place(&mut (*ptr).field3);
    } catch {
        std::panicking::panic_count::increase();
        match unwind {
            Some(_) => abort(),
            None => unwind = Some(payload),
        }
    }
    
    // Resume an in-progress unwind.
    if let Some(payload) = unwind {
        std::panicking::panic_count::decrease();
        resume_unwind(payload);
    }
}

but even this is more complicated than it need be, as those abort calls could be replaced by unreachable_unchecked, as hitting that case requires a double panic to unwind rather than immediately abort.

maybe better suggestion
Suggested change
// Codegen with -Z panic-in-drop=unwind (old behavior)
unsafe fn drop_in_place<T>(ptr: *mut T) {
// Call the Drop impl if there is one.
try {
<T as Drop>::drop(&mut *ptr);
} catch {
// If the Drop impl panics the keep trying to drop fields.
try {
drop_in_place(&mut (*ptr).field1);
drop_in_place(&mut (*ptr).field2);
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}
// Drop the first field.
try {
drop_in_place(&mut (*ptr).field1);
} catch {
// If dropping field1 panics the keep trying to drop the remaining fields.
try {
drop_in_place(&mut (*ptr).field2);
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}
// Drop the second field.
try {
drop_in_place(&mut (*ptr).field2);
} catch {
// If dropping field2 panics the keep trying to drop the remaining fields.
try {
drop_in_place(&mut (*ptr).field3);
} catch {
abort();
}
}
// Drop the third field.
drop_in_place(&mut (*ptr).field3);
}
// Codegen with -Z panic-in-drop=unwind (old behavior)
unsafe fn drop_in_place<T>(ptr: *mut T) {
let unwind;
// Call the Drop impl if there is one.
try {
<T as Drop>::drop(&mut *ptr);
} catch(payload) {
magic::assume!(!magic::initialized!(unwind));
std::panicking::panic_count::increase();
unwind = payload;
}
// Drop the first field.
try {
drop_in_place(&mut (*ptr).field1);
} catch {
magic::assume!(!magic::initialized!(unwind));
std::panicking::panic_count::increase();
unwind = payload;
}
// Drop the second field.
try {
drop_in_place(&mut (*ptr).field2);
} catch {
magic::assume!(!magic::initialized!(unwind));
std::panicking::panic_count::increase();
unwind = payload;
}
// Drop the third field.
try {
drop_in_place(&mut (*ptr).field3);
} catch {
magic::assume!(!magic::initialized!(unwind));
std::panicking::panic_count::increase();
unwind = payload;
}
// Resume an in-progress unwind.
if magic::initialized!(unwind) {
std::panicking::panic_count::decrease();
resume_unwind(unwind);
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the code that I showed is pretty much what is currently emitted by the compiler. Specifically:

  • The panic count is not checked, because it requires a TLS access which can be very slow.
  • Separate drop calls are used in the normal path and the unwind path because a different landing pad needs to be used when those calls unwind.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, because of extern "C-unwind" we can have C++ exceptions unwinding through Rust code which don't increment the panic count. This means that we can't rely on the panic count to guarantee that a drop within an unwind guard won't itself panic.

@CAD97 CAD97 Jul 6, 2022

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the compiler is emitting intrinsics::r#try directly. AIUI, this means that the drop glue can in fact just not touch the panic count, as the panic count is already set by the in-progress unwind. It is currently the case that a panic-in-panic will abort via the panic_count check in rust_panic_with_hook.

It is the case that calling drop_in_place while panicking cannot unwind due to a rust panic. The only way that the separate landing pad would landed on is if an external unwind is caught. (IIUC, the intent is for extern "C-unwind" to convert allowed unwinds into a rust panic. This must involve incrementing the panic count (and thus a place to abort on double panic), as catching will decrement the panic count.)

If you can show a case, where the drop glue second landing pad is landed on, I'll happily be proven wrong. But the normal case is in fact that the abort comes immediately from the panic!, not the drop glue catching it.

So here's my updated drop glue impl:
// Codegen with -Z panic-in-drop=unwind (necessary behavior, different from current)
unsafe fn drop_in_place<T>(ptr: *mut T) {
    let unwind;

    // Note that try {} catch {} here is the raw intrinsics::try.
    // The panic count is not touched, so remains set from the first panic.

    // Call the Drop impl if there is one.
    try {
        <T as Drop>::drop(&mut *ptr);
    } catch(payload) {
        unwind = payload;
        goto 'unwind_field1;
    }

    // Drop the first field.
    try {
        drop_in_place(&mut (*ptr).field1);
    } catch(payload) {
        unwind = payload;
        goto 'unwind_field2;
    }

    // Drop the second field.
    try {
        drop_in_place(&mut (*ptr).field2);
    } catch(payload) {
        unwind = payload;
        goto 'unwind_field3;
    }

    // Drop the third field.
    try {
        drop_in_place(&mut (*ptr).field3);
    } catch(payload) {
        unwind = payload;
        goto 'done;
    }
    
    goto 'done;
    
    try {
    'unwind_field1:
        drop_in_place(&mut (*ptr).field1);
    'unwind_field2:
        drop_in_place(&mut (*ptr).field1);
    'unwind_field3:
        drop_in_place(&mut (*ptr).field1);
    } catch {
        // A rust panic cannot hit this landing pad, because it would've aborted.
        __rust_foreign_exception(); // aborts
    }

'done:

    // Resume an in-progress unwind.
    if magic::initialized!(unwind) {
        __rust_resume_panic(unwind);
    }
}

@Amanieu

Amanieu commented Jul 6, 2022

Copy link
Copy Markdown
Member Author

I would like to echo that the RFC as currently written puts too much weight on the code size / compilation benefits of the change. These are a strong argument for making the option available, but distract from the correctness argument for making it mandatory. Presenting this in the main text which is concerned with justifying changing the semantics of existing code serves to distinctly weaken the justification, since the argued position is not necessary for a primary argued benefit.

I personally find the code size argument more convincing since it is backed by hard numbers which show the improvement. The exception safety argument is inherently subjective since it can just be countered by saying "well it's your fault for writing buggy unsafe code". It's not impossible to write exception-safe Rust code, just difficult.

Also, while the change would possibly simplify the correctness of unsafe code, it would compromise the correctness of the application at large, which may rely on properly handling Drop panics. This is a big issue, both because the application is much harder to audit due to its size and lack of compiler-enforced panic boundaries, and because the application is usually written by less experienced users, including total newbies, while unsafe code is relegated to the experts.

I find this argument quite strange. Does any existing code actually rely on panics for normal operation? In general, panics will only happen in the case of buggy code, when an unexpected error occurs. Additionally, the proportion of code executed by Drop impls is tiny compared to non-drop code: assuming an even distribution of potential unexpected panics in a codebase, they will almost always come from non-drop code.

There's additionally another intriguing middle ground which should be listed as an alternative: drop glue / drop_in_place can still unwind, and a field's drop glue unwinding continues to unwind and drop other fields (siblings and of parent containing types), but unwind is implicitly caught and turned into an abort only when the unwind exits from the drop glue stack into a calling function.

This is something that has been suggested a lot. If I understand correctly, what you are suggesting is that unwinding out of a drop aborts only for implicit drops but not explicit ones. The problem is: if you already know which drop calls are going to potentially panic then you could just convert that code to use a close method like I suggested in the example. I don't see much benefit in adding compiler support for something which can easily be done in library code.

In order to justify changing the default, the RFC should justify the claim that RAII drop glue is an issue, and not just explicit drop_in_place calls unwinding.

The RAII drop glue calls drop_in_place, so making that function nounwind will affect both explicit and implicit drops.

I would much rather see (and am drafting) a proposal providing APIs for unsafe code to control unwinds more carefully; namely some combination of:

The problem with these is that none of them give the same code size & compilation time benefits as the approach proposed in this RFC. This is something that I feel quite strongly about: a 10% speedup in compilation time on a crate like syn is huge.

@carbotaniuman

Copy link
Copy Markdown

I personally find the code size argument more convincing since it is backed by hard numbers which show the improvement. The exception safety argument is inherently subjective since it can just be countered by saying "well it's your fault for writing buggy unsafe code". It's not impossible to write exception-safe Rust code, just difficult.

Given that the Rust standard library (ostensibly a codebase with a much higher level of rigor and review than most crates) has gotten this wrong on several occasions, I'm doubtful that most developers would be able to disarm this footgun, or even recognize that this exists.

@afetisov

afetisov commented Jul 6, 2022

Copy link
Copy Markdown

Does any existing code actually rely on panics for normal operation? In general, panics will only happen in the case of buggy code, when an unexpected error occurs.

Panics may be used to implement non-local exception-based control flow, even though it is clearly not an intended usage.

But to the point, while I doubt that panics could occur during normal operation of most non-buggy code, the correctness argument is about the graceful handling of bugs. Code may rely on being able to catch the panics for correctness. A long-running web server, or an embedded device, or a security-critical firmware, or an OS kernel absolutely should not panic. End-users may rely on being able to catch any possible panics with catch_unwind, and currently this includes single panics occurring in Drop glue. Note that the panic strategy is chosen by the end-user during compilation. If one sets -C panic=unwind, they can reasonably expect that they will be able to handle panics.

With the proposed changes, a relatively benign panic for some rare edge case will be escalated to a total crash, which can lead to DoS or worse.

Additionally, the proportion of code executed by Drop impls is tiny compared to non-drop code: assuming an even distribution of potential unexpected panics in a codebase, they will almost always come from non-drop code.

The argument about distribution of panics is quite weird, I don't know what to make of it. Since Drop must deal with disposing potentially fallible resources, I find it likely that Drop impls on average have more panics, but this is also a baseless assumption.

@CAD97

CAD97 commented Jul 6, 2022

Copy link
Copy Markdown

I personally find the code size argument more convincing since it is backed by hard numbers which show the improvement.

But the code size can be achieved by opt-in or opt-out behavior. It is not a justification for making panic-in-drop=abort the only option. It can serve as a supporting argument to making the change, but it cannot be the primary argument, because it does not justify making this breaking change to behavior.

I find this argument quite strange. Does any existing code actually rely on panics for normal operation?

Yes. For example, rust-analyzer uses unwinds for cancellation. Unwinding is the semantically correct way to handle cancellation, and is essentially what cancelling an async function and dropping the witness table looks like (just without thread::panicking()).

This is not unique to just rust-analyzer; this is a design choice of salsa.

It has generally been understood that while you SHOULD NOT require unwinding support in a library (and MUST support unwinding), you MAY rely on unwinding being available if you control the application compilation.

I agree that likely no code is deliberately relying on unwinding out of drop for application correctness, but it is the case that long-running programs intend to catch any task unwind, and turning unwinds that would have been recovered from into aborts is adding a new DoS vector to such servers.

This is something that has been suggested a lot. If I understand correctly, what you are suggesting is that unwinding out of a drop aborts only for implicit drops but not explicit ones. The problem is: if you already know which drop calls are going to potentially panic then you could just convert that code to use a close method like I suggested in the example. I don't see much benefit in adding compiler support for something which can easily be done in library code.

Then I can take this argument and apply it unchanged to making drop_in_place nounwind as well! You already know where things are going to potentially panic (every drop_in_place call) and can convert the code to use a close method.

Unless I'm sorely mistaken, the justification for making this a mandatory breaking change without opt-out is that it is too difficult to know which drop calls are going to potentially panic, and the proposed resolution is to make no drop calls panic. I fail to see how this alternative fails at the goal, as every point that can unwind is clearly marked. And if you do want the behavior where drop_in_place aborts unwinds as well, drop_in_place has a reminder on it to "remember that this can unwind from panicking drop glue! If this is undesirable, wrap the call in a drop_unwind or call drop_in_place_no_unwind instead."

I'd even be happy if we took the usual "safer by default" approach (as with copy/copy_nonoverlapping, sort/sort_unstable) and made drop_in_place abort unwinds but provided (and drop glue used) drop_in_place_allow_unwinds. The thesis I am defending is to provide safeguards but with the choice and tools to allow unwinds.

The problem with these is that none of them give the same code size & compilation time benefits as the approach proposed in this RFC. This is something that I feel quite strongly about: a 10% speedup in compilation time on a crate like syn is huge.

I will again also reiterate that the compile-time and code-size improvements are not an argument to make this mandatory. They are a very good argument to make it an option. They may even support making this the default. They would definitely be an argument against adding panic-in-drop=unwind if the existing behavior was panic-in-drop=abort.

Swift's lack of deinit throws is reasonable prior art to point to for not providing fallible destruction. However, Swift has a large advantage here: they don't have any unwinding. Your only choice is throws or abort. This means that you have a built-in guarantee that a Swift destructor doesn't contain an uncaught exception/unwind. In fact, in Swift, indexing an Array out of bounds aborts. (Most collections return an optional value from subscripts, but Array doesn't.) Basically any developer error which would panic! in Rust is an abort in Swift.

If anything, this is an argument in support of -Cpanic=abort becoming the default instead of -Cpanic=unwind. I would support such a change. (This may run into similar issues around encouraging crates which don't support unwinding, but tests continuing to be -Cpanic=unwind I think serves as a strong force against this.)

In order to justify changing the default, the RFC should justify the claim that RAII drop glue is an issue, and not just explicit drop_in_place calls unwinding.

The RAII drop glue calls drop_in_place, so making that function nounwind will affect both explicit and implicit drops.

Yes, but I'm not arguing that point. It is a perfectly viable strategy for the unwind-to-abort shim to be added in the drop glue rather than in drop_in_place.


Given that the Rust standard library (ostensibly a codebase with a much higher level of rigor and review than most crates) has gotten this wrong on several occasions, I'm doubtful that most developers would be able to disarm this footgun, or even recognize that this exists.

Nobody (including std devs) really understood the issue of a caught unwind's drop unwinding until recently. (This discussion started almost immediately after it was discovered.) We can attack this with documentation on catch_unwind and any other APIs for managing unwinds.

Other than this specific case, std has generally had no issue with unwind safety, because it is something you so rarely have to think about that std basically only encounters it for BTreeMap and where catch_unwind unwinds.

@bjorn3

bjorn3 commented Jul 6, 2022

Copy link
Copy Markdown
Member

Code may rely on being able to catch the panics for correctness. A long-running web server, or an embedded device, or a security-critical firmware, or an OS kernel absolutely should not panic.

Also many TUI applications depend on unwinding to avoid getting stuck in raw mode when panicking. Very few register an atexit callback and signal handlers to exit raw mode on crashes.

@LegionMammal978

LegionMammal978 commented Jul 6, 2022

Copy link
Copy Markdown
Contributor

In order to justify changing the default, the RFC should justify the claim that RAII drop glue is an issue, and not just explicit drop_in_place calls unwinding. I would much rather see (and am drafting) a proposal providing APIs for unsafe code to control unwinds more carefully

I wrote up an outline for a related alternative in an IRLO thread a while back. The idea is to have a catch_unwind_v2<F, R>(...) -> UnwindResult<R>, where UnwindResult is #[must_use] and has many of the methods you listed, acting as a sort of by-value builder API. To summarize what I wrote there:

use std::{any::Any, panic::UnwindSafe};

pub fn catch_unwind_v2<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> UnwindResult<R>;

#[must_use]
pub struct UnwindResult<T> { ... }

impl<T> UnwindResult<T> {
    // Unwinding functions
    pub fn into_result(self) -> Result<T, Box<dyn Any + Send>>;
    pub fn drop_err(self) -> Option<T>;
    pub fn resume_unwind(self) -> T;

    // Unwind-free functions
    pub fn inspect_err<F>(self, f: F) -> UnwindResult<T>
    where
        F: FnOnce(&(dyn Any + Send + 'static));

    pub fn inspect_err_mut<F>(mut self, f: F) -> UnwindResult<T>
    where
        F: FnOnce(&mut (dyn Any + Send + 'static));

    pub fn forget_err(self) -> Option<T>;
    pub fn unwrap_or_abort(self) -> T;
    pub fn try_drop_err(self) -> Result<T, UnwindResult<()>>;
    pub fn drop_err_or_forget(self) -> Option<T>;
    pub fn drop_err_or_abort(self) -> Option<T>;

    // Add `new()`, `map()`, etc. as desired
}

I've been doing some surveying of real-world catch_unwind usage (via GitHub Code Search) to evaluate this; I'll probably post my findings in a few days.

@carbotaniuman

Copy link
Copy Markdown

After a bit of research, I wanna say I don't particularly find the comparisons to C++ noexcept particularly compelling. In C++, the basic exception guarantee is as follows:

If the function throws an exception, the program is in a valid state. No resources are leaked, and all objects' invariants are intact.

Rust code provides neither of these guarantees, even in idiomatic code. (It is only better than having no exception guarantee that memory corruption iins not allowed). Leak amplification is used throughout the Rust stdlib (unlike C++).

In addition, unwinds do not require object invariants to be preserved, which is the entire reason why UnwindSafe is a thing, but issues like rust-lang/rust#89832 aren't really considered worth fixing. Observing broken object invariants in Rust is safe, for better or for worse.

Given that objects with throwing destructors were already forbidden in STL containers, the change in C++ likely impacted a lot less code than these proposed Rust changes. And I think given the difference in semantics for unwinding vs exceptions, this change is too big a hammer.

@Lokathor

Lokathor commented Jul 7, 2022

Copy link
Copy Markdown
Contributor

Yeah, UnwindSafe is kinda "fake" in the sense that it doesn't relate to unsafe at all.

@ijackson

ijackson commented Jul 7, 2022

Copy link
Copy Markdown

The RFC text (in the Downsides) ought IMO to discuss the "drop bomb" pattern, sometimes used to simulate linear types - or rather, to avoid shipping code which treats a particular type as nonlinear. E.g. rust-lang/rust#78802 (comment)

This is relevant here, because drop bombs precisely involve deliberately panicking inside drop, in circumstances where the author would probably have made very different design choices if they expected that such a panic would be an immediately irrecoverable abort.

@Gankra

Gankra commented Jul 7, 2022

Copy link
Copy Markdown
Contributor

Drop bombs are usually being used to cover up for things you would want to be compile-time errors, so it's not at all clear to me that you would care about whether the bomb unwinds or aborts -- if linear (true must-use) type lovers had their druthers, the program wouldn't have even been runnable!

Comment thread text/0000-panic-in-drop.md Outdated
@joshtriplett joshtriplett added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Jul 12, 2022
@joshtriplett

Copy link
Copy Markdown
Member

I think there's been a huge amount of focus on the optimization possibilities here, as opposed to the correctness argument. The optimization possibilities would justify creating an option, but not changing the default. The correctness issue justifies changing the default.

I think the correctness argument is the primary reason to do this; that it enables great optimizations is a bonus.

Handling objects that panic in their drop implementations is incredibly complex, and as noted, even the standard library has gotten it wrong. I think it's reasonable for us to evaluate whether we expect libraries to handle this, or whether we want to proscribe it entirely so that libraries don't have to handle it.

I am quite sympathetic to cases like long-running servers that want to catch panics and turn them into 500 Internal Server Error or similar, and keep running. However:

turning unwinds that would have been recovered from into aborts is adding a new DoS vector to such servers

Such servers already have multiple DoS vectors:

  • If you panic while holding a lock, you'll poison the lock, and everything touching that lock is unlikely to be able to recover.
  • If you encounter another panic while panicking, you'll already abort.

This proposal is not making all panics abort; I agree that that would be a huge change to the default. This proposal is making panics in drop abort. And it's already possible to make servers robust against that, by adding close() methods and similar.

@Lokathor

Copy link
Copy Markdown
Contributor

"you already can't recover from lock poisoning" was obviously a gap in the design, and closing that gap is already in progress: rust-lang/rust#96469
So we're actually already working to improve the DoS situation, and the RFC as written would worsen things.

There's three alternatives given, including "opt-in to unwinding", but there isn't an alternative given for "opt-out of unwinding". Since particular unsafe code is the code that can go wrong with a panic in drop (not all unsafe code does), why don't we put the burden on the particular instance of unwind sensitive unsafe code to opt in to having a drop that will abort if an unwind tries to escape from it? Call it #[about_on_unwind_escape] or whatever name. That seems the least disruptive to existing code, while allowing unsafe code the tools it needs to be correct.

@joshtriplett

joshtriplett commented Jul 12, 2022

Copy link
Copy Markdown
Member

@Amanieu We discussed this in today's @rust-lang/lang meeting.

We felt like all of the arguments have been made at this point, and now we need to weigh the tradeoffs between possibilities:

  • Do we feel that we gain more robustness by support catch_unwind on panics from Drop, at the expense of the lower robustness of all the library code that doesn't handle this?
  • Or do we feel that we gain more robustness by eliminating this case as something libraries need to handle, at the expense of the lower robustness of code that wants to handle all possible panics including panics in Drop?

However, before we do that, there are a few changes we'd like to see:

  • The correctness argument should be at the forefront, as that's the primary justification for doing this. Performance is a bonus, and not a reason to change the default.
  • There should be some kind of hook that runs when aborting, to give code an opportunity to do cleanups. (The same hook should run on all panics with panic=abort.)

@TimNN

TimNN commented Jul 12, 2022

Copy link
Copy Markdown
Contributor

The correctness argument should be at the forefront, as that's the primary justification for doing this.

Once the correctness argument is at the forefront, I think it would be great for the RFC to also consider (or at least mention) alternatives that would aid the user in writing correct code, such as:

  1. a less error-prone catch_unwind API.
  2. a lint against any implicit method calls.
  3. some form of #[panics(never)] attribute.

I'm no expert on the compiler, but I feel like (2) should be reasonably easy to implement.

Implementing (3) in a useful manner would likely require a lot of additional design work1.

Footnotes

  1. Last time I though about this I ended up with multiple additional qualifiers like #[panics(never(verify_unstable)], #[panics(never(assume_unsafe)] or #[panics(never(type_dependent)]. Some could be safely combined (like #[panics(never(verify_unstable,type_dependent)]) but others should be linted against (like #[panics(never(assume_unsafe,type_dependent)] unless the annotated function is unsafe). And those qualifiers still don't cover all generic use cases.

@Amanieu

Amanieu commented Jul 12, 2022

Copy link
Copy Markdown
Member Author

There should be some kind of hook that runs when aborting, to give code an opportunity to do cleanups. (The same hook should run on all panics with panic=abort.)

This already exists: an unwind guard will call the panic handler with a PanicInfo that return false for can_unwind (#92988). std's panic handler will call the panic hook as normal but then abort instead of unwinding.

@RalfJung

RalfJung commented Jul 7, 2026

Copy link
Copy Markdown
Member

FWIW, I think at least for implicit drops, there is a very good case to be made that even a "nounwind block" is not a good enough substitute -- having invisible unwinding sites is a level of footgun that's big enough that a more explicit way of dealing with the problem alone is not sufficient, because in too many cases, people will not even realize that they have to reach to such tools. So it seems to be the RFC could be amended fairly easily to deal with my concern.

The same arguments do not apply to explicit drop_in_place calls. Those are "yet another function call" so there is at least something visible in the syntax which indicates that unwinding might occur here.

@Nadrieril

Copy link
Copy Markdown
Member

It seems a lot of the trouble is the combination of unsafe and implicit code. If so, it seems to me that we could make lints that automatically trigger if a function body has both an unsafe block and an implicit operation like autoderef, drop, match ergonomics etc, thus forcing the user to make these things explicit.

(We could even imagine a feature that makes unwinding code explicit)

@maxdexh

maxdexh commented Jul 16, 2026

Copy link
Copy Markdown

@RalfJung

This comment was marked as resolved.

@maxdexh

This comment was marked as resolved.

@RalfJung

Copy link
Copy Markdown
Member

This FCP has stalled out, so let's cancel it. If it's to be proposed again, we should ensure that the concern noted in #3288 (comment) is first addressed in the text.

FWIW, my proposal to resolve the concern would be:

  • First of all, I think explicit drop calls can continue to unwind. The argument for unsafe code being too hard to reason about only really applies to implicit drops; an explicit call to drop or drop_in_place is just another function call and it is already the case that one has to consider every function call as an unwind point.
  • This reduces the question to being able to better control the code the compiler automatically generates for us. The most radical version of this would be to say that we default this auto-generated code to abort-on-unwind, that's basically what the RFC proposes (except it proposes that for all drops, not just implicit ones). The status quo is that it defaults to propagating unwinds. But there's a slider here, we can add an attribute (or some other mechanism) which explicitly controls how all implicit drop calls that are syntactically in its scope behave. If we are maximizing for backwards compatibility, we default to "propagate" and unsafe code authors have to remember to use that attribute. (We'd definitely put that attribute on the btree module. ;) If we are maximizing for soundness of unsafe code, we default to "abort" and people that want to recover from panics have to remember to use the attribute.

I view this as orthogonal to "nounwind" blocks -- even if we had "nounwind" blocks, figuring out where to place them all in btree would be non-trivial.

I didn't expect discussion here would grind to a halt after I raised the concern, but here we are, 2 years later. I agree there is a genuine need here but the amount of people concerned about the change gives me pause, so what does the continuum between the status quo and the RFC look like?

@maxdexh

maxdexh commented Jul 16, 2026

Copy link
Copy Markdown

First of all, I think explicit drop calls can continue to unwind. The argument for unsafe code being too hard to reason about only really applies to implicit drops; an explicit call to drop or drop_in_place is just another function call and it is already the case that one has to consider every function call as an unwind point.

I'm a bit confused by this. The implementation of drop is famously a noop (it's even documented as such). Are you proposing to change that?

But there's a slider here, we can add an attribute (or some other mechanism) which explicitly controls how all implicit drop calls that are syntactically in its scope behave.

I think this is just a less powerful version of an implicit operation lint. An opt-in lint against implicit drops (all values with potential drop glue that are not unconditionally moved into another function) would allow whatever explicit logic is needed (maybe also a std::mem::drop_nounwind function).

@RalfJung

Copy link
Copy Markdown
Member

I'm a bit confused by this. The implementation of drop is famously a noop (it's even documented as such). Are you proposing to change that?

I didn't realize we document this as a noop, that's unfortunate -- so yes I am proposing to take back that part of the docs.

I think this is just a less powerful version of an implicit operation lint. An opt-in lint against implicit drops (all values with potential drop glue that are not unconditionally moved into another function) would allow whatever explicit logic is needed (maybe also a std::mem::drop_nounwind function).

I am doubtful that such a lint is going to be very helpful -- it's surely sometimes useful, but often it's not. First of all I'd then have to clutter my unsafe code with tons of drop everywhere, most of which I don't even care about. Secondly there is some code where you can't make drop explicit, namely any time that code relies on the implicitly generated drop flags.

I'm not a against such a lint, but I cannot see how it could replace the need for "disarming" the implicitly generated code.

@maxdexh

maxdexh commented Jul 16, 2026

Copy link
Copy Markdown

I didn't realize we document this as a noop, that's unfortunate -- so yes I am proposing to take back that part of the docs.

Someone even recently made a YouTube video about the beauty of the docs and implementation of that function. I can't imagine I'm the only one who would be upset by this change.

@Kixunil

Kixunil commented Jul 16, 2026

Copy link
Copy Markdown

I didn't realize we document this as a noop, that's unfortunate

It's not. It's documented to do nothing only for Copy values (obviously) but not for every value. And it's documented that it's implementation contains no body but that doesn't mean the compiler didn't insert drop code - on the contrary, this is not forget! It will certainly run the destructors.

So it makes sense that if we had #[panic_from_implicit_drop = "unwind"] it would be applied to drop as you proposed and I don't think this would be a breaking change if this is the global default anyway. It'd be only important once an edition change switches the default to abort. (Though, I'm not sure I like it. Maybe if the function contains an unsafe block it'd make sense but then we might need unsafe fields.)

@RalfJung

Copy link
Copy Markdown
Member

Someone even recently made a YouTube video about the beauty of the docs and implementation of that function. I can't imagine I'm the only one who would be upset by this change.

Sometimes we have to sacrifice a bit of beauty on the altar of engineering tradeoffs.

@maxdexh

maxdexh commented Jul 16, 2026

Copy link
Copy Markdown

Just to advocate a bit more for a lint, it also has the advantage of being able to be toggled on and off arbitrarily.

I am doubtful that such a lint is going to be very helpful -- it's surely sometimes useful, but often it's not. First of all I'd then have to clutter my unsafe code with tons of drop everywhere, most of which I don't even care about. Secondly there is some code where you can't make drop explicit, namely any time that code relies on the implicitly generated drop flags.

For drop flags, there is already a common workaround: introduce your own by wrapping the type in an option.

The only disadvantage I can think of is that, as you said, this is verbose and that that option would also be subject to the lint. It's also not entirely clear where you would put the allow attribute (e.g. when the val goes out of scope or on the expr that produced it or if it's in a variable, the variable?).

Edit: Wow, I can't write today, sorry.

@RalfJung

Copy link
Copy Markdown
Member

it also has the advantage of being able to be toggled on and off arbitrarily.

Same for the attribute I proposed.

I like the idea of an "implicit code lint", but I view that as orthogonal to, not an alternative to, this RFC.

@Nadrieril

Copy link
Copy Markdown
Member

Secondly there is some code where you can't make drop explicit, namely any time that code relies on the implicitly generated drop flags.

Well, I personally would quite like to see language support for conditional drops like this, namely some kind of drop_if_not_yet_dropped!(place)/ensure_dropped!(place) built-in that does exactly what rustc does at the end of each scope: call drop_in_place on any parts of this place that are still initialized.

(Sorry this is my hobby-horse: I think we'd benefit from the ability to make all the implicits of Rust explicit)

@RalfJung

Copy link
Copy Markdown
Member

While that would be nice, I think it is way too speculative to block this RFC on it. Also for the reasons given above I don't think it would replace this RFC, but merely complement it.

@nbdd0121

Copy link
Copy Markdown
Member

I don't see how a global flag will help with soundness given that people can still opt to be able unwind on drop, and outright disallowing it would be a big compatibility breakage). A per-function #[no_drop_unwind] would serve that purpose better.

@RalfJung

Copy link
Copy Markdown
Member

I think you misunderstood what I said. I didn't talk about a global flag. I talked about attributes that one puts inside the crate so there's no way for downstream crates to overwrite them.

@Diggsey

Diggsey commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@RalfJung distinguishing implicit vs explicit drops was discussed quite a bit in the earlier thread (rust-lang/lang-team#97) so you may wish to read some of the objections to it there.

IMO, making Drop unconditionally panic is elegant, but it does have real downsides and I think it's probably too late to make that kind of change to the language.

What we could do is lint against possible panics from Drop implementations. If the compiler can prove that a given Drop doesn't panic then it can omit the unwinding code and avoid unexpected process aborts, and this gives the best of both worlds. The lint can be introduced in a new edition. This may require the compiler to track more generally whether functions can panic, and I think that would be quite beneficial. It doesn't need to impact the type system: dynamic calls can just be assumed to always potentially unwind.

@RalfJung

RalfJung commented Jul 18, 2026

Copy link
Copy Markdown
Member

I can only find a little bit of discussion there, am I missing something?

Specifically, the only point where anyone argue against it that I was able to find is @Amanieu's comment here. However, IMO that doesn't really engage with the core of the argument (reducing the ecosystem impact of changing this now). Also, writing an explicit self-consuming close-like method will be awkward in many cases and might even require unsafe since, if your type has a destructor, you cannot move out of the fields. File is a non-representative example since the resource it "moves out" is represented by a Copy type (a file descriptor).

The pattern @Amanieu suggests makes sense for types that want to return a Result on drop, which we don't support. But we already do support "signaling errors on drop via panics", for better or worse. Asking types that use this maybe-accidental-but-definitely-real language feature to add a specific cleanup method will cause a lot more churn than is needed to achieve the aims of this proposal -- dealing with unsafe code footguns. So IMO @Amanieu is making the perfect the enemy of the good here. With a time machine I'd also prefer a different solution, but alas, we have to work with the hand we are dealt.

In particular, I was not able to find any comment from a @rust-lang/lang member on this proposal. I'd be curious to hear their thoughts on it.

@programmerjake

Copy link
Copy Markdown
Member

I have a use-case for unwinding from Drop: I have a simulation that generates a trace and I have a drop guard for use in tests that compares the trace to the expected trace (in .vcd format) and panics if they don't match, however before that it checks if a panic is in progress and then just prints the message instead of panicking.

my Drop::drop

a working example of how I use it


With this RFC the behavior of panics in `Drop` is changed to always abort the process. This mirrors a [similar change](https://akrzemi1.wordpress.com/2013/08/20/noexcept-destructors/) made in C++11 where destructors terminate the process if an exception escapes them.

If you have any existing code which relies on panics in `Drop` not aborting the process, you should refactor your code to return errors using a method that consumes `self`. For example consider this hypothetical `close` method on `BufWriter`:

@programmerjake programmerjake Jul 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a good alternative to being forced to add a close-like method is to add an attribute that you can put on variables that should allow unwinding when dropping them? like:

struct PanicOnDrop;

impl Drop for PanicOnDrop {
    fn drop(&mut self) {
        panic!()
    }
}

#[test]
fn my_test() {
    #[drop_can_unwind]
    let v = PanicOnDrop;
    // dropping v will not abort the process
}

fn tricky_unsafe_code<T>(v: T) {
    unsafe {
        ...
        let opt = (i > 5).then_some(v); // won't unwind when dropping v
        ...
    }
}

View changes since the review

@Diggsey

Diggsey commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

@RalfJung oops it was in this thread but in collapsed comments. See from here: #3288 (comment) down to #3288 (comment)

@RalfJung

Copy link
Copy Markdown
Member

I see various people arguing both ways, and a single t-lang comment by @nikomatsakis here.

I don't understand the motivation for some explicit drops being allowed to panic but not others. It seems like these kinds of scenarios would be better encoded as a fn finish(self) that panics, rather than (ab)using Drop. So I'd like to understand what benefit we get specifically from leveraging Drop, given that the drop handler must be explicitly invoked. My view on Drop is that it is meant to be the cleanup code that runs implicitly when the value goes out of scope.

I agree with Niko in a first-principles sense -- if I were to define a new language, it would make sense to say that drop does not have access to effects such as "try" or "async", to contexts, or to panics, and if you need a destructor that needs any of those things then you should write a finish method and make the type linear to ensure it always gets called.

But Rust gave destructors access to panics, and we don't have linear types. We need to navigate how to make unsafe code less error-prone in the context of Rust, not in an idealized language.

So to answer the question here -- the benefit we get is that we are breaking less code and fewer use-cases by letting people still invoke drop in a way that may unwind when they are not doing dangerous unsafe things.

Niko also writes:

Put another way, I really think we should be striving for a single set of rules that apply to Drop impls, no matter how they wind up being invoked. If we want differing sets of rules, perhaps we want another trait. So can we compare the value from distinguishing explicit drop vs having a finish method vs having a another trait (say Finish)?

The "rules for drop" in the sense of opsem and safety are the same either way. All we are doing is we are changing the implicitly inserted drops to be more like

abort_on_unwind(|| drop(x));

I think we can even let people control this in a per-function way.

Especially in this world where people can control drop-on-unwind in a per-function way, I think it makes a lot more sense to do that only for automatic drop happening inside the function. Explicit drop calls are dropped in std where that function can set the "please propagate unwinds" flag, and while explicit drop_in_place calls could be wrapped in abort_on_unwind that seems like a strangely magical thing to do. It's much less strange to have an attribute affect code that the compiler is already magically generating, than having it affect code the user wrote.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.