Skip to content

Undefined behavior when moving a coroutine that reborrows from itself #159443

Closed

Description

@ais523

(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

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    S-has-mcveStatus: A Minimal Complete and Verifiable Example has been found for this issue
    on Jul 17, 2026
  2. RalfJung commented on Jul 17, 2026

    @RalfJung
    Member

    I find this variant more clear:

    #![feature(gen_blocks)]
    
    fn make_gen(r: &mut u8) -> impl Iterator<Item=u8> {
        gen move {
            let a = &mut *r; // explicit reborrow
            *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 think fundamentally what is going wrong here is that r is a borrowed-from field, but when the generator is moved it is treated like any normal mutable reference, so it gets retagged in the j = i assignment. 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 a MaybeDangling.

  3. ais523 commented on Jul 17, 2026

    @ais523
    Author

    I think that if you just wrap r in MaybeDangling, 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 while a is reborrowing from r, then advance the generator inside the function in a way that causes the reborrow to end and then accesses r, then the access via r violates the protector that was created on a while passing the generator to the function (even though a is 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).

  4. RalfJung commented on Jul 17, 2026

    @RalfJung
    Member

    then the access via r violates the protector that was created on a

    Sounds like a also needs to be put into a MaybeDangling.

    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 suppress noalias and dereferenceable).

    I find it incredibly unintuitive that a reference that has been dropped already could potentially prevent accesses through the reference it was reborrowed from

    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.

  5. added 3 commits that reference this issue on Sep 4, 2026
  6. added a commit that references this issue on Sep 5, 2026
  7. added a commit that references this issue on Sep 5, 2026
  8. added a commit that references this issue on Sep 5, 2026
  9. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 5, 2026
  10. added a commit that references this issue on Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-coroutinesArea: CoroutinesC-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessS-has-mcveStatus: A Minimal Complete and Verifiable Example has been found for this issuerequires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions