Related to #159443, but different because it does not involve a reborrow: if you move an Unpin coroutine while it has a false drop flag on one of its local variables, this will do a typed move on the coroutine field that represents the global variable, even though the field is in a moved-from state (which is what the false drop flag represents). This can be undefined behavior if the field has a type that makes it dangerous to move out of the same field twice without moving a value into it in between.
I tried this code under Miri (playground):
#![feature(gen_blocks)]
// a struct that has a drop flag and contains a reference
struct DropMut<T: 'static>(&'static mut T);
impl<T: 'static> Drop for DropMut<T> { fn drop(&mut self) {} }
fn main() {
let mut a = gen {
let b = DropMut(Box::leak(Box::new(1)));
// create a drop flag on `b`
let c;
if true { c = b; } // and ensure it's set to false
else { c = DropMut(Box::leak(Box::new(2))); }
*c.0 = 3;
4.yield;
*c.0 = 5;
};
let _ = a.next();
let mut d = a;
let _ = d.next();
}
I expected the correct behavior to be: No Miri errors are reported until the program exits (at which point it reports a memory leak, caused by the call to Box::leak).
Instead, this happened: Miri reported undefined behavior under both Stacked Borrows and Tree Borrows.
As usual, simpler examples will produce UB under Stacked Borrows, but this example produces UB under Tree Borrows too.
What's happening here is that the field of a that represents the coroutine's local variable b is being moved from twice without having a value moved into it in between: the first move is into the field of a that represents c, and the second move is into the field of d that represents b. The coroutine contains a drop flag that reflects the fact that b is no longer valid; however, the drop flag is stored as a product type rather than a sum type (i.e. the field is represented as (DropMut, bool) rather than Option<DropMut>), so even if the drop flag is false, the field gets moved anyway. Some types of fields are dangerous to move from twice; this was famously a problem with Box in the past, but nowadays I think the only such types are those which have a destructor and contain a reference as a struct/enum field.
This could be fixed either by wrapping the coroutine in MaybeDangling, or by using the equivalent of Option<T> as the representation for drop-flagged variables rather than (T, bool). (Note that the same problem can probably happen with drop-flagged struct fields of local variables, which may rule out the latter option.) (EDIT: and the former option doesn't work either because there are cases where it doesn't help; see the next comment.)
I noticed this bug while working on an aliasing model that's intended to be more suitable for coroutines than the current aliasing models, which forced me to consider all the possible things you might be able to do to a local variable via manipulating the coroutine it belonged to.
Meta
@rustbot label +A-coroutines +A-destructors +requires-nightly +I-unsound +S-has-mcve
This is nightly-only, because the only coroutines you can create on stable are futures generated by async, and those are !Unpin.
Tested on the Rust playground, version 1.99.0-nightly (2026-08-12 c98d0cb27cc63afdd626)
Backtrace
error: Undefined Behavior: write access through <565> at alloc303[0x0] is forbidden
--> src/main.rs:14:9
|
14 | *c.0 = 5;
| ^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information
= help: the accessed tag <565> is a child of the conflicting tag <560>
= help: the conflicting tag <560> has state Frozen which forbids this child write access
help: the accessed tag <565> was created here
--> src/main.rs:17:17
|
17 | let mut d = a;
| ^
help: the conflicting tag <560> was created here, in the initial state Reserved
--> src/main.rs:10:19
|
10 | if true { c = b; }
| ^
help: the conflicting tag <560> later transitioned to Unique due to a child write access at offsets [0x0..0x4]
--> src/main.rs:12:9
|
12 | *c.0 = 3;
| ^^^^^^^^
= help: this transition corresponds to the first write to a 2-phase borrowed mutable reference
help: the conflicting tag <560> later transitioned to Frozen due to a reborrow (acting as a foreign read access) at offsets [0x0..0x4]
--> src/main.rs:17:17
|
17 | let mut d = a;
| ^
= help: this transition corresponds to a loss of write permissions
= note: stack backtrace:
0: main::{closure#0}
at src/main.rs:14:9: 14:17
1: main
at src/main.rs:18:13: 18:21
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error
Related to #159443, but different because it does not involve a reborrow: if you move an
Unpincoroutine while it has a false drop flag on one of its local variables, this will do a typed move on the coroutine field that represents the global variable, even though the field is in a moved-from state (which is what the false drop flag represents). This can be undefined behavior if the field has a type that makes it dangerous to move out of the same field twice without moving a value into it in between.I tried this code under Miri (playground):
I expected the correct behavior to be: No Miri errors are reported until the program exits (at which point it reports a memory leak, caused by the call to
Box::leak).Instead, this happened: Miri reported undefined behavior under both Stacked Borrows and Tree Borrows.
As usual, simpler examples will produce UB under Stacked Borrows, but this example produces UB under Tree Borrows too.
What's happening here is that the field of
athat represents the coroutine's local variablebis being moved from twice without having a value moved into it in between: the first move is into the field ofathat representsc, and the second move is into the field ofdthat representsb. The coroutine contains a drop flag that reflects the fact thatbis no longer valid; however, the drop flag is stored as a product type rather than a sum type (i.e. the field is represented as(DropMut, bool)rather thanOption<DropMut>), so even if the drop flag is false, the field gets moved anyway. Some types of fields are dangerous to move from twice; this was famously a problem withBoxin the past, but nowadays I think the only such types are those which have a destructor and contain a reference as astruct/enumfield.This could be fixed either by wrapping the coroutine in
MaybeDangling, or by using the equivalent ofOption<T>as the representation for drop-flagged variables rather than(T, bool). (Note that the same problem can probably happen with drop-flagged struct fields of local variables, which may rule out the latter option.) (EDIT: and the former option doesn't work either because there are cases where it doesn't help; see the next comment.)I noticed this bug while working on an aliasing model that's intended to be more suitable for coroutines than the current aliasing models, which forced me to consider all the possible things you might be able to do to a local variable via manipulating the coroutine it belonged to.
Meta
@rustbot label +A-coroutines +A-destructors +requires-nightly +I-unsound +S-has-mcve
This is nightly-only, because the only coroutines you can create on stable are futures generated by
async, and those are!Unpin.Tested on the Rust playground, version
1.99.0-nightly (2026-08-12 c98d0cb27cc63afdd626)Backtrace