-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TypeId
exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness.
#97156
Comments
Ok, I have an idea on how to convert higher ranked types to some canonical representation. I just don't think that idea is very good. I also don't know how I would extend this to implication types in the future |
To clarify, is
the issue here, and the rest is just exploiting this to demonstrate the unsoundness? (At least this seems to break the guarantees I understand If this holds, then the resolution is to ether give Giving cc also @eddyb #95845, as using the v0 mangling for TypeId would erase the lifetimes from higher ranked types AIUI. The current impl of #95845 still uses the current hash, so wouldn't be problematic, but this is something to keep in mind w.r.t. the information contained in TypeId vs the v0 type mangling. (The issue OP ccs itself; this is likely a typo?) |
There is a lot of confusion here wrt all the pieces involved. Both legacy and v0 mangling, and both the You couldn't not mangle them because otherwise That all uses "binder anonymization" so the fully normalized types are:
The issue here is this broken fragment of type unification code: rust/compiler/rustc_infer/src/infer/equate.rs Lines 154 to 159 in cd282d7
This treats Too much of Rust's typesystem depends on I don't think there's an "obvious choice" for which of Even if we normalized one of |
TypeId
in the type system is unsoundTypeId
exposes equality-by-subtyping vs normal-form-judgemental-equality unsoundness.
Retitled the issue to reflect the situation as I understand it ( |
In which sense are these types "equal"? Two allows references with different lifetimes, One does not, so those look like different types to me. Going off the issue title, are you saying they each are a subtype of the other? That is different from saying they are "equal" though. Might be good to update the issue description because this confused the heck out of me until I read #97156 (comment).
You are Cc'ing an issue in itself? probably a typo, not sure which issue you meant though. |
Well... from a user perspective any function that will be allowed as a function pointer of type So they are structurally different, but behaviour wise they are the same. If we had sufficiently smart normalization we could normalize |
@RalfJung FWIW I changed the issue title, it was something else before.
Specifically unification in the @oli-obk The normalization wouldn't need to be perfectly clever. You would just perform it before checking for equality (without involving subtyping), just like we do with anonymizing the lifetimes bound by the That is, we have to remove the unsound equality-via-subtyping logic and try to cover any breakage with a subset of the possible higher-ranked normalization (e.g. if a lifetime only appears in the outermost EDIT: I regret using the word "judgmental" (or its misspelling, apparently), because I just remembered that type equality judgments can definitely involve subtyping, that distinction is more about "being built-in" vs reified proofs of type equality (or "paths" in HoTT). |
TypeId
exposes equality-by-subtyping vs normal-form-judgemental-equality unsoundness.TypeId
exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness.
A few thoughts: How I understand the bugSome parts of rustc do indeed assume that For the compiler itself, lacking a canonical type The
Whether or not the "nondeterministic type-id" is a problem depends on how you choose to draw the lines:
Ways to fix the bugBased on the above analysis, it seems like we can ...
I'm not really aware of other solutions, I'm curious if folks have any ideas there. |
Having two notions of equality (one more fine-grained than the other) is certainly possible, but needs some care to avoid accidentally using the wrong one in the wrong spot. I am not enough of a subtyping expert to know if there are any common pitfalls with this approach. It would of course be nicer, conceptually, to make subtyping antisymmetric, i.e., "a <= b /\ b <= a -> a == b".
Some algebraic thoughts: whatever equality on types ends up being, the property we need is that all types in an equivalence class have the same type ID. The easiest way to achieve this is to pick a unique representative for each equivalence class, have a normalization function that returns said representative, and use its type ID. However, in principle it is conceivable that one could generate a suitable type ID directly, without having such a normalization function. I don't know if that is actually any easier in this case, but it might be worth thinking about. |
Random thought: For the specific case of |
Afaik we're currently moving towards removing also, |
What's the reasoning behind this change? Any discussion I can read up on? For context, from what I understand, |
Well I don't your idea proposed in the linked internals thread is possible:
I don't think this works, because not only does the same impl has to apply for coherence, but this must also recursively hold for all associated types. If we again look at |
Not sure, you can try asking about it in the #t-types stream on zulip 😅 i think that's mostly been mentioned as a sidenote during other discussions. |
I'm not sure I understand what you are saying here? |
I don't think this restriction can be enforced with the best example being Does that make sense to you? I don't have a clear image of why you need that invariant, so I might not make much sense here |
The "one Let's say you have a trait fn takes_a_u(_: U) {}
fn takes_a_t(t: T) {
// `t: T` coerced to supertype `U`
takes_a_u(t);
} However, the following doesn't work today: fn takes_impl_trait(_: impl Trait) {}
fn takes_a_t(t: T) {
// Compile error!
// `t: T` not coerced to supertype `U`
takes_impl_trait(t);
} To make pattern types ergonomic, the second example most likely needs to compile. The issue is that there might be more than one possible
The "one Does that make sense? |
…egions-on-iat, r=<try> Do not erase late bound regions when selecting inherent associated types In the fix for rust-lang#97156 we would want the following code: ```rust #![feature(inherent_associated_types)] #![allow(incomplete_features)] struct Foo<T>(T); impl Foo<fn(&'static ())> { type Assoc = u32; } trait Other {} impl Other for u32 {} // FIXME(inherent_associated_types): Avoid emitting two diagnostics (they only differ in span). // FIXME(inherent_associated_types): Enhancement: Spruce up the diagnostic by saying something like // "implementation is not general enough" as is done for traits via // `try_report_trait_placeholder_mismatch`. fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} //~^ ERROR mismatched types //~| ERROR mismatched types fn main() {} ``` to fail with ... ``` error[E0220]: associated type `Assoc` not found for `Foo<for<'a> fn(&'a ())>` in the current scope --> tests/ui/associated-inherent-types/issue-109789.rs:18:36 | 4 | struct Foo<T>(T); | ------------- associated item `Assoc` not found for this struct ... 18 | fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} | ^^^^^ associated item not found in `Foo<for<'a> fn(&'a ())>` | = note: the associated type was found for - `Foo<fn(&'static ())>` error: aborting due to previous error For more information about this error, try `rustc --explain E0220`. ``` This PR fixes the ICE we are currently getting "was a subtype of Foo<Binder(fn(&ReStatic ()), [])> during selection but now it is not" Also fixes rust-lang#112631 r? `@lcnr`
…egions-on-iat, r=<try> Do not erase late bound regions when selecting inherent associated types In the fix for rust-lang#97156 we would want the following code: ```rust #![feature(inherent_associated_types)] #![allow(incomplete_features)] struct Foo<T>(T); impl Foo<fn(&'static ())> { type Assoc = u32; } trait Other {} impl Other for u32 {} // FIXME(inherent_associated_types): Avoid emitting two diagnostics (they only differ in span). // FIXME(inherent_associated_types): Enhancement: Spruce up the diagnostic by saying something like // "implementation is not general enough" as is done for traits via // `try_report_trait_placeholder_mismatch`. fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} //~^ ERROR mismatched types //~| ERROR mismatched types fn main() {} ``` to fail with ... ``` error[E0220]: associated type `Assoc` not found for `Foo<for<'a> fn(&'a ())>` in the current scope --> tests/ui/associated-inherent-types/issue-109789.rs:18:36 | 4 | struct Foo<T>(T); | ------------- associated item `Assoc` not found for this struct ... 18 | fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} | ^^^^^ associated item not found in `Foo<for<'a> fn(&'a ())>` | = note: the associated type was found for - `Foo<fn(&'static ())>` error: aborting due to previous error For more information about this error, try `rustc --explain E0220`. ``` This PR fixes the ICE we are currently getting "was a subtype of Foo<Binder(fn(&ReStatic ()), [])> during selection but now it is not" Also fixes rust-lang#112631 r? `@lcnr`
…r=<try> Fix for TypeId exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness Fixes rust-lang#97156 This PR revives rust-lang#97427 idea, it sits on top of rust-lang#118118 because the idea uncovered some problems with IATs. r? `@lcnr` This is ICEing yet for `tests/ui/traits/new-solver/escaping-bound-vars-in-writeback-normalization.rs` using the new trait solver. After rust-lang#118118 and this ICE is fixed, we would need a rebase and a crater run. Opening as a WIP for now.
…egions-on-iat, r=compiler-errors Do not erase late bound regions when selecting inherent associated types In the fix for rust-lang#97156 we would want the following code: ```rust #![feature(inherent_associated_types)] #![allow(incomplete_features)] struct Foo<T>(T); impl Foo<fn(&'static ())> { type Assoc = u32; } trait Other {} impl Other for u32 {} // FIXME(inherent_associated_types): Avoid emitting two diagnostics (they only differ in span). // FIXME(inherent_associated_types): Enhancement: Spruce up the diagnostic by saying something like // "implementation is not general enough" as is done for traits via // `try_report_trait_placeholder_mismatch`. fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} //~^ ERROR mismatched types //~| ERROR mismatched types fn main() {} ``` to fail with ... ``` error[E0220]: associated type `Assoc` not found for `Foo<for<'a> fn(&'a ())>` in the current scope --> tests/ui/associated-inherent-types/issue-109789.rs:18:36 | 4 | struct Foo<T>(T); | ------------- associated item `Assoc` not found for this struct ... 18 | fn bar(_: Foo<for<'a> fn(&'a ())>::Assoc) {} | ^^^^^ associated item not found in `Foo<for<'a> fn(&'a ())>` | = note: the associated type was found for - `Foo<fn(&'static ())>` error: aborting due to previous error For more information about this error, try `rustc --explain E0220`. ``` This PR fixes the ICE we are currently getting "was a subtype of Foo<Binder(fn(&ReStatic ()), [])> during selection but now it is not" Also fixes rust-lang#112631 r? `@lcnr`
…r=<try> Fix for TypeId exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness Fixes rust-lang#97156 This PR revives rust-lang#97427 idea, it sits on top of rust-lang#118118 because the idea uncovered some problems with IATs. r? `@lcnr` This is ICEing yet for `tests/ui/traits/new-solver/escaping-bound-vars-in-writeback-normalization.rs` using the new trait solver. After rust-lang#118118 and this ICE is fixed, we would need a rebase and a crater run. Opening as a WIP for now.
…r=<try> Fix for TypeId exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness Fixes rust-lang#97156 This PR revives rust-lang#97427 idea. r? `@lcnr`
…r=<try> Fix for TypeId exposes equality-by-subtyping vs normal-form-syntactic-equality unsoundness Fixes rust-lang#97156 This PR revives rust-lang#97427 idea. r? `@lcnr`
…lnay Stabilize `type_name_of_val` Make the following API stable: ```rust // in core::any pub fn type_name_of_val<T: ?Sized>(_val: &T) -> &'static str ``` This is a convenience method to get the type name of a value, as opposed to `type_name` that takes a type as a generic. Const stability is not added because this relies on `type_name` which is also not const. That has a blocking issue rust-lang#97156. Wording was also changed to direct most of the details to `type_name` so we don't have as much duplicated documentation. Fixes tracking issue rust-lang#66359. There were two main concerns in the tracking issue: 1. Naming: `type_name_of` and `type_name_of_val` seem like the only mentioned options. Differences in opinion here come from `std::mem::{size_of, align_of, size_of_val, align_of_val}`. This PR leaves the name as `type_name_of_val`, but I can change if desired since it is pretty verbose. 2. What this displays for `&dyn`: I don't think that having `type_name_of_val` function resolve those is worth the headache it would be, see rust-lang#66359 (comment) for some workarounds. I also amended the docs wording to leave it open-ended, in case we have means to change that behavior in the future. `@rustbot` label -T-libs +T-libs-api +needs-fcp r? libs-api
…lnay Stabilize `type_name_of_val` Make the following API stable: ```rust // in core::any pub fn type_name_of_val<T: ?Sized>(_val: &T) -> &'static str ``` This is a convenience method to get the type name of a value, as opposed to `type_name` that takes a type as a generic. Const stability is not added because this relies on `type_name` which is also not const. That has a blocking issue rust-lang#97156. Wording was also changed to direct most of the details to `type_name` so we don't have as much duplicated documentation. Fixes tracking issue rust-lang#66359. There were two main concerns in the tracking issue: 1. Naming: `type_name_of` and `type_name_of_val` seem like the only mentioned options. Differences in opinion here come from `std::mem::{size_of, align_of, size_of_val, align_of_val}`. This PR leaves the name as `type_name_of_val`, but I can change if desired since it is pretty verbose. 2. What this displays for `&dyn`: I don't think that having `type_name_of_val` function resolve those is worth the headache it would be, see rust-lang#66359 (comment) for some workarounds. I also amended the docs wording to leave it open-ended, in case we have means to change that behavior in the future. ``@rustbot`` label -T-libs +T-libs-api +needs-fcp r? libs-api
Rollup merge of rust-lang#118234 - tgross35:type_name_of_value, r=dtolnay Stabilize `type_name_of_val` Make the following API stable: ```rust // in core::any pub fn type_name_of_val<T: ?Sized>(_val: &T) -> &'static str ``` This is a convenience method to get the type name of a value, as opposed to `type_name` that takes a type as a generic. Const stability is not added because this relies on `type_name` which is also not const. That has a blocking issue rust-lang#97156. Wording was also changed to direct most of the details to `type_name` so we don't have as much duplicated documentation. Fixes tracking issue rust-lang#66359. There were two main concerns in the tracking issue: 1. Naming: `type_name_of` and `type_name_of_val` seem like the only mentioned options. Differences in opinion here come from `std::mem::{size_of, align_of, size_of_val, align_of_val}`. This PR leaves the name as `type_name_of_val`, but I can change if desired since it is pretty verbose. 2. What this displays for `&dyn`: I don't think that having `type_name_of_val` function resolve those is worth the headache it would be, see rust-lang#66359 (comment) for some workarounds. I also amended the docs wording to leave it open-ended, in case we have means to change that behavior in the future. ``@rustbot`` label -T-libs +T-libs-api +needs-fcp r? libs-api
By the way this makes use ::qcell::{TCell, TCellOwner};
type Q1 = for<'any> fn(fn(&'any ()));
type Q2 = fn(fn(&'static ()));
let a = TCellOwner::<Q1>::new();
let b = TCellOwner::<Q2>::new(); // does not panic since `TypeId::of::<Q2>()` is distinct.
let x = TCell::new(42);
::core::mem::swap(
x.rw(&mut { a }),
x.rw(&mut { b }), // Q2 -> Q1 even though `TCellOwner<_>` is invariant
); Adding to what @BoxyUwU mentioned, it is interesting to observe that we currently have
even more of a footgun for the unaware. |
Deprecated because of fallout from: rust-lang/rust#97156 `TypeId::of::<L>() != TypeId::of::<R>()` does not imply `L != R` Removed all uses of `with_any` constructor outside of docs/tests specific to it.
change equate for binders to not rely on subtyping *summary by `@spastorino` and `@lcnr*` ### Context The following code: ```rust type One = for<'a> fn(&'a (), &'a ()); type Two = for<'a, 'b> fn(&'a (), &'b ()); mod my_api { use std::any::Any; use std::marker::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::<One>::new((|_, _| ()) as One); foo.deref(); let foo: Foo<Two> = foo; foo.deref(); } ``` has UB from hitting the `unreachable_unchecked`. This happens because `TypeId::of::<One>()` is not the same as `TypeId::of::<Two>()` despite them being considered the same types by the type checker. Currently the type checker considers binders to be equal if subtyping succeeds in both directions: `for<'a> T<'a> eq for<'b> U<'b>` holds if `for<'a> exists<'b> T<'b> <: T'<a> AND for<'b> exists<'a> T<'a> <: T<'b>` holds. This results in `for<'a> fn(&'a (), &'a ())` and `for<'a, 'b> fn(&'a (), &'b ())` being equal in the type system. `TypeId` is computed by looking at the *structure* of a type. Even though these types are semantically equal, they have a different *structure* resulting in them having different `TypeId`. This can break invariants of unsafe code at runtime and is unsound when happening at compile time, e.g. when using const generics. So as seen in `main`, we can assign a value of type `Foo::<One>` to a binding of type `Foo<Two>` given those are considered the same type but then when we call `deref`, it calls `downcast_ref` that relies on `TypeId` and we would hit the `None` arm as these have different `TypeId`s. As stated in rust-lang/rust#97156 (comment), this causes the API of existing crates to be unsound. ## What should we do about this The same type resulting in different `TypeId`s is a significant footgun, breaking a very reasonable assumptions by authors of unsafe code. It will also be unsound by itself once they are usable in generic contexts with const generics. There are two options going forward here: - change how the *structure* of a type is computed before relying on it. i.e. continue considering `for<'a> fn(&'a (), &'a ())` and `for<'a, 'b> fn(&'a (), &'b ())` to be equal, but normalize them to a common representation so that their `TypeId` are also the same. - change how the semantic equality of binders to match the way we compute the structure of types. i.e. `for<'a> fn(&'a (), &'a ())` and `for<'a, 'b> fn(&'a (), &'b ())` still have different `TypeId`s but are now also considered to not be semantically equal. --- Advantages of the first approach: - with the second approach some higher ranked types stop being equal, even though they are subtypes of each other General thoughts: - changing the approach in the future will be breaking - going from first to second may break ordinary type checking, as types which were previously equal are now distinct - going from second to first may break coherence, because previously disjoint impls overlap as the used types are now equal - both of these are quite unlikely. This PR did not result in any crater failures, so this should not matter too much Advantages of the second approach: - the soundness of the first approach requires more non-local reasoning. We have to make sure that changes to subtyping do not cause the representative computation to diverge from semantic equality - e.g. we intend to consider higher ranked implied bounds when subtyping to [fix] rust-lang/rust#25860, I don't know how this will interact and don't feel confident making any prediction here. - computing a representative type is non-trivial and soundness critical, therefore adding complexity to the "core type system" --- This PR goes with the second approach. A crater run did not result in any regressions. I am personally very hesitant about trying the first approach due to the above reasons. It feels like there are more unknowns when going that route. ### Changing the way we equate binders Relating bound variables from different depths already results in a universe error in equate. We therefore only need to make sure that there is 1-to-1 correspondence between bound variables when relating binders. This results in concrete types being structurally equal after anonymizing their bound variables. We implement this by instantiating one of the binder with placeholders and the other with inference variables and then equating the instantiated types. We do so in both directions. More formally, we change the typing rules as follows: ``` for<'r0, .., 'rn> exists<'l0, .., 'ln> LHS<'l0, .., 'ln> <: RHS<'r0, .., 'rn> for<'l0, .., 'ln> exists<'r0, .., 'rn> RHS<'r0, .., 'rn> <: LHS<'l0, .., 'ln> -------------------------------------------------------------------------- for<'l0, .., 'ln> LHS<'l0, .., 'ln> eq for<'r0, .., 'rn> RHS<'r0, .., 'rn> ``` to ``` for<'r0, .., 'rn> exists<'l0, .., 'ln> LHS<'l0, .., 'ln> eq RHS<'r0, .., 'rn> for<'l0, .., 'ln> exists<'r0, .., 'rn> RHS<'r0, .., 'rn> eq LHS<'l0, .., 'ln> -------------------------------------------------------------------------- for<'l0, .., 'ln> LHS<'l0, .., 'ln> eq for<'r0, .., 'rn> RHS<'r0, .., 'rn> ``` --- Fixes #97156 r? `@lcnr`
EDIT by @BoxyUwU
playground
has UB from hitting the
unreachable_unchecked
becauseTypeId::of::<One>()
is not the same asTypeId::of::<Two>()
despite them being considered the same types by the type checker. Originally this was thought to be a nightly-only issue withfeature(generic_const_exprs)
but actually the weird behaviour ofTypeId
can be seen on stable and result in crashes or UB in unsafe code.original description follows below:
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 theirTypeId
. 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 offeature(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
The text was updated successfully, but these errors were encountered: