rust-lang / rust-lang/rust

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

Open
#156,713 6 comments 0 reactions 1 assignee View on GitHub

@cjgillot is already working on this.

Since Jun 23, 2026.

A-borrow-checker A-destructors A-MIR A-panic C-bug needs-triage T-compiler T-lang
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

This issue is based on @orlp's discovery in https://github.com/rust-lang/rust/issues/136703#issuecomment-4471186826 and https://github.com/rust-lang/rust/issues/155902#issuecomment-4471860192.

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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.