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);
}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 = ()>).
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.
rust/compiler/rustc_trait_selection/src/traits/vtable.rs
Lines 328 to 334 in 79966ae
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
rust/compiler/rustc_trait_selection/src/traits/vtable.rs
Lines 326 to 334 in 79966ae
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:
- Fix
first_method_vtable_slot's assumption thatdyn Trait: Traitnot holding means thatdyn Traitvtable accesses don't matter. (also fixsupertrait_vtable_slotwhich makes this assumption). - Make
Pointee/Thin/Tupleand 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] traitsThin/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].