Skip to content

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

@theemathas

Description

@theemathas

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

This issue uses a similar mechanism to #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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)A-pinArea: PinA-proc-macrosArea: Procedural macrosC-bugCategory: This is a bug.F-derive_coerce_pointeeFeature: RFC 3621's oft-renamed implementationI-lang-radarItems that are on lang's radar and will need eventual work or consideration.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-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.requires-nightlyThis issue requires a nightly compiler in some way.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions