Repository navigation
Undefined behavior when moving a coroutine that reborrows from itself #159443
Description
(Split from #63818 on @RalfJung's request, because the mechanism is significantly different from the existing cases that were discussed there.)
In the current implementation of coroutines, a coroutine can be Unpin if it does not borrow from itself (which seems correct to me, because if it contains no pointers to itself, moving it does not directly invalidate any references). However, if the coroutine reborrows (as opposed to borrowing) from itself, the current lowering of coroutines makes it undefined behavior to actually move.
I tried this code under Miri (playground):
#![feature(gen_blocks)]
fn make_gen(r: &mut u8) -> impl Iterator<Item=u8> {
gen move {
let a = r;
*a = 1;
yield 1;
*a = 2;
}
}
pub fn main() {
let mut a = 1;
let mut i = make_gen(&mut a);
dbg!(i.next());
let mut j = i;
dbg!(j.next());
}I expected to see this happen: The program produces debug output of Some(1) followed by None and exits successfully.
Instead, this happened: Under both Stacked Borrows and Tree Borrows, Miri reports undefined behavior. (The example could be simplified a bit and still be undefined behavior under Stacked Borrows, but this version of it is undefined behavior under Tree Borrows too.)
I discovered this bug while trying to make a safe abstraction around the concept of "an object that effectively contains two references, one of which reborrows from the other". One of my attempts to implement it contained a subtle mistake (related to violating protectors that were protecting an object that had since been dropped), and I was curious as to whether the same mistake had been made in the implementation of coroutines (which are also able to reborrow from themselves). I ended up finding this (much less subtle) issue instead.
Although coroutines are indirectly accessible on stable Rust (via using async/await), I don't believe it's possible to create an Unpin coroutine in stable Rust, and thus I think this bug is nightly-only.
Meta
Tested on the Rust playfield, version "1.99.0-nightly (2026-07-16 3d50c25)".
@rustbot label +I-unsound +S-has-mcve +A-coroutines +requires-nightly
Activity
RalfJung commentedon Jul 17, 2026
I find this variant more clear:
I think fundamentally what is going wrong here is that
ris a borrowed-from field, but when the generator is moved it is treated like any normal mutable reference, so it gets retagged in thej = iassignment. That does not make sense, we need this mutable reference to be carried along without ever getting retagged. The canonical way to achieve that is to store it inside aMaybeDangling.ais523 commentedon Jul 17, 2026
I think that if you just wrap
rinMaybeDangling, you hit the original subtle problem that inspired me to look into this in the first place: if you pass the generator by value as a function/method argument whileais reborrowing fromr, then advance the generator inside the function in a way that causes the reborrow to end and then accessesr, then the access viarviolates the protector that was created onawhile passing the generator to the function (even thoughais no longer live).I find it incredibly unintuitive that a reference that has been dropped already could potentially prevent accesses through the reference it was reborrowed from (and much of my recent work on theoretical Rust has been about trying to find a way to change the opsem to permit this sort of access without causing a significant loss of optimisation potential).
RalfJung commentedon Jul 17, 2026
Sounds like
aalso needs to be put into aMaybeDangling.Alternatively we could just declare that all fields of generators are "like
MaybeDangling" in that we don't retag inside of them. That's fairly trivial to do in Miri, and hopefully not too hard for codegen either (where it has to suppressnoaliasanddereferenceable).dropping a reference is a NOP so I can't entirely make sense of this statement. But if it's a wider point, not specifically about this issue, then maybe it is better suited for a Zulip topic.Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Rollup merge of rust-lang#160745 - RalfJung:closure-maybe-dangling, r…
Unrolled build for #160745 in rollup 162333
Rollup merge of #160745 - RalfJung:closure-maybe-dangling, r=WaffleLa…