rust-lang / rust-lang/rust

`pin!()` (and any other unsafe code relying on local variables having a fixed location) in a coroutine or `gen` block is unsound

Open
#153,688 20 comments 3 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-macros A-pin C-bug F-coroutines F-gen_blocks I-unsound requires-nightly T-lang T-libs T-opsem
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

View all comments

Coroutines whose local variables don't borrow each other across a yield point implements Unpin. Moving an ongoing Unpin coroutine causes all its alive local variables to be moved. pin!() relies on the assumption that an unnamable local variable cannot be moved. All of this combined together results in an unsoundness.

The below code causes a use-after-free. In my testing, it prints 8192, which is presumably some garbage data lying around.

#![feature(coroutines, coroutine_trait)]

use std::ops::Coroutine;
use std::pin::{Pin, pin};
use std::task::{Context, Waker};

// Pins the value, calls the callback with it, moves the value, then drops it.
// This violates the Pin guarantee
fn wrong_pin<T>(value: T, callback: impl FnOnce(Pin<&mut T>)) {
    let mut coro = Box::new(Some(
        #[coroutine]
        || {
            let value = value;
            // Move the value into an unnamable variable, and lifetime-extend it
            let pinned_value = pin!(value);
            callback(pinned_value);
            // The unnamable variable is still alive by the time we yield
            yield;
        },
    ));
    // Run the coroutine until the yield
    let _ = Pin::new((&mut *coro).as_mut().unwrap()).resume(());
    // Move the coroutine while the unnamable variable is still alive
    let moved = *coro;
    // Overwrite the heap memory to make the UB easily observable.
    *coro = None;
    drop(coro);
    // Drop the unnamable variable
    drop(moved);
}

// Cause UB with the Pin guarantee violation
fn main() {
    let fut = async {
        // Work around #63818, to make it clear that this is a distinct issue
        let dummy = std::marker::PhantomPinned;
        let x = 1;
        // Make the future self-referential,
        // and access that reference when the future is dropped
        let _y = PrintOnDrop(&x);
        std::future::pending::<()>().await;
        // Hold the value across an await point, so the generated future
        // contains the value and becomes !Unpin, maybe? Not sure.
        let _dummy = dummy;
    };
    wrong_pin(fut, |pinned_fut| {
        // Run the future until the await point
        let mut ctx = Context::from_waker(Waker::noop());
        let _ = pinned_fut.poll(&mut ctx);
    });
}

struct PrintOnDrop<'a>(&'a i32);
impl Drop for PrintOnDrop<'_> {
    fn drop(&mut self) {
        println!("{}", self.0);
    }
}

Miri output:

error: Undefined Behavior: constructing invalid value at .<enum-variant(Some)>.0.<coroutine-state(3)>.<captured-var(value)>.<coroutine-state(3)>.<captured-var(2)>.0: encountered a dangling reference (use-after-free)
  --> src/main.rs:29:10
   |
29 |     drop(moved);
   |          ^^^^^ Undefined Behavior occurred here
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: stack backtrace:
           0: wrong_pin::<{async block@src/main.rs:34:15: 34:20}, {closure@src/main.rs:46:20: 46:32}>
               at src/main.rs:29:10: 29:15
           1: main
               at src/main.rs:46:5: 50:7

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

cc @RalfJung, @dianne

Meta

Reproducible on the playground with version 1.96.0-nightly (2026-03-09 2d76d9bc76f27b03b489)

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.

Research direction

Start with the reproducer in src/main.rs and run it under Miri, using the reported nightly version and backtrace. Read the pin!, coroutine, gen, and Unpin behavior involved; the issue is resolved when the demonstrated use-after-free is no longer possible and the Miri run no longer reports undefined behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.