Skip to content

Inconsistent and nonsensical borrow-checking error from unwind path of destructor of moved-out variable #156713

Description

@theemathas

This issue is based on @orlp's discovery in #136703 (comment) and #155902 (comment).

I tried this code:

// edition 2021 or earlier

struct Foo;
impl Drop for Foo {
    fn drop(&mut self) {}
}

struct Bar<'a>(&'a Foo);
impl Drop for Bar<'_> {
    fn drop(&mut self) {}
}

// This compiles
fn works() {
    let foo = Foo;
    let bar = Bar(&foo);
    drop(match {(bar,)} {
        args => args,
    })
}

// This errors
fn fails() {
    let foo = Foo;
    let bar = Bar(&foo);
    drop(match (bar,) {
        args => args,
    })
}

I expected both works and fails to compile. Instead, works compiles, but fails produces the following error:

error[E0597]: `foo` does not live long enough
  --> src/lib.rs:25:19
   |
24 |     let foo = Foo;
   |         --- binding `foo` declared here
25 |     let bar = Bar(&foo);
   |                   ^^^^ borrowed value does not live long enough
26 |     drop(match (bar,) {
   |                ------ a temporary with access to the borrow is created here ...
...
29 | }
   | -
   | |
   | `foo` dropped here while still borrowed
   | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `(Bar<'_>,)`
   |
help: consider adding semicolon after the expression so its temporaries are dropped sooner, before the local variables declared by the block are dropped
   |
28 |     });
   |       +

For more information about this error, try `rustc --explain E0597`.

Below is a comparison of the MIR of the two functions. The difference in the MIR is where the control flow goes if the destructor of Bar panics (drop(_7) in bb0). This destructor seems to be, for some reason, ran immediately after Bar is moved into the tuple.

