Repository navigation
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
Activity
Darksonn commentedon Nov 13, 2025
Many third-party crate macros such as zerocopy::FromBytes are probably also broken. This doesn't really sound like a bug in
CoercePointee. I guess we will await discussion on #148423.theemathas commentedon Nov 13, 2025
Actually.... exploiting this can also be done by implementing
CoerceUnsizedmanually, without using macros. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=84dd0ca73852dc7d90c85c8fb31fa7e3Not sure why
CoerceUnsizedis a safe trait.Darksonn commentedon Nov 13, 2025
Oh ... that one could be fixed by preventing downstream
CoerceUnsizedimpls similar to #145608.Darksonn commentedon Nov 13, 2025
Normally
Pinonly implementsCoerceUnsizedifPinCoerceUnsizedis implemented. The problem here is that by providing a new implementation ofCoerceUnsizedforPin, you did something that requires the safety requirements ofPinCoerceUnsizedwithout actually implementing it.Darksonn commentedon Nov 13, 2025
There is a pretty big difference between this macro and #148423. The partialeq derive macro emits a wrong unsafe block, but corecepointee does not emit any unsafe code at all. The problem is that it emits something using an unstable feature that is apparently unsound.
traviscross commentedon Nov 14, 2025
cc @dingxiangfei2009
theemathas commentedon Nov 15, 2025
See also #148727, where implementing a trait on references could bypass a check required for soundness.
PinCoerceUnsizedcan be bypassed usingCoerceUnsizedimpl directly on the fundamentalPin#160756 as a duplicate of this issueVorpalBlade commentedon Sep 19, 2026
Is there any way forward for this (or some other way to allow unsizing for user defined smart pointer types)? I would be okay with an unsafe way to do it, but not having anything at all hurts.
And it doesn't look like there has been any progress on this. Does the Linux kernel (which I understand was one of the driving motivators for this) no longer need the feature?
theemathas commentedon Sep 19, 2026
#160391, if implemented properly, will probably fix this issue.
The problem with macros in general though is unsolved, and nobody knows how to solve it. See #148423
Darksonn commentedon Sep 19, 2026
The Linux kernel still needs this. It would let us remove the last remaining
#![feature(_)]annotations in kernel code. And progress is being made. For this particular bug, there is the PR mentioned by @theemathas that will close this. As forderive(CoercePointee)in general, we are just waiting for arbitrary self types which @dingxiangfei2009 tells me is making progress as well.