Skip to content

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

Open

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

Activity

  1. added
    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    C-bugCategory: This is a bug.
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    F-derive_coerce_pointeeFeature: RFC 3621's oft-renamed implementation
    on Nov 13, 2025
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 13, 2025
  3. added
    T-langRelevant to the language team
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    T-libsRelevant to the library team, which will review and decide on the PR/issue.
    on Nov 13, 2025
  4. Darksonn commented on Nov 13, 2025

    @Darksonn
    Member

    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.

  5. theemathas commented on Nov 13, 2025

    @theemathas
    ContributorAuthor

    Actually.... exploiting this can also be done by implementing CoerceUnsized manually, without using macros. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=84dd0ca73852dc7d90c85c8fb31fa7e3

    Not sure why CoerceUnsized is a safe trait.

  6. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Nov 13, 2025
  7. Darksonn commented on Nov 13, 2025

    @Darksonn
    Member

    Oh ... that one could be fixed by preventing downstream CoerceUnsized impls similar to #145608.

  8. Darksonn commented on Nov 13, 2025

    @Darksonn
    Member

    Normally Pin only implements CoerceUnsized if PinCoerceUnsized is implemented. The problem here is that by providing a new implementation of CoerceUnsized for Pin, you did something that requires the safety requirements of PinCoerceUnsized without actually implementing it.

  9. Darksonn commented on Nov 13, 2025

    @Darksonn
    Member

    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.

  10. added
    I-lang-radarItems that are on lang's radar and will need eventual work or consideration.
    on Nov 14, 2025
  11. traviscross commented on Nov 14, 2025

    @traviscross
    Contributor
  12. theemathas commented on Nov 15, 2025

    @theemathas
    ContributorAuthor

    See also #148727, where implementing a trait on references could bypass a check required for soundness.

  13. removed
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    on Dec 13, 2025
  14. added
    requires-nightlyThis issue requires a nightly compiler in some way. When possible, use a F-* label instead.
    on Feb 26, 2026
  15. VorpalBlade commented on Sep 19, 2026

    @VorpalBlade

    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?

  16. theemathas commented on Sep 19, 2026

    @theemathas
    ContributorAuthor

    #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

  17. Darksonn commented on Sep 19, 2026

    @Darksonn
    Member

    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 for derive(CoercePointee) in general, we are just waiting for arbitrary self types which @dingxiangfei2009 tells me is making progress as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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. When possible, use a F-* label instead.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions