Skip to content

pin!() is unsound due to coercions #153438

@theemathas

Description

@theemathas

Related to #153425

Edit: See #153438 (comment) for an easier exploit

The following code causes UB. In my testing, it prints a random-looking number.

use std::ops::{Deref, DerefMut};
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};

struct Thing<'a, T> {
    data: Option<T>,
    out: &'a mut Option<T>,
}
impl<T> Deref for Thing<'_, T> {
    type Target = T;
    fn deref(&self) -> &T {
        self.data.as_ref().unwrap()
    }
}
impl<T> DerefMut for Thing<'_, T> {
    fn deref_mut(&mut self) -> &mut T {
        self.data.as_mut().unwrap()
    }
}
impl<T> Drop for Thing<'_, T> {
    fn drop(&mut self) {
        *self.out = self.data.take();
    }
}

fn wrong_pin<T>(data: T, callback: impl FnOnce(Pin<&mut T>)) -> T {
    let mut storage = None::<T>;
    {
        let thing = Thing {
            data: Some(data),
            out: &mut storage,
        };
        let pinned: Pin<&mut T> = pin!(thing);
        callback(pinned);
    }
    storage.unwrap()
}

// The above function violates the Pin guarantee.
// Code below exploits this to actually cause UB.

struct PendingOnce(bool);
impl Future for PendingOnce {
    type Output = ();
    fn poll(mut self: Pin<&mut Self>, _: &mut Context) -> Poll<()> {
        if self.0 {
            Poll::Ready(())
        } else {
            self.0 = true;
            Poll::Pending
        }
    }
}

fn main() {
    let mut ctx = Context::from_waker(Waker::noop());
    let future = async {
        let x = Box::new(1);
        let y = &x;
        PendingOnce(false).await;
        println!("{y}");
    };
    let future = wrong_pin(future, |pinned_future| {
        let _ = pinned_future.poll(&mut ctx);
    });
    let _ = pin!(future).poll(&mut ctx);
}

The issue is that the pin!(thing) macro call moves the entire Thing into an inaccessible variable. Then, instead of pinning Thing, it deref-coerces the &mut Thing<T> into a &mut T and pins that instead. Then, the Drop impl can later access the &mut T, breaking the Pin invariant.

This presumably regressed in #139114, which changed pin!() to use super let. This uses a block expression in the syntax, which then presumably runs into the bug in #23014, causing the strange coercion behavior.

The code compiled since 1.88.0. It didn't compile in 1.87.0.

Meta

Reproducible on the playground with version 1.96.0-nightly (2026-03-04 b90dc1e597db0bbc0cab)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-coercionsArea: implicit and explicit `expr as Type` coercionsA-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)A-pinArea: PinC-bugCategory: This is a bug.F-super_letit's super, let's go!I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamT-libsRelevant to the library team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions