I finally found the cheat-code for disabling the type-checker /s

Is such problem caused by recursion and type erase? E.g, type Gender defined in trait Trans recursively uses Trans. No warnings at all. But for the following code

fn a(mut u: u8) -> u8 {
    u *= a(u) + 1;
    a(1)
}

The compiler just showed warning about function a instead of saying it's an error.

Recursive functions are a normal programming language feature. In bad cases, they might never terminate at run-time when you call them, but that's fine. (You can get a warning at compile time, but that's just for convenience, the run-time behavior of this function is well-defined and not problematic to the type system at large: it just never finishes - or overflows the stack.)

Bad cases of recursively defined type synonyms on the other hand, should be erraneous at compile-time, at least if they're used somewhere. Type synonyms/aliases don't exist at run-time. The issue in Typesystem soundness hole involving cyclically-defined ATIT, and normalization in trait bounds Ā· Issue #135011 Ā· rust-lang/rust Ā· GitHub is with a situation where such a type was ā€œusedā€ in some sense - to make use of its trait bound - but ended up ā€œunusedā€ in another sense - so that the non-terminating recursive nature of it didn't result in a compilation error.

That's a compiler bug, as it results in undesirable consequences[1].

This particular ā€œminimizedā€ version of the code ends up not really being recursive anymore though. If you pay attention to the definition, the right-hand side is merely = L. Badly cyclic reasoning is however still present in the trait bounds that are required & used.

The relevant point in the linked issue, where a bit more explanation is given (for a slightly different variant of this code), is this comment, offering some comments in the code, and linking some related discussions. Also after that comment a separate thread was opened for this ā€œminimizedā€ code, as it was deemed sufficiently different from the original issue.

Understanding the code in much more detail is hard, the issue involves subtle points in the interaction of various aspects of the type system – ultimately it's arguably just about understanding all the relevant implementation details of Rust's type and trait system. Remember, it is a bug, there's only limited learning opportunity from asking ā€œwhy this code worksā€ from pure language userʼs perspective, because it isn't even supposed to work. Some compiler team members who've commented on the issue definitely have an even better understanding of the underlying issue than I have; I've already linked a relevant comment above.

Please do note that the main purpose of these issues threads on GitHub is to track any progress with fixing the problems, and I expect it might take a while until they're fixable. So even though the Github issues are a relevant place to look for leaning more about the details of this exploitation, to keep the issues' discussion threads manageable[2], they might not be the best place to ask very basic questions about it. Thank you for asking here instead.


  1. unsound code without requiring the unsafe keyword; some cases of ā€œinternal compiler errorā€s of bringing the compiler into a state where e. g. the possibility of mismatching types (rightfully) wasn't anticipated by transformations or code generation that happen later than the trait and type checking; even some cases of bad code being generated for LLVM which then crashes from thinfstlike incompatible calling conventions ā†©ļøŽ

  2. not growing too large over time ā†©ļøŽ

7 Likes

Yeah, we don't pretend to fully understand the causes for the bug either, we just saw the post in a Discord and decided to make an account exactly for that one singular shitpost :stuck_out_tongue:.

This was a very good read! Thank you for all the explanation and the time taken to explain it. We read the code and just assumed "yeah something probably got erased here and some invariant unchecked".

this ended up being an actual tutorial /s

2 Likes

For a more sensible debugging solution maybe just hack std to impl Debug for everything, printing its name and turn on unstable specialization?

Is there an easy way (for human) to detect these bugs in safe code?

Not yet; the easy way will be to upgrade the compiler once the soundness bug is fixed. However, known soundness bugs rely on quite convoluted code that would be very difficult to write in a way that seems justified and not suspicious when browsing the code.

That isn't to say it's impossible, but at the end of the day, you need to trust the code you're compiling in order to run it anyway, because malware is still safe; unsafe is not a security boundary.

2 Likes

If change
let s: String = vec![65_u8, 66, 67].transmute();
to let s: String = [65u8, 66, 67].transmute();
it got a SegFault. If change it to
let s: u32 = [u8::MAX; 4].transmute();
it printed 255. But converting [u8::MAX; 4] from native endian

println!("{}", u32::from_ne_bytes([u8::MAX; 4]))

doesn't show 255 on my machine.

Funny enough, on playground this code not just breaks and not just doesn't compile - it makes LLVM generate an error:

   Compiling playground v0.0.1 (/playground)
rustc-LLVM ERROR: unable to allocate function return #3
error: could not compile `playground` (bin "playground")

Weird behavior is expected. Quoting myself from the linked issue on GitHub:

we're not really transmuting, but rather sort-of … convincing the type system that the 2 types were never different in the first place

Besides illegal LLVM IR being generated, or other compiler crashes at compile-time, even in cases where the ā€œtransmutationā€ does ā€œworkā€, I’d expect a Foo-to-Bar ā€œtransmutationā€ using my code from the OP to operate more like casting a function pointer with mismatching argument types, i.e. from fn(Foo) to fn(Bar) and then calling it with Foo arguments; and not like simple transmuting of Foo to Bar directly via std::mem::transmute. Feel free confirm or deny this conjecture or mine on your platform yourself by testing it: casting (mem::transmute) a function pointer fn(u32) to fn([u8; 4]) and passing [u8::MAX; 4] might result in the same thing (255). Though to be clear, even in cases where conditions documented e.g. this section on ABI compatibility are met, using this code/approach of breaking the type system might very well still not work – there are of course no guarantees whatsoever around usage of such exploits.

Casting function pointers has stricter requirements for soundness than simple mem::transmute does[1]. In particular, there’s no guarantee for ā€œABI-compatibilityā€ between e.g. [u8; 4] and u32 even thought they are guaranteed to have the same size, so ordinary mem::transmute between the two should work just fine.

I don’t know any relevant details of any platform’s calling conventions off the top of my head, but for a 255 result to make sense, I can e.g. imagine that there might be platforms that pass [u8; 4] to a function in 4 separate registers, whilst passing u32 in a single register (one of the 4 being used for the former) which would explain a result of 255 on such a platform.


  1. I’ve already linked relevant documentation in the previous paragraph ā†©ļøŽ

3 Likes

I used rustc 1.87.0-nightly (96cfc7558 2025-02-27) on ARM64.

    let s: *mut str = "".transmute();
    println!("{:?}", unsafe { *s })

Doesn't compile:

println!("{:?}", unsafe { *s })                   |               ----   ^^^^^^^^^^^^^ doesn't have a size known at compile-time

Is it possible to get str behind a raw pointer?

sure, it’s a compiler-bug, so it can in principle be platform dependent, compiler-version dependent, depend on optimization settings, on other seemingly unrelated code, on … I guess … really on almost anything!

yes, it’s possible, but that’s really getting a bit off-topic here.

I’d suggest you could open a new topic if you want to do much more experimentation with this and plan on asking for more help & explanations along the way.

I think your code is still hard to understand.

Why does it need unit tuple () in declaration?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.