may 20, 2026 // 5 min read
hey!! :33
so uh. you know how rust people keep saying "memory safe language btw"? i wanted
to see if i could get arbitrary code execution in Rust without a single unsafe
block. no nightly features, no proc macro tricks, no build.rs cheating. just
pure safe stable Rust, edition 2024
turns out... yes. very much yes lol
the bug
rust-lang/rust#135011 -
theres a soundness hole in how the type checker handles cyclically-defined
associated types. basically the type normalizer gets confused by a chain of
trait impls that form a cycle, and you can trick it into treating any type L
as any type R
in other words: std::mem::transmute, but in completely safe code.
fn transmute<L, R>(x: L) -> R {
transmute_inner_1::<L, R, RewriteReflRight<FakeRefl<L, R>>>(x)
}
the full machinery is ~80 lines of trait definitions that construct a fake type equality proof. the compiler accepts it because the cyclic normalization prevents it from ever actually resolving the associated types to verify they match
this compiles on stable. the compiler literally says "yeah this is fine"
so what can you do with this
anything std::mem::transmute can do... forge pointers, create fake vtables,
reinterpret arbitrary bytes as any type. its game over basically.
for the demo i went with the classic: forge a function pointer to
libc::system() and call it
type SystemFn = extern "C" fn(*const u8) -> i32;
fn main() {
let system_fn: SystemFn =
transmute::<usize, SystemFn>(libc::system as *const () as usize);
system_fn(b"echo 'mrow :3'\0".as_ptr());
}
output:
[*] rustpwn - ACE in safe stable Rust
[*] rust-lang/rust#135011
mrow :3
zero unsafe keywords anywhere. compiles on stable. runs on stable.
grep -rn unsafe src/main.rs returns nothing. #[forbid(unsafe_code)] passes
how the type trickery works
it constructs a fake proof of L == R through several layers of traits:
TypeIs<T>- trivial identity trait,type Is = Tfor everythingTyEq/TyEqImpl- trait pair representing a "proof" thatLHS == RHSRefl<T>- the real deal, legitimate proof thatT == TFakeRefl<L, R>- the scam. "proof" thatL == R, deriving itsTyEqimpl fromDeriveEqFromImplwhich delegates toProof<()>- creating the cycleRewriteReflRight<P>- rewrites the right side of a proof using another proofs definition, exploiting the fact that the compiler cant fully normalize the cyclic chain
the key: FakeRefl<L, R> claims L == R by saying "my proof is whatever
FakeRefl<L, R>::Proof<()> says"... which is itself. the compiler chases this
cycle, gives up on normalization, and goes "sure whatever". lol
btw steffahn (the person who reported the bug) managed to minimize the whole thing down to this:
trait Tr<R>: Sized { type Prf: Tr<R, Prf = Self>; }
impl<L, R> Tr<R> for L {
type Prf = R where L: Tr<R>, R: Tr<R, Prf = <L::Prf as Tr<R>>::Prf>;
}
fn transmute<L: Tr<R>, R>(r: L) -> <L::Prf as Tr<R>>::Prf { r }
4 lines. safe transmute in 4 lines of stable Rust lol
the rabbit hole
so i went digging through the issue thread and uh... it gets worse
this bug is 11 years old
steffahn found a version that compiles all the way back to Rust 1.1 (june 2015). this isnt some recent regression. its a fundamental defect in the trait solver that has existed since basically forever
its actually multiple bugs
lcnr from the types team analyzed the minimized versions and discovered that steffahn accidentally found a second separate unsoundness while minimizing the first one:
so the original exploit hits one bug, and the 4-line version hits a different one. great
P-medium. no assignee.
a full transmute in safe stable Rust - arbitrary type confusion, arbitrary
code execution, complete destruction of every safety guarantee the language
promises - and its labeled P-medium. nobody is assigned to fix it. since
january 2025
for context, as of may 2026 there are 100+ open I-unsound issues on the rust repo. this is just one of them
the fix requires rewriting the trait solver
the root cause according to lcnr: the compiler trusts item-bounds of an associated type without verifying that the type actually normalizes to something concrete. when the associated type "diverges" (references itself cyclically), the bound is invalid but the compiler never checks
Nadrieril connected it to classical type theory - its the ω combinator for non-positive recursion. the bug isnt in the implementation, its in the design of the type system itself - it allows non-positive recursion through associated types, which is known to be unsound in dependent type theory
the real fix is the
trait-system-refactor-initiative,
specifically the new trait solver (-Znext-solver). but thats a massive rewrite
thats been in progress for years and isnt ready yet
any crate can silently include this
the scariest part: any crate on crates.io can embed this transmute and break all
safety guarantees of downstream code. cargo audit wont flag it.
#[forbid(unsafe_code)] wont catch it. theres no unsafe keyword to grep for.
clippy wont complain. miri cant help because theres nothing to detect at the
source level - the types just quietly lie
but what about shellcode
so i also tried writing actual aarch64 shellcode (mmap RWX page, write
instructions, jump to them). it works in theory but on apple silicon, macos
enforces W^X for JIT pages via MAP_JIT + pthread_jit_write_protect_np and
you need to forge calls to those too. doable but annoying. the system()
approach proves the exact same thing with less hassle
on x86_64 linux where you can mmap RWX pages freely - raw shellcode execution
should just work
some conclusions
"rewrite it in rust" they said. "memory safe" they said.
11 years. P-medium. no assignee. january 2025. still compiles. still runs.
src: clickme