TypeId
exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness. · Issue #97156 · rust-lang/rust (original) (raw)
EDIT by @BoxyUwU
playground
type One = for<'a> fn(&'a (), &'a ()); type Two = for<'a, 'b> fn(&'a (), &'b ());
mod my_api { use std::any::Any; use std:📑:PhantomData;
pub struct Foo<T: 'static> {
a: &'static dyn Any,
_p: PhantomData<*mut T>, // invariant, the type of the `dyn Any`
}
impl<T: 'static> Foo<T> {
pub fn deref(&self) -> &'static T {
match self.a.downcast_ref::<T>() {
None => unsafe { std::hint::unreachable_unchecked() },
Some(a) => a,
}
}
pub fn new(a: T) -> Foo<T> {
Foo::<T> {
a: Box::leak(Box::new(a)),
_p: PhantomData,
}
}
}
}
use my_api::*;
fn main() { let foo = Foo::::new((|_, _| ()) as One); foo.deref(); let foo: Foo = foo; foo.deref(); }
has UB from hitting the unreachable_unchecked
because TypeId::of::<One>()
is not the same as TypeId::of::<Two>()
despite them being considered the same types by the type checker. Originally this was thought to be a nightly-only issue with feature(generic_const_exprs)
but actually the weird behaviour of TypeId
can be seen on stable and result in crashes or UB in unsafe code.
original description follows below:
#![feature(const_type_id, generic_const_exprs)]
use std::any::TypeId;
// One
and Two
are currently considered equal types, as both
// One <: Two
and One :> Two
holds.
type One = for<'a> fn(&'a (), &'a ());
type Two = for<'a, 'b> fn(&'a (), &'b ());
trait AssocCt {
const ASSOC: usize;
}
const fn to_usize<T: 'static>() -> usize {
const WHAT_A_TYPE: TypeId = TypeId::of::();
match TypeId::of::() {
WHAT_A_TYPE => 0,
_ => 1000,
}
}
impl<T: 'static> AssocCt for T {
const ASSOC: usize = to_usize::();
}
trait WithAssoc { type Assoc; } impl<T: 'static> WithAssoc<()> for T where [(); ::ASSOC]: { type Assoc = [u8; ::ASSOC]; }
fn generic<T: 'static, U>(x: <T as WithAssoc>::Assoc) -> <T as WithAssoc>::Assoc where [(); ::ASSOC]:, T: WithAssoc, { x }
fn unsound(x: <One as WithAssoc>::Assoc) -> <Two as WithAssoc>::Assoc where One: WithAssoc, { let x: <Two as WithAssoc>::Assoc = generic::<One, T>(x); x }
fn main() { println!("{:?}", unsound::<()>([])); }
TypeId
being different for types which are considered equal types allows us to take change the value of a projection by switching between the equal types in its substs and observing that change by looking at their TypeId
. This is possible as switching between equal types is allowed even in invariant positions.
This means that stabilizing const TypeId::of
and allowing constants to flow into the type system, e.g. some minimal version of feature(generic_const_exprs)
, will be currently unsound.
I have no idea on how to fix this. I don't expect that we're able to convert higher ranked types to some canonical representation. Ah well, cc @rust-lang/project-const-generics @nikomatsakis