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