Bootstrapsimplified

Reflections on Trusting Trust

Ken Thompson's 1984 Turing Award lecture: source code alone does not capture every trust assumption in a toolchain, because the compiler that builds the compiler can carry behavior that appears in no source anywhere. Diverse double-compiling is the known countermeasure.

The question

If I read all the source, do I know what the binary does?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The toolchain as a chain of binaries, each produced by the previous one, of which you have read the *source* of some and the *behavior* of none. The gap between those two things is the representation this lesson is about: source is a description of intended behavior, a binary is the behavior, and the function connecting them is another binary you did not read either.

What this phase may assume or do

Reading source establishes what the source says. Concluding anything about a binary from that reading requires an additional premise — that the compiler which produced it implements the source faithfully — and that premise is itself a claim about a binary. The argument is valid only if the premise is established independently of the artifact under examination, which is precisely what a self-consistent bootstrap chain cannot do and what diverse double-compiling can, given a second compiler whose own provenance is independent.

Key points

  • A compiler can be modified to recognise its own source and reinsert the modification, after which the source can be cleaned and every subsequent binary is still affected.
  • Source review establishes properties of source. It says nothing about a binary unless the compiler that produced it is independently established to be faithful.
  • The bootstrap identity check cannot detect this, because both stages are equally affected — a self-consistency check is exactly what a self-reproducing modification preserves.
  • Diverse double-compiling detects it using a second, independent compiler that is itself never trusted; defeating it requires both compilers to carry the same attack.
  • The technique requires deterministic, reproducible compilation, which is why reproducibility and toolchain trust are one engineering programme.
  • The general result is about self-reproducing systems, not compilers specifically, and applies to every binary in a build that was not built from reviewed source.

The argument, in three steps

simplifiedThompson's lecture describes the construction at a conceptual level and he did not publish a working implementation of the full self-reproducing version, though he confirmed a version existed. The point of the argument is what it establishes about trust, not a recipe: treated as an attack it is elaborate, targeted and largely superseded by easier supply-chain compromises, and treated as an epistemological result about what source review can establish it remains exactly as strong as it was in 1984.

Thompson built the argument in stages in his 1984 Turing Award lecture, and it is worth following the structure because each step is individually unremarkable and the conclusion is not.

Step one: a program can print itself. A quine is a program whose output is its own source. This is a well-known curiosity and establishes only that self-reference is mechanically possible.

Step two: a compiler can recognise a pattern and behave specially. Thompson's example was the C compiler learning to compile the escape \n — a fact about the character set that has to be hard-coded once, because the compiler's own source cannot use an escape the compiler does not yet understand. Once you accept that a compiler contains knowledge not derivable from the program it is compiling, the door is open: a compiler could recognise, say, the login program and insert an extra accepted password.

Step three: the compiler can recognise itself. Now teach the compiler to recognise that it is compiling *the compiler*, and in that case to insert both the login backdoor and this self-recognition logic into the output. Compile the compiler once with this modified version. Then delete the modification from the source entirely. The next compiler, built from clean source by the infected binary, contains the backdoor. So does the one after it. The source is clean, every subsequent source is clean, and every binary is compromised.

The result is not a trick about compilers specifically. It is a general observation: a system that reproduces itself can carry behavior that exists in no source anywhere, and no amount of source review will find it, because there is nothing in any source to find.

How the attack survives the source being cleanedsimplified
  1. Modified compiler sourceyou write it
    Compiler source containing both the payload and the self-recognition logic.
    The attack, visibly, in source that could be reviewed.
  2. Infected compiler binarybuild time
    A compiler binary that recognises both the target program and its own source.
    The behavior. From here the source is no longer needed.
  3. Source restoredyou write it
    Completely clean compiler source, with no trace of the modification.
    Every reviewable artifact of the attack. There is now nothing in any source to find.
  4. Next compiler buildbuild time
    A new compiler binary, built from clean source by the infected binary.
    The attack again, reinserted by the compiler that compiled it.
  5. Every subsequent buildbuild time
    Clean source, infected binary, indefinitely.
    A self-sustaining property of the chain rather than of any artifact you can read.

Read it asThe third row is the whole lecture. After that point, source review is looking for something that is not there — the attack lives in the relationship between the binaries, not in any file. Note also that this is exactly the fixed point [[self-hosting]] verifies: the bootstrap comparison passes, because stage 2 and stage 3 are equally infected.

What it actually establishes

