The unread binary every compiler still trusts
Translating rustc to C changes the compiler's substrate, not its logic of trust: build pipelines execute on resolved references, never verified content.
A compiler does one thing. It reads source text and emits instructions a machine will execute. rustc reads Rust, lowers it through LLVM’s intermediate representation, and produces an object file. crustc is that same procedure re-expressed in C. The grammar it parses is identical, the type system it enforces is identical, the instructions it emits are, by design, identical. The language the compiler is written in is not a property of the programs it compiles. It is a property of the compiler alone.
This matters because a compiler is not consumed the way an application is. It is a machine that produces other machines. Every binary rustc emits carries forward decisions rustc made, and rustc itself was emitted by a prior compiler. The output is trusted because the tool is trusted. The tool is trusted because it compiled cleanly under a tool that came before it. Translating rustc to C does not add a check anywhere in that chain. It changes the substrate. It does not change the logic of trust.
The visible thing, the artifact, is source code in C rather than Rust. The invisible thing, the actual object, is a self-perpetuating claim: that what a compiler emits corresponds to what its source describes. That claim is not verified at emission time. It is inherited. crustc inherits it from rustc, rustc inherits it from the compiler that bootstrapped it, and so on backward to a binary no one in the chain reads. The system executes on the strength of that inheritance. It does not re-derive it.
The assumption underneath every compiler toolchain is that trust in a build tool is persistent and transferable. Persistent, meaning a compiler verified once is treated as verified for every subsequent invocation. Transferable, meaning trust in the tool extends automatically to everything the tool produces. rustc is trusted, therefore the binaries rustc emits are trusted, therefore the next compiler those binaries build is trusted. The relation is treated as transitive without limit.
This is the assumption Ken Thompson described in “Reflections on Trusting Trust” in 1984. The point was not that a specific compiler was compromised. The point was structural: a compiler can be made to reproduce a modification in its own output, including in future copies of itself, such that the modification survives even after it is removed from the source. Nothing in the source reveals it, because the source is not what runs. What runs is the prior binary, and the prior binary was produced by a binary before it. The assumption that source review covers the tool is an assumption about a thing that is never directly executed.
Reproducible builds, deterministic output, bootstrapping from a known compiler, these exist to make the assumption defensible. They do not remove it. They narrow the window in which it must be taken on faith. crustc, as a C re-expression of rustc, is presented as a smaller window. C has more independent compilers, more scrutiny, a longer history of readers. The reasoning is that a compiler written in C is easier to audit, therefore more trustworthy. But that reasoning trusts the audit of the source, and the source is not the thing that trust flows through. Trust flows through the binary that the C is compiled into, by a C compiler that was itself trusted the same way.
What changed is not the compiler and not any actor near it. What changed is the standing of the assumption that trust in a tool transfers cleanly to its output. That assumption held when a toolchain was short, when the distance between a human-read source and an executed binary was small, when the number of intermediate compilations was countable. rustc is not that toolchain. It resolves hundreds of dependencies through cargo and crates.io, each identified by version reference, each compiled by the same trusted tool, each folding into the binary that becomes the next compiler. The chain grew. The assumption did not re-evaluate itself when it did.
The assumption assumed a world of few sources and direct verification. In practice the world became one of retrieval by reference. A build does not assess the source of the code it compiles. It resolves a name to a version, fetches the artifact that name points to, and compiles it because the reference resolved, not because the content was validated. crustc does not alter this. It is retrieved and compiled the same way. A C source tree that reproduces rustc is trusted for what it claims to be, resolved by its identity, not re-derived from its behavior. The mechanism that would catch a divergence between claim and content is the mechanism that was never there.
That assumption no longer holds, and the system contains nothing that noticed. There is no point in the pipeline where trust is recomputed against present conditions. Trust was resolved once, at some origin no current build reaches, and every build since has inherited the result. Translating rustc to C moves the substrate from one language to another. It does not move the moment of validation forward in time. The compiler still emits what it was built to emit. What it was built to emit is still defined by a compiler no one reads. The window narrowed. The inheritance did not end.
A build begins with a name. cargo reads a manifest, resolves each entry to a version, and fetches the artifact that version points to from crates.io. What arrives is accepted because the reference resolved. The resolution is the event the system treats as authoritative. The content that arrives is compiled on the strength of having been named, not on the strength of having been examined. crustc enters the same pipeline under the same rule. A C source tree that declares itself the translation of rustc is fetched, compiled, and folded into the toolchain because its identity resolved cleanly, not because its emitted instructions were compared against what rustc would have produced.
This is not a bypass. Nothing was defeated. The pipeline did exactly what a build pipeline does: it took a reference, resolved it, retrieved the referent, and executed. The integrity check the system performs is a check on the reference. That the name maps to the expected version. That the artifact matches the recorded hash of that version. It is not a check on the content, on whether the artifact does what its source describes. The hash confirms the bytes are the bytes that were published. It says nothing about whether the bytes published correspond to the source a human read. Identity of the source stood in for integrity of the content, and the system cannot tell the two apart because it was never built to.
Externally, every stage reports correct. The compiler runs. The type checks hold. The object file is produced and it functions. There is no observable divergence between a toolchain built from an honest translation and one built from a translation that departs from its source, because the only thing the pipeline observes is that the reference resolved and the build completed. The construction Thompson described is precisely the case where source and binary disagree and nothing in the pipeline is positioned to notice, because the pipeline reads the reference, not the behaviour. crustc changes what language the source is written in. It does not change what the pipeline reads. The pipeline still reads the reference, and the reference still resolves.
The pattern is execution on reference rather than verification. A system is handed a name, resolves the name to a thing, and acts on the thing because the name resolved. The step that would confirm the thing is what it claims to be, that its behaviour matches its description, is absent, because the resolution already produced an answer the system treats as sufficient. Reference is cheap and total. Verification is expensive and partial. The system optimizes for the one it can complete and calls the result trust.
Consider X.509 certificate validation, the mechanism underneath every TLS session. A server presents a certificate. The client accepts it because it chains to a root already in the local trust store, each link signed by the one above it, terminating in a certificate authority the client was configured to believe. The chain resolving is the event. What the chain proves is narrow: that some key signed the certificate below it, back to a root. It does not prove that the party presenting the certificate is the legitimate holder of the name inside it. When a certificate authority is induced to issue a valid certificate for a domain it should never have signed, the chain still resolves. Every client accepts it. Nothing was bypassed. The reference resolved, and resolution is the thing the system was built to act on.
The mechanism is identical to the compiler. In both cases trust flows through a resolution, not a re-derivation. In both cases the artifact is accepted for what it references, not for what it does. crustc and a mis-issued certificate fail the same way, not because they share a technology, but because they share a structure. A system that resolves an identity once and executes on the result treats the result as equivalent to the truth. It is not. It is a claim that resolved. The distance between a claim that resolved and a fact that was verified is the distance every supply chain compromise travels, and the system that carries it registers no motion, because from the inside, resolution and verification produce the same green light.
The toolchain resolves trust once, at an origin no current build reaches. Every build since has inherited the result. It does not revalidate.
crustc moves the substrate from Rust to C. It narrows the window in which the inheritance is taken on faith. It does not close it, because the thing that runs is still a binary no reader in the chain reads.
A compiler written in a more auditable language is a more auditable claim. The claim is still resolved by reference, not derived from behaviour. The control exists. The outcome does not.
Keep Reading
memory safetycrustc ports rustc to C and voids every safety proof
Translating rustc to C strips Rust's compile-time memory-safety guarantees and reopens out-of-bounds writes, UAF, and type confusion in the toolchain.
software supply chainA name is not trust
Package registries resolve a name to an artifact, not a source. When the party behind a trusted name changes, the system inherits trust it cannot verify.
delegated trustThe valet's key still opens your Civic
How a Honda Civic keeps granting access long after the conditions of trust expire, and why reference replaces verification across systems.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.