rust-lang/rust

Segfault on nightly with feature Pointer Metadata

Opened this issue · 8 comments

Using the Pointer Metadata feature on nightly, the following code will segfault. This is as much as I managed to minimize the issue over the last hours.

#![feature(ptr_metadata)]
use std::ptr::{Thin};
use std::{fmt::Debug};

trait Parent{}
trait Blanket: Parent + Thin{} //mind the + Thin which causes the SegFault

impl<T: Parent> Blanket for T{}

trait WithDebug: Blanket+Debug {}
impl<T: Blanket + Debug> WithDebug for T {}

#[derive(Debug)]
struct ZST;
impl Parent for ZST{}

fn main() {
    let dyn_ref: &dyn WithDebug = &ZST;
    println!("segfault {:?}", dyn_ref);
}

Rust Playground

rustc --version --verbose:

rustc 1.92.0-nightly (6501e64fc 2025-10-23)
binary: rustc
commit-hash: 6501e64fcb02d22b49d6e59d10a7692ec8095619
commit-date: 2025-10-23
host: x86_64-unknown-linux-gnu
release: 1.92.0-nightly
LLVM version: 21.1.3

Slightly minimized (playground):

#![feature(ptr_metadata)]

use std::ptr::Thin;
use std::fmt::Debug;

trait Foo: Debug + Thin {}
impl<T: Debug + Thin> Foo for T{}

fn main() {
    let dyn_ref: &dyn Foo = &42;
    println!("segfault {:?}", dyn_ref);
}

I think the underlying problem here is that Thin/Pointee<Metadata = ()> as a supertrait is considered dyn-compatible, when it probably should not be (because dyn Trait can never implement Thin, so if Trait: Thin, then we have that dyn Trait does not (or at least should not) implement Trait. In fact, in a slight modification of the above program, you can get the compiler to give an "error: the trait bound dyn Foo: Foo is not satisfied": playground).

I think this could be fixed by making Pointee/Thin not dyn-compatible, like Sized.

We do have tests that dyn Pointee<Metadata = ()> doesn't cause a compiler error, but not that it is disallowed (the test compiles and runs successfully, despite producing a &dyn Pointee<Metadata = ()>).

Nice catch!
Cc @lcnr

I think this is related to #[rustc_do_not_implement_via_object] (which Pointee has) , not Pointee/Thin specifically.

Version without Thin/Pointee (playground):

#![feature(rustc_attrs)]

#[rustc_do_not_implement_via_object]
trait NonObjectBase {
    fn non_object_method(&self) { println!("NonObjectBase::non_object_method"); }
}
impl<T> NonObjectBase for T {}

trait ObjectBase {
    fn object_method(&self) { println!("ObjectBase::object_method"); }
}
impl<T> ObjectBase for T {}

trait Sub: NonObjectBase + ObjectBase {
    fn sub_method(&self) { println!("Sub::sub_method"); }
}

impl<T> Sub for T {}

fn main() {
    let x: &dyn Sub = &42;
    x.object_method(); // Segfault
}

In this example: dyn Sub does not implement NonObjectBase, but dyn Sub does implement ObjectBase, however the codegen for calling <dyn Sub as ObjectBase>::object_method is wrong (godbolt):

#[inline(never)]
pub fn bar(x: &'static dyn Sub) {
    x.object_method();
}
example::bar::hef613c72df984030:
        jmp     qword ptr [rsi]

bar calls <dyn Sub as ObjectBase>::object_method incorrectly, it thinks the vtable index is 0 (should be jmp qword ptr [rsi + 8 * index] or something), but index 0 1 and 2 are drop_in_place, size, and align.

Debugging some more, seems like instantiate_and_check_impossible_predicates thinks that the call has impossible predicates, so it treats the call as having vtable index 0, assuming it is impossible to reach.

if tcx.instantiate_and_check_impossible_predicates((
source_principal.def_id,
source_principal.args,
)) {
return 0;
}

Adding a debug print before return 0 shows that this happens for <dyn Sub as ObjectBase>

Interesting interaction here: since codegen is treating <dyn Sub as ObjectBase>::object_method as having vtable index 0, it is actually calling drop_in_place, which is a null function pointer if the erased type has no drop glue (which gives a segfault), but if the erased type does have drop glue, then it will call that drop glue and possibly continue running: playground (NonObjectBase version) and playground (Thin version)

Ah, I misinterpreted this code snippet from first_method_vtable_slot and the comment on it

// We're monomorphizing a call to a dyn trait object that can never be constructed.
if tcx.instantiate_and_check_impossible_predicates((
source_principal.def_id,
source_principal.args,
)) {
return 0;
}

seems like instantiate_and_check_impossible_predicates thinks that the call has impossible predicates, so it treats the call as having vtable index 0, assuming it is impossible to reach

That's not what this code/comment is saying/doing. I think it's saying that if dyn Sub: Sub does not hold, it assumes that dyn Sub cannot be constructed, and thus any vtable access on a dyn Sub is unreachable and the index doesn't matter. But that is not the case for a #[rustc_do_not_implement_via_object] trait, or particularly one with such a trait as a supertrait but other supertraits that can have vtable entries.


Also, yeah, this is #[rustc_do_not_implement_via_object], not Pointee/Thin specifically; it also happens with std::marker::Tuple. (playground)


Here's what I think are a few possible solutions:

  1. Fix first_method_vtable_slot's assumption that dyn Trait: Trait not holding means that dyn Trait vtable accesses don't matter. (also fix supertrait_vtable_slot which makes this assumption).
  2. Make Pointee/Thin/Tuple and probably all #[rustc_do_not_implement_via_object] traits not be dyn-compatible, which I think would make the assumption correct.

Are there any traits that are #[rustc_do_not_implement_via_object] but where dyn Trait should be well-formed? I vaguely recall some discussion of allowing dyn Trait to be well-formed for non-dyn-compatilbe traits, and just have it not implement Trait, which I think would also make first_method_vtable_slot's assumption wrong (depending on how the semantics of that handle supertraits).

Oddly enough, this issue doesn't happen with PointeeSized or Destruct.

Note that PointeeSized is #[rustc_do_not_implement_via_object], and has multiple dyn-compatible stable subtraits.

I tried implementing option 1 (commit) and it ICEd a different test https://github.com/rust-lang/rust/blob/master/tests/ui/traits/trait-upcasting/mono-impossible.rs so 🤷

My understanding is that PointeeSized gets mostly removed after lowering, but I'm not sure why Destruct has different behavior than other #[rustc_do_not_implement_via_object] traits Thin/Pointee/Tuple here.

edit: It also does not happen for DiscriminantKind or Unsize, but does happen for BikeshedGuaranteedNoDrop and TransmuteFrom. I assume the there's probably not much that can be said about #[rustc_do_not_implement_via_object] traits "as a whole", since they are usually magic in other ways than just #[rustc_do_not_implement_via_object].