rust-lang / rust-lang/rust

`derive(CoercePointee)` is unsound with user-defined attribute macros.

Open
#148,899 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-macros A-pin A-proc-macros C-bug F-derive_coerce_pointee I-lang-radar I-unsound requires-nightly T-compiler T-lang T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

This issue has the same cause as #148423, but applied to a different derive macro.

This issue uses a similar mechanism to https://github.com/rust-lang/rust/issues/134407 to cause unsoundness, but this issue uses unsize-coercion instead of higher ranked function pointer subtyping.

The following code causes a use-after-free, crashing in my testing. (The explanation is below the code.)

(Here's the equivalent code on the playground, using the macro_attr feature to define an attribute macro without needing to use a separate crate.)

dep/src/lib.rs:

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn discard(_: TokenStream, _: TokenStream) -> TokenStream {
    TokenStream::new()
}

src/main.rs:

#![feature(derive_coerce_pointee)]

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

use dep::discard;

#[derive(CoercePointee)]
#[discard]
#[repr(transparent)]
struct Thing<T: ?Sized>(Box<T>);

#[derive(CoercePointee)]
#[repr(transparent)]
struct Wrap<T: ?Sized>(Box<T>);

type Thing<T> = Pin<Wrap<T>>;

trait IsType<T> {
    fn as_mut_type(&mut self) -> &mut T;
    fn into_type(self: Box<Self>) -> T;
}
impl<T> IsType<T> for T {
    fn as_mut_type(&mut self) -> &mut T {
        self
    }
    fn into_type(self: Box<Self>) -> T {
        *self
    }
}
trait SubIsType<T>: IsType<T> {}
impl<T> SubIsType<T> for T {}

impl<T: Sized> Deref for Wrap<T> {
    type Target = u8;
    fn deref(&self) -> &u8 {
        unreachable!()
    }
}
impl<T: Sized> Deref for Wrap<dyn IsType<T> + '_> {
    type Target = u8;
    fn deref(&self) -> &u8 {
        unreachable!()
    }
}
impl<T> Deref for Wrap<dyn SubIsType<T> + '_> {
    type Target = T;
    fn deref(&self) -> &T {
        unreachable!()
    }
}
impl<T> DerefMut for Wrap<dyn SubIsType<T> + '_> {
    fn deref_mut(&mut self) -> &mut T {
        (*self.0).as_mut_type()
    }
}

fn wrong_pin<T>(value: T, callback: impl FnOnce(Pin<&mut T>)) -> T {
    let pin_sized: Pin<Wrap<T>> = Pin::new(Wrap(Box::new(value)));
    let mut pin_sub: Pin<Wrap<dyn SubIsType<T>>> = pin_sized;
    let pin_direct: Pin<&mut T> = pin_sub.as_mut();
    callback(pin_direct);
    let pin_super: Pin<Wrap<dyn IsType<T>>> = pin_sub;
    Pin::into_inner(pin_super).0.into_type()
}

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

fn main() {
    let future = async {
        let x = String::from("abc");
        let y = &x;
        Delay(false).await;
        println!("{y}");
    };
    let mut cx = Context::from_waker(Waker::noop());
    let future = wrong_pin(future, |pinned| {
        let _ = pinned.poll(&mut cx);
    });
    let _ = pin!(future).poll(&mut cx);
}

The derive(CoercePointee) macro tries to implement the Unsize trait on a struct named Thing. Due to the #[discard] macro, the Unsize trait instead gets implemented on the type alias type Thing<T> = Pin<Wrap<T>>; (which doesn't violate orphan rules, since Pin is a fundamental type). As a result, Pin<Wrap<T>> can unsize-coerce the T type, as though Wrap<T> implemented the PinCoerceUnsized trait.

I implement Deref on Wrap<impl Sized> and Wrap<dyn IsType<T>> so that no pin guarantee exists on those types (since they deref to u8, which is Unpin). However, I implement Wrap<dyn SubIsType<T>> so that there is actually a pin guarantee.

In the wrong_pin function, I create a Wrap<impl Sized> (without any pin guarantees). I unsize-coerce it to Wrap<dyn SubIsType<T>> (which does have a pin guarantee), and then trait-upcast it to Wrap<dyn IsType<T>> (without any pin guarantees). I then extract the T out and return it. During the time where the pin guarantee exists, I call a callback which makes use of the pin guarantee. This pin guarantee is, of course, violated when the T value is extracted.

The main function then uses this violation of Pin guarantees to cause UB.

Meta

rustc --version --verbose:

rustc 1.93.0-nightly (01867557c 2025-11-12)
binary: rustc
commit-hash: 01867557cd7dbe256a031a7b8e28d05daecd75ab
commit-date: 2025-11-12
host: aarch64-apple-darwin
release: 1.93.0-nightly
LLVM version: 21.1.5

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 by reproducing the unsoundness with the provided proc-macro and derive(CoercePointee) example, then inspect the implementation of the derive macro and how user-defined attribute macros affect its target. Determine how to prevent an Unsize implementation from being attached to the altered type, and add a regression test covering the resulting pin-guarantee violation.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.