The compiler you didn't build
I opened a PR against nixpkgs to
bootstrap Rust from source — mrustc up through the version chain, no prebuilt
compiler seed. The reception taught me more about the state of our field than
about the patch. Not because the technical objections were wrong — build time
and toolchain drift are real costs — but because of the reflex underneath them:
we’ve always downloaded the compiler, and downloading the compiler is fine.
Correction, 26 July 2026: I originally ended this post by claiming that my ordinary change-management workflow had already upgraded the chain from Rust 1.90 to 1.97 in a little over three hours. It had not. The workflow had finished; the compiler update had not. I have left the correction and the actual result at the end because confusing those two facts is part of the story.
It is not fine. Let me explain why, without the mysticism.
What “trusting trust” actually means
In 1984 Ken Thompson used his Turing Award lecture, Reflections on Trusting Trust, to describe an attack that people still treat as a curiosity. You teach a compiler to recognise when it is compiling itself, and to inject a backdoor into the output. Then you remove the evidence from the source. From that point on, the source is clean, every audit passes, and every compiler built by that compiler is born already infected. The bug lives in the binary and reproduces through the binary. You cannot find it by reading code, because there is no code to read.
For decades the standard response was “cute, but theoretical.” It stopped being
theoretical in August 2009, when
W32/Induc-A turned
up in the wild. It infected the Delphi compiler — dropped itself into the
SysConst unit so that every program that compiler subsequently built carried
the virus, which then looked for other Delphi installations to infect. Here is
the part that should keep you up at night: it had no payload. It did nothing
but replicate. It rode inside shipped, signed, legitimate commercial
applications for months before anyone noticed, precisely because it wasn’t
doing anything — it was just quietly proving that a self-perpetuating compiler
infection is not a lecture-hall hypothetical. It is a thing that has already
happened, with a compiler far less central than the one under your cargo build.
Now put a payload on it.
Reproducibility that stops at the compiler
Here is the contradiction I cannot get past. NixOS is built on a beautiful promise: pin every input, isolate every build, and you get the same artifact from the same sources, forever. The lockfile is the environment. That’s the whole reason I’ve run NixOS for as long as I can remember — finally, a system that makes sense.
And then, at the root of the tree, sits a compiler that nobody in the chain built from source. It was produced by someone else’s build farm, signed, and served as a binary. Every reproducible build in the entire distribution stands on that one unreproduced object. You have poured concrete over the whole foundation and left a single brick that says trust me.
Downloading a prebuilt compiler into a system whose entire selling point is reproducibility isn’t just inelegant. It collapses the trust perimeter down to a black box: the CI infrastructure that built the binary, the access controls around it, and the key that signed it. Everything downstream — the kernel, your shell, the tool checking the next build for tampering — inherits whatever that black box decided to hand you.
The single key nobody wants to talk about
While a proposal of mine to distribute signing across multiple parties got
waved off as over-engineering, the same conversation rests its entire trust
model on cache.nixos.org-1: one long-lived Ed25519 signing key. The cache
signing mechanism landed in 2014;
the key has, as far as anyone outside the infra team can see, never been
rotated. One key, a decade in service, signing the software base of an entire
ecosystem, its handling opaque, quite possibly nowhere near a hardware security
module, and adjacent to a pile of aging Perl. That is the thing we are asked to
consider safe, while threshold signing is the thing considered exotic.
Compromise that key, or the machine that holds it, and you don’t need Thompson’s trick at all. You just sign your own binaries.
This is how you take over the world
I don’t mean this as bravado. I mean it as a threat model, because the only defense that works is the one that assumes the attacker is competent. Here is the shape of the attack that a full-source bootstrap is meant to prevent, and that a downloaded compiler invites:
┌────────────────────────────────────────────────────────────────┐
│ 1. Build bespoke malware for ONE target. No code reuse. Burn │
│ the tokens. Use an uncensored model for the obfuscation. │
└───────────────────────────────┬────────────────────────────────┘
│
┌───────────────────────────────▼────────────────────────────────┐
│ 2. Compromise ONE human with access to the compiler's build │
│ farm. You need a person, not a zero-day. │
└───────────────────────────────┬────────────────────────────────┘
│
┌───────────────────────────────▼────────────────────────────────┐
│ 3. Kernel-resident implant watches for one event: │
│ "the compiler is compiling a compiler." │
│ │
│ rustc ──builds──► rustc' │
│ ▲ │ implant injects a tiny loader │
│ └────────────────────┘ into the OUTPUT compiler │
│ the poisoned compiler now poisons every compiler it makes │
└───────────────────────────────┬────────────────────────────────┘
│
┌───────────────────────────────▼────────────────────────────────┐
│ 4. The loader ships in every binary that compiler produces. │
│ Dormant for months. Wakes on a rare time trigger. │
└───────────────────────────────┬────────────────────────────────┘
│
┌───────────────────────────────▼────────────────────────────────┐
│ 5. Stage two arrives over a decentralised channel — DHT, │
│ a chain, pick your poison. No fixed C2 to block. No │
│ source line to grep. Nothing to reproduce against. │
└───────────────────────────────┬────────────────────────────────┘
│
▼
6. You own everything that compiler ever touched.
A statically self-hosting language like Rust or Go is, if anything, the prime
candidate: the compiler is written in the language it compiles, so the loop in
step three is native. Only a compiler chain that is bootstrapped from source —
all the way down to a seed a human can audit — closes it. That is the entire
point of mrustc, of bootstrappable builds, of
Guix’s full-source bootstrap. It is not purism. It is the one thing that makes
step three impossible.
And it is going to matter more every year, not less. Exploits already ship faster than consensus systems can settle a patch — I have watched an exploit front-run its own fix on-chain. Frontier models now assemble exploit chains whose timing and vector interleaving no human reviewer would ever have found. Point that capability at the spec and silicon of our crumbling x86 architecture and tell me you want the compiler underneath it to be a binary somebody emailed you.
Two holes, two fixes
The honest framing is that there are two separate holes, and they need two separate fixes.
The compiler hole is what the nixpkgs PR closes: bootstrap the toolchain from source so there is no unreproduced brick at the foundation. That is necessary, and it is unglamorous, and it is exactly the kind of work that gets dismissed as not-worth-the-build-minutes.
The distribution hole is the one that remains even after you bootstrap: who signs the result, who agrees it’s correct, and how it reaches you. This is where I stopped arguing and started building. The system I’ve been building (publicly unnamed until now — it’s called tixim) takes the opposite bet from “one farm, one key” at every layer:
ONE FARM, ONE KEY REPRODUCIBILITY AS CONSENSUS
┌──────────────────────────────┐ ┌──────────────────────────────────┐
│ prebuilt compiler, unaudited │ │ full-source bootstrap; the seed │
│ seed │ │ is auditable by a human │
├──────────────────────────────┤ ├──────────────────────────────────┤
│ one CI farm builds it │ │ N independent, staked parties │
│ │ │ each rebuild it; the result is │
│ │ │ accepted only when their digests │
│ │ │ CONVERGE — reproducibility is the │
│ │ │ vote, disagreement forfeits stake │
├──────────────────────────────┤ ├──────────────────────────────────┤
│ one long-lived key signs it │ │ threshold MPC signature — the key │
│ (cache.nixos.org-1) │ │ exists only in shares; no single │
│ │ │ party ever holds it whole │
├──────────────────────────────┤ ├──────────────────────────────────┤
│ trust the infra, the access │ │ provenance verifiable OFFLINE; │
│ controls, and the key holder │ │ distribution replayed from an │
│ │ │ append-only, auditable log │
└──────────────────────────────┘ └──────────────────────────────────┘
Concretely, on the right: the artifact is signed by a threshold MPC “dWallet” whose private key is never assembled in one place; a build is attested only when several independently-staked parties rebuild it and their result digests agree, so a single compromised builder can’t certify anything; the provenance is a signed attestation you can verify offline; and the binary cache is a decentralised store whose history is replayed from an append-only log, not a single mutable server behind a single key. No single farm. No single key. No single point whose compromise ends the game. That was the “over-engineered” idea.
This is why it stings to watch the sharpest people in decentralised systems — Mysten Labs among them — fund StageX because it is fully bootstrapped, while the technically superior Nix model gets the same treatment my PR did. The bootstrapping instinct is correct. The dismissal of where it needs to go next is the sad part.
An aside on how this gets built
I don’t vibe-code, and I want to be precise about that, because “an AI wrote it” is used as a slur in exactly the threads where the argument has otherwise run out. I use agentic engineering to build complex subsystems, and the process is mostly me: hours of design with a research model to land the algorithms first, then a spec-driven run — specify, clarify, plan, analyse, tasks — before a line is implemented. Then it runs:
design doc ──► specify ─► clarify ─► plan ─► analyse ─► tasks
│
one worker implements each work-package
in an isolated worktree │
▼
a second model reviews ──► fix loop ──► lands
│
landing branch reviewed by a PANEL: gpt · opus · kimi · glm · qwen
findings pooled, fix-workers spawned,
each fix checked back against the finding
│
▼
human gate (full diffs + logs) ──► merge
If three or four frontier models reviewing in parallel don’t catch a bug, honestly, neither will I. What I do catch is the thing they can’t: an ugly interface, a workaround pretending to be a fix, logic sitting in the wrong layer. Taste. I spend my time designing and reviewing, not typing implementation — and the result is that I can maintain a system that would otherwise need a funded team.
Done was not done
For the record, I need to retract the line that originally occupied this spot.
I wrote that the ordinary change-management workflow had upgraded mrustc
from Rust 1.90 to 1.97 in a little over three hours.
The screen really did say COMPLETED: 3 hours 4 minutes, 90 steps, no failed
steps. But the finalizer had pruned a no-op implementation branch. The source
tree still contained the flake scaffolding it started with and none of the
claimed compiler frontiers. I had treated the workflow’s terminal state as
evidence that the requested outcome existed. That was wrong.
What followed was less flattering and much more useful: missing planner artifacts, recovery state that could not be resumed, container specs without a mount, nested templates that crossed an engine boundary unrendered, worker environments that lost their Nix paths, and a per-user runtime filesystem filled by stale rootless bundles. Some were workflow bugs. Some were engine bugs. Several fixes exposed the next one.
The correction was architectural. A compiler ascent is not a normal feature branch. It is a chain of expensive, stateful transitions, so it now has a specialized workflow: develop one frontier in a persistent environment, prove it, package it once, root the result, and let a parent workflow dispatch the next frontier as a subgraph.
That workflow reached Rust 1.97.1 — the current stable point release — on 26 July. Seven successful frontier runs averaged 15 minutes 33 seconds. The final composed parent advanced the rooted 1.93.1 checkpoint through 1.97.1 in 1 hour 5 minutes.
Tixim is not public yet, and this is not an unattended public update service.
When Rust releases, either I trigger the private workflow or one of my
vx-maintainer instances does. The important change is that the expertise now
persists outside any one agent session.
The complete engineering trace is in “The workflow was the program”. The short version of what runs next is “The 16-minute compiler update”.
The fix is here whether the reflex likes it or not. Bootstrap the compiler. Split the key. Make reproducibility a vote instead of a promise. The old architecture — one farm, one key, trust us — made sense when there was no other option. There is now.