$ diff mir_dump/foo.{works,fails}.1-1-000.built.after.mir
1c1
< // MIR for `works` after built
---
> // MIR for `fails` after built
3c3
< fn works() -> () {
---
> fn fails() -> () {
41c41
<         drop(_7) -> [return: bb1, unwind: bb13];
---
>         drop(_7) -> [return: bb1, unwind: bb10];
79c79
<         drop(_6) -> [return: bb8, unwind: bb15];
---
>         drop(_6) -> [return: bb8, unwind: bb13];
100c100
<         drop(_6) -> [return: bb15, unwind terminate(cleanup)];
---
>         drop(_6) -> [return: bb13, unwind terminate(cleanup)];
104,111d103
<         drop(_2) -> [return: bb14, unwind terminate(cleanup)];
<     }
< 
<     bb14 (cleanup): {
<         drop(_1) -> [return: bb15, unwind terminate(cleanup)];
<     }
< 
<     bb15 (cleanup): {
MIR of works
// MIR for `works` after built

fn works() -> () {
    let mut _0: ();
    let _1: Foo;
    let mut _3: &Foo;
    let _4: &Foo;
    let mut _5: (Bar<'_>,);
    let mut _6: (Bar<'_>,);
    let mut _7: Bar<'_>;
    scope 1 {
        debug foo => _1;
        let _2: Bar<'_>;
        scope 2 {
            debug bar => _2;
            let _8: (Bar<'_>,);
            scope 3 {
                debug args => _8;
            }
        }
    }

    bb0: {
        StorageLive(_1);
        _1 = Foo;
        FakeRead(ForLet(None), _1);
        StorageLive(_2);
        StorageLive(_3);
        StorageLive(_4);
        _4 = &_1;
        _3 = &(*_4);
        _2 = Bar::<'_>(move _3);
        StorageDead(_3);
        FakeRead(ForLet(None), _2);
        StorageDead(_4);
        StorageLive(_5);
        StorageLive(_6);
        StorageLive(_7);
        _7 = move _2;
        _6 = (move _7,);
        drop(_7) -> [return: bb1, unwind: bb13];
    }

    bb1: {
        StorageDead(_7);
        PlaceMention(_6);
        StorageLive(_8);
        _8 = move _6;
        _5 = move _8;
        drop(_8) -> [return: bb3, unwind: bb10];
    }

    bb2: {
        FakeRead(ForMatchedPlace(None), _6);
        unreachable;
    }

    bb3: {
        StorageDead(_8);
        goto -> bb4;
    }

    bb4: {
        _0 = std::mem::drop::<(Bar<'_>,)>(move _5) -> [return: bb5, unwind: bb9];
    }

    bb5: {
        StorageDead(_5);
        drop(_2) -> [return: bb6, unwind: bb11];
    }

    bb6: {
        StorageDead(_2);
        drop(_1) -> [return: bb7, unwind: bb12];
    }

    bb7: {
        StorageDead(_1);
        drop(_6) -> [return: bb8, unwind: bb15];
    }

    bb8: {
        StorageDead(_6);
        return;
    }

    bb9 (cleanup): {
        drop(_5) -> [return: bb10, unwind terminate(cleanup)];
    }

    bb10 (cleanup): {
        drop(_2) -> [return: bb11, unwind terminate(cleanup)];
    }

    bb11 (cleanup): {
        drop(_1) -> [return: bb12, unwind terminate(cleanup)];
    }

    bb12 (cleanup): {
        drop(_6) -> [return: bb15, unwind terminate(cleanup)];
    }

    bb13 (cleanup): {
        drop(_2) -> [return: bb14, unwind terminate(cleanup)];
    }

    bb14 (cleanup): {
        drop(_1) -> [return: bb15, unwind terminate(cleanup)];
    }

    bb15 (cleanup): {
        resume;
    }
}
MIR of fails
// MIR for `fails` after built

fn fails() -> () {
    let mut _0: ();
    let _1: Foo;
    let mut _3: &Foo;
    let _4: &Foo;
    let mut _5: (Bar<'_>,);
    let mut _6: (Bar<'_>,);
    let mut _7: Bar<'_>;
    scope 1 {
        debug foo => _1;
        let _2: Bar<'_>;
        scope 2 {
            debug bar => _2;
            let _8: (Bar<'_>,);
            scope 3 {
                debug args => _8;
            }
        }
    }

    bb0: {
        StorageLive(_1);
        _1 = Foo;
        FakeRead(ForLet(None), _1);
        StorageLive(_2);
        StorageLive(_3);
        StorageLive(_4);
        _4 = &_1;
        _3 = &(*_4);
        _2 = Bar::<'_>(move _3);
        StorageDead(_3);
        FakeRead(ForLet(None), _2);
        StorageDead(_4);
        StorageLive(_5);
        StorageLive(_6);
        StorageLive(_7);
        _7 = move _2;
        _6 = (move _7,);
        drop(_7) -> [return: bb1, unwind: bb10];
    }

    bb1: {
        StorageDead(_7);
        PlaceMention(_6);
        StorageLive(_8);
        _8 = move _6;
        _5 = move _8;
        drop(_8) -> [return: bb3, unwind: bb10];
    }

    bb2: {
        FakeRead(ForMatchedPlace(None), _6);
        unreachable;
    }

    bb3: {
        StorageDead(_8);
        goto -> bb4;
    }

    bb4: {
        _0 = std::mem::drop::<(Bar<'_>,)>(move _5) -> [return: bb5, unwind: bb9];
    }

    bb5: {
        StorageDead(_5);
        drop(_2) -> [return: bb6, unwind: bb11];
    }

    bb6: {
        StorageDead(_2);
        drop(_1) -> [return: bb7, unwind: bb12];
    }

    bb7: {
        StorageDead(_1);
        drop(_6) -> [return: bb8, unwind: bb13];
    }

    bb8: {
        StorageDead(_6);
        return;
    }

    bb9 (cleanup): {
        drop(_5) -> [return: bb10, unwind terminate(cleanup)];
    }

    bb10 (cleanup): {
        drop(_2) -> [return: bb11, unwind terminate(cleanup)];
    }

    bb11 (cleanup): {
        drop(_1) -> [return: bb12, unwind terminate(cleanup)];
    }

    bb12 (cleanup): {
        drop(_6) -> [return: bb13, unwind terminate(cleanup)];
    }

    bb13 (cleanup): {
        resume;
    }
}

Note that the works function previously errored in rust 1.35.0, but started compiling in 1.36.0. The fails function has been erroring since rust 1.0.0

Meta

Reproducible on the playground with version 1.97.0-nightly (2026-05-17 507271bc119683008ec7)

Activity

  1. added
    A-destructorsArea: Destructors (`Drop`, …)
    T-langRelevant to the language team
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.html
    on May 18, 2026
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on May 18, 2026
  3. changed the title [-]Inconsistent and nonsensical borrow-checking error from unwind path of moved-out tuple[/-] [+]Inconsistent and nonsensical borrow-checking error from unwind path of moved-out variable[/+] on May 18, 2026
  4. changed the title [-]Inconsistent and nonsensical borrow-checking error from unwind path of moved-out variable[/-] [+]Inconsistent and nonsensical borrow-checking error from unwind path of destructor of moved-out tuple[/+] on May 18, 2026
  5. changed the title [-]Inconsistent and nonsensical borrow-checking error from unwind path of destructor of moved-out tuple[/-] [+]Inconsistent and nonsensical borrow-checking error from unwind path of destructor of moved-out variable[/+] on May 19, 2026
  6. dianne commented on May 23, 2026

    @dianne
    Member

    The difference in the MIR is where the control flow goes if the destructor of Bar panics (drop(_7) in bb0). This destructor seems to be, for some reason, ran immediately after Bar is moved into the tuple.

    That drop is for the temporary that holds the tuple constructor's operand: https://doc.rust-lang.org/nightly/reference/destructors.html#operands. It's not actually run, since that temporary is moved out of immediately (drop elaboration should turn it into a goto), but borrowck doesn't seem to ignore dead unwind edges so it's effectively treated as if it can panic.

    In fails, unwinding from drop(_7) drops the temporary holding the match scrutinee before foo, due to pre-2024 tail expression scoping.

    In works, we seemingly skip dropping the scrutinee when unwinding from drop(_7)? There's two separate unwind paths there, whereas normally they're joined together in a tree (like in fails): unwinding from drop(_7) drops _2, then _1 (i.e. bar, then foo, leaving the scrutinee un-dropped), but unwinding elsewhere drops _2, then _1, then _6" (i.e. bar, then foo, then the scrutinee, as expected). When leaving the scope for the tuple expression, (i.e. when the drop for _7 is built), _6's DropKind::Value drop isn't scheduled, so it doesn't end up in the drop tree. Temporaries' DropKind::Value drops are only scheduled after lowering the expression being put into the temporary. Usually (and in fails) this is fine: when we leave the tuple expression, we've scheduled the drop for _6, so if _7's drop panics, _6 is dropped. In works, we leave the tuple expression (i.e. we build the drop for _7) before leaving the expression being put into the temporary (i.e. the block), so _6's drop hasn't yet been scheduled when building the unwind drops for _7's drop.

    I'll have to think about whether this can cause drops to be forgotten on unwind paths that can actually be run.

  7. dianne commented on May 23, 2026

    @dianne
    Member

    Here's an example where it's observable at runtime (playground):

    struct LoudDrop;
    impl Drop for LoudDrop {
        fn drop(&mut self) {
            println!("dropped");
        }
    }
    fn new() -> LoudDrop {
        println!("created");
        LoudDrop
    }
    
    struct PanicOnDrop;
    impl Drop for PanicOnDrop {
        fn drop(&mut self) {
            panic!()
        }
    }
    
    fn main() {
        match { let _control = new(); let _panicky = PanicOnDrop; new() } {
            _ => {}
        }
    }

    This properly runs the destructor for _control after panicking, but it forgets the result of the new() in the tail expression. the output is:

    created
    created
    dropped
    
  8. theemathas commented on May 23, 2026

    @theemathas
    ContributorAuthor

    So... we have a resource leak? 😱

    I thought only return values could be leaked this way. See #47949.

  9. theemathas commented on May 23, 2026

    @theemathas
    ContributorAuthor

    Oh, that's already known. See #47949 (comment)

  10. theemathas commented on May 24, 2026

    @theemathas
    ContributorAuthor

    @dianne In works(), replacing {(bar,)} with identity((bar,)) also results in code that compiles. Can that case also result in a leak?

  11. theemathas commented on May 24, 2026

    @theemathas
    ContributorAuthor

    Ah, it can, if the function is a closure:

    struct LoudDrop;
    impl Drop for LoudDrop {
        fn drop(&mut self) {
            println!("dropped");
        }
    }
    fn new() -> LoudDrop {
        println!("created");
        LoudDrop
    }
    
    struct PanicOnDrop;
    impl Drop for PanicOnDrop {
        fn drop(&mut self) {
            panic!()
        }
    }
    
    fn foo(f: impl FnOnce(LoudDrop) -> LoudDrop) {
        f(new());
    }
    
    fn main() {
        let x = PanicOnDrop;
        foo(move |y| (&x, y).1);
    }

    The above code prints "created" but not "drop".

  12. self-assigned this
    on Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-borrow-checkerArea: The borrow checkerA-destructorsArea: Destructors (`Drop`, …)A-panicArea: Panicking machineryC-bugCategory: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions