Repository navigation
Description
This isn't quite trivial, but well… it works.
a “transmute” in safe Rust:
fn main() {
let s: String = transmute(vec![65_u8, 66, 67]);
println!("{s}"); // ABC
}fn transmute<L, R>(x: L) -> R {
transmute_inner_1::<L, R, RewriteReflRight<FakeRefl<L, R>>>(x)
}
fn transmute_inner_1<L, R, P: TyEq<LHS = L, RHS = R>>(x: L) -> R {
transmute_inner_2::<<P::ProofIsProofValue as TyEq>::ProofDef, L, R, P::ProofValue, P>(x)
}
fn transmute_inner_2<Rw, L, R, Actual, P: TyEq<LHS = L, RHS = R, ProofValue = Actual>>(x: L) -> R
where
<Rw as TypeIs<P::ProofValue>>::Is: TyEq<LHS = L, RHS = R>,
{
transmute_inner_3::<L, R, Actual>(x)
}
fn transmute_inner_3<L, R, P: TyEq<LHS = L, RHS = R>>(x: L) -> R {
transmute_inner_4::<R, P::ProofDef>(x)
}
fn transmute_inner_4<R, Rw>(x: <Rw as TypeIs<R>>::Is) -> R {
x
}trait TypeIs<T> {
type Is;
}
impl<_Self, T> TypeIs<T> for _Self {
type Is = T;
}
trait TyEq: TyEqImpl {
type ProofDef: TypeIs<Self::RHS, Is = Self::LHS>;
}
trait TyEqImpl {
type LHS;
type RHS;
type Proof<_Dummy>: TyEq<LHS = Self::LHS, RHS = Self::RHS>;
type ProofValue;
type ProofIsProofValue: TyEq<LHS = Self::Proof<()>, RHS = Self::ProofValue>;
}
trait DeriveEqFromImpl {}
impl<P: TyEqImpl + DeriveEqFromImpl> TyEq for P {
type ProofDef = <P::Proof<()> as TyEq>::ProofDef;
}
struct Refl<T>(T);
impl<T> TyEqImpl for Refl<T> {
type LHS = T;
type RHS = T;
type Proof<_Dummy> = Refl<T>;
type ProofValue = Refl<T>;
type ProofIsProofValue = Refl<Refl<T>>;
}
impl<T> TyEq for Refl<T> {
type ProofDef = ();
}
struct FakeRefl<L, R>(L, R);
impl<L, R> DeriveEqFromImpl for FakeRefl<L, R> {}
impl<L, R> TyEqImpl for FakeRefl<L, R> {
type LHS = L;
type RHS = R;
type Proof<_Dummy> = FakeRefl<L, R>;
type ProofValue = FakeRefl<L, R>;
type ProofIsProofValue = Refl<FakeRefl<L, R>>;
}
struct RewriteReflRight<P>(P);
impl<P> DeriveEqFromImpl for RewriteReflRight<P> {}
impl<P, L, R> TyEqImpl for RewriteReflRight<P>
where
P: TyEq<LHS = L, RHS = R>,
{
type LHS = L;
type RHS = R;
type Proof<_Dummy: RewriteReflRightHelper> = _Dummy::RewriteRight<Refl<L>, R, P::ProofDef>;
type ProofValue = Refl<L>;
type ProofIsProofValue = Refl<Refl<L>>;
}
trait RewriteReflRightHelper {
type RewriteRight<P, R, Rw>: TyEq<LHS = P::LHS, RHS = R>
where
P: TyEq<RHS = <Rw as TypeIs<R>>::Is>;
}
impl<_Dummy> RewriteReflRightHelper for _Dummy {
type RewriteRight<P, R, Rw>
= P
where
P: TyEq<RHS = <Rw as TypeIs<R>>::Is>;
}Besides this unsoundness, there are also many ICEs. This isn't surprising, since we're not really transmuting, but rather sort-of … convincing the type system that the 2 types were never different in the first place.
fn main() {
let s: u8 = transmute(());
println!("{s:?}");
}Or interesting LLVM-errors
fn main() {
let s: &() = transmute("hi");
println!("{s:?}");
} Compiling playground v0.0.1 (/playground)
Invalid bitcast
%7 = bitcast { ptr, i64 } %6 to ptr, !dbg !425
in function _ZN10playground17transmute_inner_317h363424402e6a7153E
rustc-LLVM ERROR: Broken function found, compilation aborted!
error: could not compile `playground` (bin "playground")
Oh… that one on nightly has… the compiler itself execute a SIGILL? (Is that actual UB in the compiler!? Or UB in LLVM? Or can SIGILL be part of a deliberate crash condition?)
From a background of “this works a bit like Coq”, the relevant parts that make this work is how:
FakeRefl<L, R>is a “proof” thatLandRare the same types, which doesn't terminate (i.e. it is cyclically defined in terms of itself)- unlike true proof assistants, Rust's trait system does allow some stuff to be defined in a somewhat cyclical manner, especially pre-monomorphization
- overflow during monomorphization is avoided anyways by:
- introducing the
Rewrite…step which rewritesRefl<L>(a proof ofTyEq<LHS = L, RHS = R>) using thisFakeReflproof - at the same time also tracking that the proof term itself doesn't actually change with the rewrite
- using this tracked meta-information in order to eliminate the usage of
FakeReflbefore its (indirectly and cyclically defined)ProofDeftype ever ends up being instantiated
- introducing the
Of course, this is just my intuitive description from a user perspective :-)
I have no idea which part of this code to blame exactly. Perhaps someone else with better knowledge of the theory behind Rust's typesystem and trait system could have a better clue here.
I'm not quite sure if this can perhaps be "simplified" to remove the need for GATs
Update: It can be "simplified" to remove the need for GATs. (playground)
Update2: It's even possible to rework this into working since Rust 1.1
(this section has become somewhat outdated due to this update, click to expand anyway)
I'm not quite sure if this can perhaps be "simplified" to remove the need for GATs; however, I wasn't able to the Rewrite… stuff to work without this trick
type Proof<_Dummy: RewriteReflRightHelper> = _Dummy::RewriteRight<Refl<L>, R, P::ProofDef>;where the (vacuous) _Dummy: RewriteReflRightHelper restriction prevents the compiler from already knowing the concrete RewriteReflRightHelper impl. (If you worry about the syntactical mismatch of
type Proof<_Dummy: RewriteReflRightHelper>here vs
type Proof<_Dummy>: TyEq<LHS = Self::LHS, RHS = Self::RHS>;without bounds on _Dummy in the trait definition: that's actually avoidable (<- see this playground on how the bound can be repeated everywhere and the impl RewriteReflRightHelper for … can even be for a concrete type only).
So for now, I would conjecture that this soundness hole does require GATs.
Edit: I completely forgot about the (generous) usage of GATs inside of RewriteReflRightHelper ~ so, yes, GATs seem fairly integral to this.
Edit2: Nevermind, those can all be eliminated leaving only a single GAT.
(end of somewhat outdated section)
This is not a regression. It also isn't influenced by -Znext-solver.
This is a relatively clean typesystem hole, on stable Rust, without many of the other known-problematic features such as HRTBs, lifetimes, variance, trait objects, or implicitly defined trait impls for functions/closures/etc…
@rustbot label I-unsound, I-ICE, I-crash, T-types, A-type-system, A-trait-system, A-associated-items
related issues
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsnew solver everywhere
Activity
saethlin commentedon Jan 2, 2025
With LLVM assertions enabled, I get this instead of just a SIGILL:
and if I pass
-Zverify-llvm-ir, I get the same nice error:I believe the difference you're observing on nightly is #133499 cc @nikic
Oh oops, we document that IR is not verified by default, so it's definitely intentional that on nightly we crash somewhat incoherently with invalid IR.
apiraino commentedon Jan 2, 2025
@steffahn Can you (or anyone else having context here) please provide a digestible summary of this issue? What is happening that shouldn't?
By reading this sentence I infer that it's not a simple regression. Would you like the appropriate team to discuss this?
Thank you from the triage.
workingjubilee commentedon Jan 2, 2025
It's not really a regression at all in the conventional sense.
18 remaining items
steffahn commentedon Jan 7, 2025
Is this more “minimal”? I don’t know… but it’s certainly more concise… more dense… anyway 😅
Edit: Eliminating/combining some more types…
steffahn commentedon Jan 7, 2025
I'll make this one into a follow-up because it's getting a bit wild; only one (generic) associated type is enough
or, with a bit less
, ()… things:the
Rcan be a trait parameter, toolcnr commentedon Jan 7, 2025
that minization is actually a separate unsoundness (or at least what I intend to do to fix the original is insufficient there 💀). That sure is something
lcnr commentedon Jan 7, 2025
What's happening at ¹ is that proving that the item-bounds of an associated type is able
to assume the item-bounds of exactly that associated type. This is non-productive cyclic reasoning.
You've found a new way to exploit rust-lang/trait-system-refactor-initiative#62, answering the question posed in rust-lang/trait-system-refactor-initiative#116 😊
lcnr commentedon Jan 7, 2025
My perspective of the original unsoundness is that we're able to rely on the item-bounds of an associated type while never actually normalizing the associated type, which is unsound in case it diverges.
While your new minimization does not actually have any associated type which diverges. We're able to normalize
<L::Proof as Trait<R>>::ProoftoRwithout issues. The problem is the cyclic reasoning when proving that the item-bounds are actually valid.steffahn commentedon Jan 7, 2025
Oh… that’s interesting! I didn’t even really pay attention to the fact that it’s no longer containing containing any “cyclically defined ATIT”, at least for certain notions of “cyclically defined”. I got there (to the minimization) in a relatively incremental/step-by-step manner; but that certainly explains why I was having a hard time [i.e. not succeeding in] trying to translate the last version back into one without ATIT-
where-bounds 🙃lcnr commentedon Jan 8, 2025
Alright, have a minimization I am happy with now and feel like I properly understand this bug:
https://rust.godbolt.org/z/Y6K3aKGGq
As mentioned by @nikomatsakis in rust-lang/project-dictionary-passing#7 (comment) we've got a few ways forward here. I expect that the most likely approach will be to make sure we recheck where-clauses of functions during monomorphization to trigger the overflow at this point. Eagerly detecting potentially diverging associated types feels impossible to me without breaking too much existing code. @compiler-errors has implemented this in #135216 for a crater run.
Landing this may be blocked on the new solver as it will likely cause new overflow errors due to hitting the recursion limit. These are fatal with the old trait solver.
Nadrieril commentedon Jan 10, 2025
Small note that may be of interest: this also works:
In other words, there's no need for a self-recursive trait impl for this. This is in fact the classic exploit for how to construct a non-terminating term in a language that allows for non-positive recursion (
impl Diverges for ()is ω from the link and the rest of the exploit is about carefully applying it to itself).Also tiny simplification: you don't need to involve
Tin theApplyimpl, this suffices:I made my own version of this to help me understand.
steffahn commentedon Jan 14, 2025
I've tried to minimize a GAT-free version (and no
whereon associated types, which stabilized at the same time), this one works since 1.49 again. I might also try to minimize for a version that still supports all the way to Rust 1.1.(on 1.49 in compiler-explorer)
This is rather minimized for "size" (fewer traits, fewer associated types), not for ease of understanding. Which also means traits and types get dual use and are hard to name; like
Equalbeing used forProof: Equal<T1, To = T2>to mean thatProofguaranteesT1andT2are the same, but also it has a trait bound ans is used to defined the cyclically defined typeCyclicinMulti; which in turn however also contains another associated typeSimplwhich itself has dual-use; one being thatT1: Multi<(), Simpl = T2>serves as another way to write:T1andT2are the same type; and the other relates to the: Multi…bound on it – in this function, it corresponds most closely to the role ofProofIsProofValuein the original post.steffahn commentedon Jan 14, 2025
Of course, given the 1.1 version exists, that’s not a full soundness regression at that point, but it still seems interesting 1.49 keeps coming up here. So I was curious where exactly the relevant change was lying:
Bisection this points to #80246 being relevant… which also explains the remarkably confusing bisection behavior, lol. (I had to go 2 release cycles newer than the anticipated nightly range; but the PR apparently was beta-backported very close before a release).
It’s funny that the PR description reads like the change was mainly about becoming more restrictive, but apparently for this code, effectively the compiler became more lenient instead?
Edit: There’s a second relevant bisection point. The code between stable versions goes from not compiling due to a (huge) list of overflow and mismatch errors
(this is rather lengthy… click to expand)
directly to the code working.
On nightly, there’s an intermediate step, where it returns a different, shorter error
which goes away with the abovementioned #80246.
Out of curiosity, I've bisected this earlier point of change now as well, and the relevant PR appears to be #73905
[-]Typesystem soundness hole involving cyclically-defined ATIT, and normalization in trait bounds[/-][+]Typesystem soundness hole involving cyclically-defined associated type in trait, and normalization in trait bounds[/+]Test rejection of rust-lang/rust#135011