The conclusion Thompson drew is the one worth carrying: "You can't trust code that you did not totally create yourself." Not "compilers are dangerous" — the compiler is an illustration of a general property that applies to every self-reproducing tool in a chain, and the chain includes the assembler, the linker, the libc, the kernel and the hardware.

It is easy to over-read this as fatalism, and it is more useful read as a statement about *where* trust lives. Source review establishes properties of source. Trusting a binary requires either building it yourself with a toolchain you trust — which regresses — or establishing the correspondence between source and binary by some means that does not rely on the artifact under examination.

It is equally easy to under-read it. "Nobody would go to that trouble" is not a rebuttal; it is a risk assessment, and it has been wrong before about other supply-chain attacks that were also considered too elaborate. The XZ backdoor of 2024 was a multi-year social engineering effort culminating in a payload hidden in test fixtures, which is a different technique with the same structural insight: put the attack where nobody is reading.

The practical relevance today is less about someone backdooring GCC and more about the reasoning pattern. Every time a build downloads a binary — a stage 0 compiler, a container base image, a language server, a pre-built dependency — the same gap opens between what was reviewed and what runs. [[toolchain-trust]] enumerates how wide that gap is in an ordinary build.

Diverse double-compiling

specWheeler's result is a proof under stated assumptions, not a heuristic. The assumptions are the ones that matter: compilation must be deterministic, the environment must not itself be subverting the comparison, and B must be genuinely independent of A. Two compilers sharing a backend, a build environment or a maintainer are not independent in the sense the argument needs.

David A. Wheeler's 2005 work — later his doctoral dissertation — is the known countermeasure, and it is a genuinely clever piece of reasoning rather than a mitigation. It establishes, under stated assumptions, that a compiler binary corresponds to its source, using a *second, independent* compiler that is itself allowed to be untrusted.

The technique: let A be the compiler binary you want to check and S be its source. Let B be any other compiler for the same language, from an independent lineage — a different vendor, a different implementation, ideally a different heritage entirely. Compile S with B, producing X. X is probably a poor compiler — slow, badly optimized — but if B was honest it implements what S says. Now compile S with X, producing Y. And compile S with A, producing Z.

If A is faithful to S, then A and X implement the same source and should produce identical output from identical input, so Y and Z must be byte-identical. If A contains a self-reproducing modification not in S, it inserts that modification into Z and X does not insert it into Y, so they differ. The check succeeds unless *both* A and B contain the same attack — which, for genuinely independent compilers, is a much stronger assumption than either being compromised alone.

The catch is the prerequisite, and it is the reason this connects directly to the next lesson: the comparison requires that compilation be deterministic and that the compiler be reproducible, or "byte-identical" has no meaning. Wheeler's dissertation makes this precondition explicit, and it is why the reproducible-builds movement and the trusting-trust problem are the same engineering programme viewed from two directions.

Diverse double-compiling, as a check
1Given: A = the compiler binary under examination
2 S = its source code
3 B = an independent compiler for the same language
4
5 X = compile(B, S) # S built by the other compiler. Poor, but honest
6 # if B is honest.
7 Y = compile(X, S) # S built by X.
8 Z = compile(A, S) # S built by the compiler we are checking.
9
10 assert Y == Z # byte-identical
11
12If A is faithful to S, then A and X implement the same language and
13must agree on this input, so Y == Z.
14
15If A carries a modification absent from S, it injects it into Z.
16X does not inject it into Y. The comparison fails.
17
18Defeating this requires BOTH A and B to carry the same attack --
19which is why the diversity of B is the whole security argument.
20
21Prerequisite: compilation must be deterministic, or "byte-identical"
22means nothing. This is not a detail; it is the load-bearing assumption.

Note the elegance: B is never trusted. It only has to be independently compromised or not compromised at all, and the argument goes through in either case unless the two attacks coincide. That is what makes this a verification technique rather than a chain of trust — it converts "trust this compiler" into "trust that two unrelated compilers are not carrying the identical backdoor".

What to actually do about it

For almost every engineer the answer is not to run diverse double-compiling. It is to understand what the argument implies about ordinary practice, and to prefer the practices that narrow the gap it identifies.

Reproducible builds are the most valuable one, and they are the direct descendant of this argument: if many independent parties build the same source and get byte-identical output, then a compromise of any one builder is detectable. That is a practical, deployed version of the same reasoning, and it is what Debian, Arch, F-Droid and the Reproducible Builds project have spent a decade on — see [[reproducible-compilation]].

