Skip to content

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

@theemathas

Description

@theemathas

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)A-pinArea: PinC-bugCategory: This is a bug.F-coroutines`#![feature(coroutines)]`F-gen_blocks`gen {}` expressions that produce `Iterator`sI-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-langRelevant to the language teamT-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.T-opsemRelevant to the opsem team

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions