Auto merge of #135318 - compiler-errors:vtable-fixes, r=lcnr
Fix deduplication mismatches in vtables leading to upcasting unsoundness We currently have two cases where subtleties in supertraits can trigger disagreements in the vtable layout, e.g. leading to a different vtable layout being accessed at a callsite compared to what was prepared during unsizing. Namely: ### #135315 In this example, we were not normalizing supertraits when preparing vtables. In the example, ``` trait Supertrait<T> { fn _print_numbers(&self, mem: &[usize; 100]) { println!("{mem:?}"); } } impl<T> Supertrait<T> for () {} trait Identity { type Selff; } impl<Selff> Identity for Selff { type Selff = Selff; } trait Middle<T>: Supertrait<()> + Supertrait<T> { fn say_hello(&self, _: &usize) { println!("Hello!"); } } impl<T> Middle<T> for () {} trait Trait: Middle<<() as Identity>::Selff> {} impl Trait for () {} fn main() { (&() as &dyn Trait as &dyn Middle<()>).say_hello(&0); } ``` When we prepare `dyn Trait`, we see a supertrait of `Middle<<() as Identity>::Selff>`, which itself has two supertraits `Supertrait<()>` and `Supertrait<<() as Identity>::Selff>`. These two supertraits are identical, but they are not duplicated because we were using structural equality and *not* considering normalization. This leads to a vtable layout with two trait pointers. When we upcast to `dyn Middle<()>`, those two supertraits are now the same, leading to a vtable layout with only one trait pointer. This leads to an offset error, and we call the wrong method. ### #135316 This one is a bit more interesting, and is the bulk of the changes in this PR. It's a bit similar, except it uses binder equality instead of normalization to make the compiler get confused about two vtable layouts. In the example, ``` trait Supertrait<T> { fn _print_numbers(&self, mem: &[usize; 100]) { println!("{mem:?}"); } } impl<T> Supertrait<T> for () {} trait Trait<T, U>: Supertrait<T> + Supertrait<U> { fn say_hello(&self, _: &usize) { println!("Hello!"); } } impl<T, U> Trait<T, U> for () {} fn main() { (&() as &'static dyn for<'a> Trait<&'static (), &'a ()> as &'static dyn Trait<&'static (), &'static ()>) .say_hello(&0); } ``` When we prepare the vtable for `dyn for<'a> Trait<&'static (), &'a ()>`, we currently consider the PolyTraitRef of the vtable as the key for a supertrait. This leads two two supertraits -- `Supertrait<&'static ()>` and `for<'a> Supertrait<&'a ()>`. However, we can upcast[^up] without offsetting the vtable from `dyn for<'a> Trait<&'static (), &'a ()>` to `dyn Trait<&'static (), &'static ()>`. This is just instantiating the principal trait ref for a specific `'a = 'static`. However, when considering those supertraits, we now have only one distinct supertrait -- `Supertrait<&'static ()>` (which is deduplicated since there are two supertraits with the same substitutions). This leads to similar offsetting issues, leading to the wrong method being called. [^up]: I say upcast but this is a cast that is allowed on stable, since it's not changing the vtable at all, just instantiating the binder of the principal trait ref for some lifetime. The solution here is to recognize that a vtable isn't really meaningfully higher ranked, and to just treat a vtable as corresponding to a `TraitRef` so we can do this deduplication more faithfully. That is to say, the vtable for `dyn for<'a> Tr<'a>` and `dyn Tr<'x>` are always identical, since they both would correspond to a set of free regions on an impl... Do note that `Tr<for<'a> fn(&'a ())>` and `Tr<fn(&'static ())>` are still distinct. ---- There's a bit more that can be cleaned up. In codegen, we can stop using `PolyExistentialTraitRef` basically everywhere. We can also fix SMIR to stop storing `PolyExistentialTraitRef` in its vtable allocations. As for testing, it's difficult to actually turn this into something that can be tested with `rustc_dump_vtable`, since having multiple supertraits that are identical is a recipe for ambiguity errors. Maybe someone else is more creative with getting that attr to work, since the tests I added being run-pass tests is a bit unsatisfying. Miri also doesn't help here, since it doesn't really generate vtables that are offset by an index in the same way as codegen. r? `@lcnr` for the vibe check? Or reassign, idk. Maybe let's talk about whether this makes sense. <sup>(I guess an alternative would also be to not do any deduplication of vtable supertraits (or only a really conservative subset) rather than trying to normalize and deduplicate more faithfully here. Not sure if that works and is sufficient tho.)</sup> cc `@steffahn` -- ty for the minimizations cc `@WaffleLapkin` -- since you're overseeing the feature stabilization :3 Fixes #135315 Fixes #135316
This commit is contained in:
commit
c37fbd873a
63 changed files with 998 additions and 821 deletions
|
@ -5,6 +5,7 @@ use std::borrow::Cow;
|
|||
|
||||
use either::{Left, Right};
|
||||
use rustc_abi::{self as abi, ExternAbi, FieldIdx, Integer, VariantIdx};
|
||||
use rustc_hir::def_id::DefId;
|
||||
use rustc_middle::ty::layout::{FnAbiOf, IntegerExt, LayoutOf, TyAndLayout};
|
||||
use rustc_middle::ty::{self, AdtDef, Instance, Ty, VariantDef};
|
||||
use rustc_middle::{bug, mir, span_bug};
|
||||
|
@ -693,25 +694,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
|
|||
trace!("Virtual call dispatches to {fn_inst:#?}");
|
||||
// We can also do the lookup based on `def_id` and `dyn_ty`, and check that that
|
||||
// produces the same result.
|
||||
if cfg!(debug_assertions) {
|
||||
let tcx = *self.tcx;
|
||||
|
||||
let trait_def_id = tcx.trait_of_item(def_id).unwrap();
|
||||
let virtual_trait_ref =
|
||||
ty::TraitRef::from_method(tcx, trait_def_id, instance.args);
|
||||
let existential_trait_ref =
|
||||
ty::ExistentialTraitRef::erase_self_ty(tcx, virtual_trait_ref);
|
||||
let concrete_trait_ref = existential_trait_ref.with_self_ty(tcx, dyn_ty);
|
||||
|
||||
let concrete_method = Instance::expect_resolve_for_vtable(
|
||||
tcx,
|
||||
self.typing_env,
|
||||
def_id,
|
||||
instance.args.rebase_onto(tcx, trait_def_id, concrete_trait_ref.args),
|
||||
self.cur_span(),
|
||||
);
|
||||
assert_eq!(fn_inst, concrete_method);
|
||||
}
|
||||
self.assert_virtual_instance_matches_concrete(dyn_ty, def_id, instance, fn_inst);
|
||||
|
||||
// Adjust receiver argument. Layout can be any (thin) ptr.
|
||||
let receiver_ty = Ty::new_mut_ptr(self.tcx.tcx, dyn_ty);
|
||||
|
@ -744,6 +727,30 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
|
|||
}
|
||||
}
|
||||
|
||||
fn assert_virtual_instance_matches_concrete(
|
||||
&self,
|
||||
dyn_ty: Ty<'tcx>,
|
||||
def_id: DefId,
|
||||
virtual_instance: ty::Instance<'tcx>,
|
||||
concrete_instance: ty::Instance<'tcx>,
|
||||
) {
|
||||
let tcx = *self.tcx;
|
||||
|
||||
let trait_def_id = tcx.trait_of_item(def_id).unwrap();
|
||||
let virtual_trait_ref = ty::TraitRef::from_method(tcx, trait_def_id, virtual_instance.args);
|
||||
let existential_trait_ref = ty::ExistentialTraitRef::erase_self_ty(tcx, virtual_trait_ref);
|
||||
let concrete_trait_ref = existential_trait_ref.with_self_ty(tcx, dyn_ty);
|
||||
|
||||
let concrete_method = Instance::expect_resolve_for_vtable(
|
||||
tcx,
|
||||
self.typing_env,
|
||||
def_id,
|
||||
virtual_instance.args.rebase_onto(tcx, trait_def_id, concrete_trait_ref.args),
|
||||
self.cur_span(),
|
||||
);
|
||||
assert_eq!(concrete_instance, concrete_method);
|
||||
}
|
||||
|
||||
/// Initiate a tail call to this function -- popping the current stack frame, pushing the new
|
||||
/// stack frame and initializing the arguments.
|
||||
pub(super) fn init_fn_tail_call(
|
||||
|
|
|
@ -414,36 +414,33 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
|
|||
|
||||
// Sanity-check that `supertrait_vtable_slot` in this type's vtable indeed produces
|
||||
// our destination trait.
|
||||
if cfg!(debug_assertions) {
|
||||
let vptr_entry_idx =
|
||||
self.tcx.supertrait_vtable_slot((src_pointee_ty, dest_pointee_ty));
|
||||
let vtable_entries = self.vtable_entries(data_a.principal(), ty);
|
||||
if let Some(entry_idx) = vptr_entry_idx {
|
||||
let Some(&ty::VtblEntry::TraitVPtr(upcast_trait_ref)) =
|
||||
vtable_entries.get(entry_idx)
|
||||
else {
|
||||
span_bug!(
|
||||
self.cur_span(),
|
||||
"invalid vtable entry index in {} -> {} upcast",
|
||||
src_pointee_ty,
|
||||
dest_pointee_ty
|
||||
);
|
||||
};
|
||||
let erased_trait_ref = upcast_trait_ref
|
||||
.map_bound(|r| ty::ExistentialTraitRef::erase_self_ty(*self.tcx, r));
|
||||
assert!(
|
||||
data_b
|
||||
.principal()
|
||||
.is_some_and(|b| self.eq_in_param_env(erased_trait_ref, b))
|
||||
let vptr_entry_idx =
|
||||
self.tcx.supertrait_vtable_slot((src_pointee_ty, dest_pointee_ty));
|
||||
let vtable_entries = self.vtable_entries(data_a.principal(), ty);
|
||||
if let Some(entry_idx) = vptr_entry_idx {
|
||||
let Some(&ty::VtblEntry::TraitVPtr(upcast_trait_ref)) =
|
||||
vtable_entries.get(entry_idx)
|
||||
else {
|
||||
span_bug!(
|
||||
self.cur_span(),
|
||||
"invalid vtable entry index in {} -> {} upcast",
|
||||
src_pointee_ty,
|
||||
dest_pointee_ty
|
||||
);
|
||||
} else {
|
||||
// In this case codegen would keep using the old vtable. We don't want to do
|
||||
// that as it has the wrong trait. The reason codegen can do this is that
|
||||
// one vtable is a prefix of the other, so we double-check that.
|
||||
let vtable_entries_b = self.vtable_entries(data_b.principal(), ty);
|
||||
assert!(&vtable_entries[..vtable_entries_b.len()] == vtable_entries_b);
|
||||
};
|
||||
}
|
||||
let erased_trait_ref =
|
||||
ty::ExistentialTraitRef::erase_self_ty(*self.tcx, upcast_trait_ref);
|
||||
assert!(data_b.principal().is_some_and(|b| self.eq_in_param_env(
|
||||
erased_trait_ref,
|
||||
self.tcx.instantiate_bound_regions_with_erased(b)
|
||||
)));
|
||||
} else {
|
||||
// In this case codegen would keep using the old vtable. We don't want to do
|
||||
// that as it has the wrong trait. The reason codegen can do this is that
|
||||
// one vtable is a prefix of the other, so we double-check that.
|
||||
let vtable_entries_b = self.vtable_entries(data_b.principal(), ty);
|
||||
assert!(&vtable_entries[..vtable_entries_b.len()] == vtable_entries_b);
|
||||
};
|
||||
|
||||
// Get the destination trait vtable and return that.
|
||||
let new_vptr = self.get_vtable_ptr(ty, data_b)?;
|
||||
|
|
|
@ -54,7 +54,8 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
|
|||
) -> &'tcx [VtblEntry<'tcx>] {
|
||||
if let Some(trait_) = trait_ {
|
||||
let trait_ref = trait_.with_self_ty(*self.tcx, dyn_ty);
|
||||
let trait_ref = self.tcx.erase_regions(trait_ref);
|
||||
let trait_ref =
|
||||
self.tcx.erase_regions(self.tcx.instantiate_bound_regions_with_erased(trait_ref));
|
||||
self.tcx.vtable_entries(trait_ref)
|
||||
} else {
|
||||
TyCtxt::COMMON_VTABLE_ENTRIES
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue