Rust: Panics in destructors can cause the return value to be leaked

Created on 1 Feb 2018  路  24Comments  路  Source: rust-lang/rust

STR

struct NoisyDrop;
impl Drop for NoisyDrop {
    fn drop(&mut self) {
        println!("dropping a NoisyDrop");
    }
}

impl NoisyDrop {
    fn new() -> Self {
        println!("creating a NoisyDrop");
        NoisyDrop
    }
}

struct PanickyDrop;
impl Drop for PanickyDrop {
    fn drop(&mut self) {
        panic!()
    }
}

fn foo() -> NoisyDrop {
    let p = PanickyDrop;
    NoisyDrop::new()
}

fn main() {
    foo();
}

Expected Result

"creating a NoisyDrop" and "dropping a NoisyDrop" should appear the same number of times

Actual Result

creating a NoisyDrop
thread 'main' panicked at 'explicit panic', src/main.rs:18:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.

The destructor is ignored.

A-destructors C-enhancement I-unsound 馃挜 P-medium T-compiler T-lang

Most helpful comment

Oh, looks like it was backed out due to a perf regression. (EDIT: you beat me to it.^^)

I agree we should avoid perf regressions, but ultimately I think correct compilation is more important than performance... otherwise I have some good ideas for making the compiler go faster by producing incorrect code. ;) In particular I do think this qualifies as a soundness bug (like all incorrect compilations do); there might be unsafe code relying on rustc doing the right thing here.

All 24 comments

Heh, this even happens on Rust 1.0

@jonas-schievink

Rust 1.0 is far worse than this - it leaks everything when a destructor panics.

@arielb1, could you explain why this is tagged as a soundness bug? The language makes no guarantee that destructors will ever be invoked.

ping @rust-lang/compiler this should be triagged, even if it is not a soundness bug, unwinding working properly is something that a lot of code relies on.

So, to be clear, it's not that we leak all destructors when panic in drop occurs. For example, this function drops both p and n as expected:

fn foo() {
    let n = NoisyDrop::new();
    let p = PanickyDrop;
}

The specific scenario here is that:

  • we are in the middle of returning the NoisyDrop to our caller (so our caller "owns it")
  • but we panic before our return completes (so they don't realize they own it)

I don't think it's quite right to say that "we make no guarantee that destructors will ever be invoked". We don't guarantee that all destructors are always invoked, but we do guarantee some things: for example, that if you have a local variable whose scope includes a call, it will be dropped when the call panics (here, n would be dropped):

let n = ...;
foo();

So the question is what we should guarantee in this particular scenario. Perhaps the answer is that we should just be very clear to document the current behavior, given how long it has existed.

Adding T-lang -- I think that documenting the expected order would be a good first step.

We discussed in the @rust-lang/lang meeting. Everyone agreed this behavior is wrong; the returned value ought to be dropped, presumably after all other variables in the frame have been dropped. Basically it should be equivalent to having stored the value into an outer let.

However, as a good first step, it seems like we should begin by verifying the behavior with a more thorough set of test cases. For example, I found (playground) that the problem applies not only to returns but to any store:

fn foo() -> NoisyDrop {
    let n = NoisyDrop::new(1);
    let x = {
        let p = PanickyDrop;
        NoisyDrop::new(2) // <-- dtor for this never runs 
    };
    panic!()
}

Ideally what we would do is this:

  • Create various tests covering these corner cases (also perhaps things like panic when running the dtor of a struct field) so we know the current behavior
  • Decide on the desired behavior and, depending on what we think, maybe writing up an RFC
  • Implementing the fixes in drop elaboration

We thought perhaps @matthewjasper might be interested in tackling this, in particular =)

I'm leaving nominated so we follow up next meeting and discuss priority -- how serious do we think it is to fix this. Obviously, this is a long-standing behavior.

Oh, there's an issue for this.

  • Create various tests covering these corner cases (also perhaps things like panic when running the dtor of a struct field) so we know the current behavior

This should be done for evaluation order as well.

  • Implementing the fixes in drop elaboration

The bug is in MIR construction. I don't think it's that hard to fix (except possibly for the return place).

We thought perhaps @matthewjasper might be interested in tackling this, in particular =)

=)

@matthewjasper

The bug is in MIR construction.

Oh?

The latest perf results in https://github.com/rust-lang/rust/pull/66703#issuecomment-593002683 are a bit higher than I would like, but I can't see any way to reduce it.

So https://github.com/rust-lang/rust/pull/61430 was marked as fixing this, but actually did not, or why did the issue get reopened?

61430 was reverted for being far too large a perf regression.

Oh, looks like it was backed out due to a perf regression. (EDIT: you beat me to it.^^)

I agree we should avoid perf regressions, but ultimately I think correct compilation is more important than performance... otherwise I have some good ideas for making the compiler go faster by producing incorrect code. ;) In particular I do think this qualifies as a soundness bug (like all incorrect compilations do); there might be unsafe code relying on rustc doing the right thing here.

Here's an example for why this is a soundness bug. weird_drop::drop is silly, but it should be correct, and this bug makes it cause UB (as you can see when running this code in Miri).

I'll re-add the I-unsound label.

Hmm, yeah, I had forgotten about this.

@RalfJung you predicted correctly, https://github.com/taiki-e/pin-project/pull/194 ran into this issue, and if @taiki-e hadn't spotted it, it would have caused unsoundness in the generated code.

@Diggsey @taiki-e that's a good catch!

So, can we revive #66703? I don't know what the policy is on how much perf impact we are willing to accept for soundness fixes -- this is probably a case-by-case thing.

@RalfJung It looks like Matthew is now going for a different approach in #71840 that should be more efficient

It's the same approach as in #66703, which has perf runs.

Indeed, I'll create an issue to reapply it.

https://github.com/rust-lang/rust/pull/71840 has been re-landed in https://github.com/rust-lang/rust/pull/77466. What's the next step for this issue?

The next step is for me to resurrect #72152 and see how a perf run looks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dnsl48 picture dnsl48  路  3Comments

mcarton picture mcarton  路  3Comments

dwrensha picture dwrensha  路  3Comments

pedrohjordao picture pedrohjordao  路  3Comments

SharplEr picture SharplEr  路  3Comments