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