-
Notifications
You must be signed in to change notification settings - Fork 14k
Description
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