Repository navigation
Inconsistent and nonsensical borrow-checking error from unwind path of destructor of moved-out variable #156713
Copy link
Copy link
Open
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-borrow-checkerArea: The borrow checkerArea: The borrow checkerA-destructorsArea: Destructors (`Drop`, …)Area: Destructors (`Drop`, …)A-panicArea: Panicking machineryArea: Panicking machineryC-bugCategory: This is a bug.Category: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamRelevant to the language teamneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
Description
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)
Metadata
Metadata
Assignees
Labels
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-borrow-checkerArea: The borrow checkerArea: The borrow checkerA-destructorsArea: Destructors (`Drop`, …)Area: Destructors (`Drop`, …)A-panicArea: Panicking machineryArea: Panicking machineryC-bugCategory: This is a bug.Category: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamRelevant to the language teamneeds-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triagingThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
Activity
[-]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[/+][-]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[/+][-]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[/+]dianne commentedon May 23, 2026
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 fromdrop(_7)drops the temporary holding the match scrutinee beforefoo, due to pre-2024 tail expression scoping.In
works, we seemingly skip dropping the scrutinee when unwinding fromdrop(_7)? There's two separate unwind paths there, whereas normally they're joined together in a tree (like infails): unwinding fromdrop(_7)drops_2, then_1(i.e.bar, thenfoo, leaving the scrutinee un-dropped), but unwinding elsewhere drops_2, then_1, then_6" (i.e.bar, thenfoo, then the scrutinee, as expected). When leaving the scope for the tuple expression, (i.e. when the drop for_7is built),_6'sDropKind::Valuedrop isn't scheduled, so it doesn't end up in the drop tree. Temporaries'DropKind::Valuedrops are only scheduled after lowering the expression being put into the temporary. Usually (and infails) this is fine: when we leave the tuple expression, we've scheduled the drop for_6, so if_7's drop panics,_6is dropped. Inworks, 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.
dianne commentedon May 23, 2026
Here's an example where it's observable at runtime (playground):
This properly runs the destructor for
_controlafter panicking, but it forgets the result of thenew()in the tail expression. the output is:theemathas commentedon May 23, 2026
So... we have a resource leak? 😱
I thought only return values could be leaked this way. See #47949.
theemathas commentedon May 23, 2026
Oh, that's already known. See #47949 (comment)
theemathas commentedon May 24, 2026
@dianne In
works(), replacing{(bar,)}withidentity((bar,))also results in code that compiles. Can that case also result in a leak?theemathas commentedon May 24, 2026
Ah, it can, if the function is a closure:
The above code prints "created" but not "drop".