Bootstrappability is the second, and reduces the size of what has to be trusted rather than verifying it. Shrinking the unauditable seed from "a compiler binary somebody built in 2019" to "a few hundred bytes of hex a human could read" is a real reduction in exposure even though it does not eliminate the problem.

And the third is simply to know how large the trusted base actually is, which is the subject of [[toolchain-trust]] and is usually much larger than people expect.

  • Reproducible builds: many independent builders, byte-identical output, so a single compromised builder is detectable.
  • Bootstrappability: shrink the unauditable seed until it is small enough to actually read.
  • Diverse double-compiling where it matters: a real verification, expensive, and dependent on genuine independence.
  • Signed artifacts and provenance attestation, which establish *who* built something rather than *what* it does — a weaker but far cheaper property.
  • Knowing the size of the trusted computing base, because a mitigation aimed only at the compiler leaves the assembler, the linker, the libc and the build system untouched.

How it works

The steps, in the order the compiler takes them.

  • A modification is added to the compiler's source: recognise a target program and alter its compilation, and recognise the compiler's own source and reinsert both behaviors.
  • The modified source is compiled once, producing an infected compiler binary.
  • The modification is removed from the source, leaving nothing reviewable.
  • The infected binary compiles the clean source, and reinserts the modification into the resulting binary.
  • Every subsequent build repeats this, so the property persists in the chain of binaries with no representation in any source file.
  • Diverse double-compiling breaks the chain by compiling the source with an independent compiler and comparing the two results, which requires deterministic compilation to be meaningful.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A security review reads the entire compiler source, finds nothing, and concludes the toolchain is clean — a valid finding about the source and an invalid conclusion about the binaries.
  • A build downloads a stage 0 compiler over TLS, verifies its checksum against a value published by the same party that built it, and treats that as provenance rather than integrity.
  • A bootstrap comparison passes and is reported as evidence of toolchain integrity, when it is a self-consistency check that this attack is specifically designed to survive.
  • A team invests in compiler verification and continues to run an unaudited assembler, linker, libc and build system, leaving the trusted base essentially unchanged.
  • A reproducible-build effort is undermined by a single non-deterministic input — a timestamp, a build path — so no two builders ever agree and the whole detection mechanism produces noise instead of signal.

When it helps

  • Reasoning correctly about what a source audit establishes, which is a distinction that matters far beyond compilers.
  • Evaluating a toolchain's supply-chain posture: "can this be built from source, and would two builders agree" are the right questions and this argument is why.
  • Understanding why the reproducible-builds and bootstrappable-builds efforts exist, which otherwise look like disproportionate effort for a theoretical risk.

When it hurts

  • As a reason for paralysis. The correct response is to narrow the gap with reproducibility and bootstrappability, not to conclude that nothing can be trusted and stop.
  • As a threat model on its own. For nearly every organisation, a compromised dependency or a leaked credential is a far more probable path than a compiler backdoor, and effort should be allocated accordingly.

What it costs

Every one of these is paid by something.

  • Diverse double-compiling buys a real correspondence check between a binary and its source, and costs a genuinely independent second compiler for the same language plus a fully deterministic build — both of which are substantial and sometimes unavailable.
  • Reproducible builds buy detection of a single compromised builder by many parties, and cost the systematic elimination of every source of non-determinism across the compiler, the build system and every dependency.
  • Bootstrappability buys a small, readable trust root and costs a maintained chain of increasingly capable compilers plus a language team constrained to features the chain already supports.
  • Accepting a downloaded binary toolchain buys enormous convenience and costs exactly the property this lecture is about — and is what nearly everyone chooses, usually without noticing there was a choice.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Reproducible builds with multiple independent builders, which detects a compromised build without ever verifying a compiler — cheaper, deployed at scale, and the practical descendant of this argument.
  • Bootstrappable builds, which reduce rather than verify: shrink the unauditable seed until a person could read it.
  • Formal verification of the compiler, as CompCert does, which addresses correctness rather than the source-to-binary correspondence — a proof about the source is still a proof about the source. See [[verified-compilers]].
  • Binary analysis and decompilation of the shipped compiler, which is exactly the "read the binary" step the lecture says would be needed, and is impractical at compiler scale.
  • Provenance attestation and signed artifacts — SLSA, sigstore — which establish who built something and from what inputs, a weaker property than correspondence and far cheaper to obtain.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Read the source: Thompson's "Reflections on Trusting Trust", 1984 Turing Award lecture, is four pages and is the clearest statement of the argument.
  • Read the countermeasure: David A. Wheeler's "Countering Trusting Trust through Diverse Double-Compiling" and his dissertation give the proof and the assumptions it needs.
  • See the practical descendant: the Reproducible Builds project publishes tooling (diffoscope, reprotest) and per-distribution status pages showing which packages currently rebuild identically.
  • See the bootstrap-shrinking effort: the Bootstrappable Builds project documents a chain from a few hundred bytes of hex up to a full toolchain.
  • Test the premise locally: build a package twice on the same machine and run diffoscope on the two outputs. Most projects fail this immediately, which is the state of the art being described.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "This means compilers are backdoored." It means source review cannot rule it out. Those are very different claims and only the second is being made.
  • "Open source solves this." Open source makes the source reviewable. The attack is specifically constructed to leave nothing in the source, and the binary that most people run was built by someone else.
  • "Checksums prevent it." A checksum proves you received the file the publisher published. It says nothing about what that file does or what built it.
  • "The bootstrap comparison would catch it." It cannot. Both stages carry the modification, so they agree, which is exactly what the check tests for.

Misconceptions

The claim, and what is actually true.

Reading the source tells you what the program does.
It tells you what the source says. What runs is a binary produced by another binary, and connecting the two requires a premise the source cannot supply.
This is a theoretical problem with no practical relevance.
The specific attack is elaborate and rare; the reasoning applies directly to every downloaded binary in a modern build, which is most of them.
Diverse double-compiling requires trusting the second compiler.
It does not. The second compiler may be compromised; the check fails only if both carry the same attack, which is what independence is for.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

Ken Thompson pointed out that a compiler could be altered to insert a backdoor, and also to insert that alteration whenever it compiles a compiler. Do it once, then delete the change from the source, and every future compiler built from clean source is still affected — with nothing left anywhere for a reviewer to find. The lesson is that reading source tells you about source, and the program that runs is a binary somebody else produced.

practical

You are not going to run diverse double-compiling. What you can do is prefer toolchains that can be built from source, check whether your builds are reproducible (diffoscope on two builds of the same source is a five-minute experiment that most projects fail), and be precise in reviews about what a source audit established. And when someone offers a checksum as provenance, notice that it proves you got the file the publisher sent and nothing about what produced it.

advanced

The reason this argument has aged so well is that it is not really about compilers — it is about the difference between a description and a thing, in a system where things produce things. Any self-reproducing process can carry properties absent from every description of it, and the compiler is simply the cleanest example because the description and the product are so obviously separate artifacts. The same structure appears in a container image that builds container images, in a CI system that deploys itself, in a package manager installed by a package, and in a model trained on data produced by a model. In each case the tempting verification — check that the thing agrees with itself — is exactly the check the failure survives, and the only escape is an independently produced second opinion. Wheeler's contribution is not really a compiler technique; it is the observation that you can get that second opinion from something you do not trust, provided it is independent, which is a considerably cheaper requirement than trustworthiness.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

simplifiedThe three-step construction is presented at the level Thompson presented it, as an argument rather than an implementation. A working version must handle the compiler recognising a source that has been edited, refactored or reformatted, which is substantially harder than the sketch suggests and is one reason this is discussed more often than it is attempted.
specWheeler's diverse double-compiling result is a proof under explicit assumptions: deterministic compilation, a trustworthy comparison environment, and genuine independence between the two compilers. Each assumption is load-bearing, and two compilers sharing a backend, a build environment or a maintainer do not satisfy the third.
typicalThat most builds today download a binary toolchain rather than bootstrapping from an auditable seed is typical of current practice across essentially every language ecosystem. The reproducible-builds and bootstrappable-builds efforts are attempts to change that and have made real progress in some distributions and none in most ecosystems.

If you were asked this in an interview

  • Explain the trusting-trust attack, and say why reading the compiler's source cannot detect it.
  • Why does a three-stage bootstrap comparison fail to catch it?
  • How does diverse double-compiling work, and why does it not require trusting the second compiler?

Connections

Domains that do not exist yet
  • DevOps / Production Engineering — Build provenance, attestation and multi-party verification of artifacts
    The practical response to this argument is operational rather than compiler-technical: independent rebuilds, attested provenance and reproducible outputs. Designing and running that infrastructure is owned there, and this lesson is the reason it is worth running.