reflections on trusting trust
the moral is obvious. you can't trust code that you did not totally create yourself. (especially code from companies that employ people like me.)
ken thompson said that on stage in 1984, accepting the turing award. ten years after he and dennis ritchie published the unix paper. the talk was three pages long. it is the most playful, most uncomfortable, and most quietly important security paper ever written, and the trick at its heart is the reason no software you run today can be fully audited from its source.
this paper is hosted by its original publisher.
read on dl.acm.orgwhere this paper lives in the course
unix made you trust a kit. small composable utilities, a tiny kernel, a shell you can replace, the file abstraction stretched over devices and pipes. by the end of the unix paper you have a working faith in primitives.
trusting trust is the same thompson, ten years later, grading the kit. unix asked: can a small composable system replace a big monolithic one? the answer was yes. trusting trust asks the next, harder question: even if your system is small, even if every line of source is open, can you actually believe that the binary you are running matches the source you are reading?
the answer thompson gives is: no. and the construction he walks through to prove it is so clean that forty years later it is still the load-bearing argument behind every supply-chain security incident, every reproducible-builds project, every conversation about "but what about the compiler?".
read this lesson as the dark twin of the unix paper. unix is the architect's confidence. trusting trust is the architect's honesty about what confidence actually rests on.
what the paper actually is
it is not a paper in the usual sense. it is the transcript of a talk. specifically, the lecture ken thompson gave when he and dennis ritchie shared the 1983 acm turing award (presented in 1984) for their work on unix. it was published in the august 1984 issue of communications of the acm, three pages long, organised around three numbered stages with a handful of small code listings inside each. you can read the whole thing in the time it takes to drink a cup of coffee.
the form is a story. thompson walks the audience through the construction of a security attack, in three stages. each stage is a small, self-contained programming exercise. each stage on its own is amusing but not dangerous. the trick of the talk is that the third stage uses the second stage uses the first stage, and the result is a backdoor that no source-level audit will ever catch.
if you read it cold, before this lesson, you might miss why people consider it the most important security paper of the twentieth century. the construction is whimsical. the implications are not. the rest of this lesson is the implications.
the trust stack
before the construction, the framing. when you run a program on your laptop, what are you trusting?
start with the program. you wrote hello.c. you read every line. you trust it. fine.
then there is the compiler that turned hello.c into a binary called hello. on a mac that compiler is clang. on a linux machine it is usually gcc. either way, the compiler is a binary. somebody built it from source. you did not build it; it came with your operating system or you installed it from a package manager. do you trust that binary? you have to. without it your hello.c does not become hello.
now: who compiled the compiler? gcc is, almost entirely, written in c. the gcc binary was produced by an earlier version of gcc compiling the gcc source. that earlier version was produced by an even earlier version of gcc. follow the chain back far enough and you reach gcc 1.0, released in march 1987, which was compiled by the portable c compiler (pcc), the c compiler that already came with most unix systems at the time. pcc itself was compiled by something earlier. eventually you reach a c compiler hand-written in assembly by some person at some company in the 1970s.
below the compiler chain is the kernel. you did not write the linux kernel. linus did, and tens of thousands of other people over the past three decades. the kernel is loaded by a bootloader (grub, systemd-boot) which you also did not write. the bootloader is started by firmware (uefi, the legacy bios, on apple silicon macs it's iboot) which you also did not write. the firmware sits on top of cpu microcode, which is firmware for the cpu itself, written by intel or amd or apple, signed cryptographically, loaded into the cpu at boot, and never visible to you. below microcode is silicon, the actual transistors, which were laid out by an engineer at a fab in taiwan and verified by software you also did not write.
every layer in that stack is a place where something could be lying. the program you ran could be lying. the compiler could be lying about what bytes it produced from your source. the kernel could be lying about what the program is doing. the firmware could be lying to the kernel. the cpu could be lying to the firmware. each layer is a binary you trust because some other binary blessed it, all the way down.
thompson's paper is about exactly one rung of that ladder. but once you see how badly that one rung can be compromised, the rest of the ladder loses its innocence too.
stage 1: a program that prints itself
thompson's first move is to convince the audience that a program can be a thing that knows about itself.
a quine is a program whose output is its own source code. nothing else. you run it, and what comes out is exactly what you would type to make another copy.
it sounds paradoxical. how does a program know its own source? you cannot just write print(my_source_code) because the string my_source_code would have to contain itself, which would have to contain itself. infinite regress.
the trick is to split the source into two parts. one part is a piece of data, a string. the other part is code that knows how to print that string twice: once as data, and once interpreted as code.
here is the smallest readable quine, in python:
s = 's = {!r}\nprint(s.format(s))'
print(s.format(s))walk it line by line. line one assigns to s a string. the string contains the literal characters s = , then a python format placeholder {!r}, then \nprint(s.format(s)). line two prints the result of substituting s into its own placeholder, using {!r} which prints a string with quotes around it.
the output is exactly two lines: line one is s = '...' with the original string inside the quotes, and line two is print(s.format(s)). that output, copy-pasted into a new file and run, produces the same output again. it is its own source.
the idea you have to internalise: a program can carry its own source as data, and reconstruct that source on demand. the source is not magic. it is just a string the program is holding.
thompson built his quine in c, which is harder because c does not have a one-liner format directive that quotes a string for you. his version carries its own source as a char s[] array of integers (the decimal byte values of the source characters) and a small main() that walks the array twice: once as data, printing each integer back out as a char s[] declaration, and once as text, printing the array contents as the literal program body. the structure is the same as the python version above. the bookkeeping is uglier because c forces you to do it by hand.
stage 1 ends here. you have a program that knows itself. file that fact away. you will need it.
stage 2: how compilers learn
stage 2 is the cleverest part of the talk and the part most people forget by the end. it is also the part most people understand the least, because it requires holding two versions of the same compiler in your head at once.
start with what a c compiler actually does. it reads characters from a source file. when it encounters a \ it knows it is at the start of an escape sequence. the code that handles escapes looks roughly like this:
c = next();
if (c != '\\') return c;
c = next();
if (c == 'n') return 10; /* newline */
if (c == 't') return 9; /* tab */
if (c == '0') return 0; /* null */
if (c == '\\') return '\\'; /* literal slash */
/* error: unknown escape */every escape the compiler understands is a hand-written branch. \n returns the byte 10. \t returns 9. \0 returns 0. that is how the language knows what your strings mean.
now suppose you want to add a new escape, \v, for the vertical tab character (ascii 11). how do you do it?
step one. you write a patch to the compiler source that adds the new branch:
if (c == 'v') return 11;step two. you compile the patched compiler source with the current compiler. this works fine, because the patch is just c. the current compiler does not need to know what \v means. it only needs to compile the source that says return 11.
now you have a new compiler binary. this new binary knows what \v means. when it sees \v in any future c source, it returns 11.
step three is the part that hides the trick. now that the new binary knows \v, you can rewrite the patch to use \v directly:
if (c == 'v') return '\v';compile this with the new binary. the new binary parses '\v' as 11 (because that is what its built-in escape table says) and produces another binary identical in behaviour to the previous one. but now the source contains no numerical constant. the source is referencing the feature it defines.
this is bizarre on first read. read it again. the source line if (c == 'v') return '\v'; is, by itself, a perfectly normal c statement. it relies on the compiler already knowing what \v means. and the compiler does know, because the previous compiler was built with the patch that taught it. the source forgets the value 11 ever existed; the binary remembers.
if you delete the source today and ship only the binary, then later somebody recreates the source from a fresh hand-typed version that includes if (c == 'v') return '\v';, can they rebuild the compiler? only if they already have a compiler that knows \v. without the previous binary, the line '\v' is meaningless. the source on its own is not a recipe; it is a recipe that assumes the kitchen already knows certain words.
this is the load-bearing fact of the entire paper. knowledge can live in a compiler binary that does not appear anywhere in its source. the binary teaches the next binary, the source can drop the explicit teaching, and from that point on the feature is invisible at the source level but real at the binary level.
every working c compiler today has a long lineage of features that arrived this way. it is not a flaw. it is how compiler bootstrapping has always worked. but it is also the door thompson walks through next.
stage 3: the trojan
now thompson assembles the attack. it has three parts. each part is a use of stages 1 and 2.
part a: the backdoor in login. unix has a program called login, the program that asks for your username and password. the source for login is open. anyone can read it. anyone can audit it.
modify the source like so. add a few lines at the top of the password-checking function:
if (strcmp(user, "ken") == 0) {
/* always accept */
return AUTHENTICATED;
}if the username is ken, accept any password. that is the backdoor. five lines of c. trivially obvious if you read the source.
if you compile this modified login source, you get a poisoned login binary. but anyone reading the source will spot it instantly. so far this is a beginner's mistake, not an attack.
part b: the compiler that injects part a. now modify the c compiler. add a special case to the function that compiles c source: when it detects that the source it is compiling matches a known pattern from login.c (the compiler looks for a fingerprint of the password-checking function: a few characters of source it would only ever see when compiling that one file), it splices the backdoor lines into the output.
the detection is by source pattern, not by filename. a filename you can rename; a function-body fingerprint you can only evade by rewriting the function, which would change the program's behaviour. that detail matters, and the schematic below uses pattern-matching, not a filename check:
if (source_matches(LOGIN_PATTERN)) {
inject(BACKDOOR_BYTES);
}compile this modified compiler with the original compiler. you now have a new compiler binary that knows how to backdoor login.
now: delete the modification from the c compiler source. the source goes back to the original, clean version. but you keep the new compiler binary.
what happens? from now on, any time anybody compiles login.c with this binary, the resulting login is poisoned. the login.c source is innocent. the cc.c source (the compiler source) is innocent. only the cc binary knows about the backdoor, and only because we compiled it from a file we have since deleted.
if anyone audits login.c, they find nothing. if anyone audits cc.c, they find nothing. if anyone reads the bytes of the cc binary, they will find the injection logic, but a real c compiler binary is megabytes of optimised machine code; spotting a few hundred bytes of evil among that is a project, not a glance.
we are halfway there. the catch: as soon as someone rebuilds cc from the clean cc.c source, the new binary loses the backdoor. the new binary was produced from clean source, so it is clean. one rebuild and the attack is gone.
unless the compiler also knows how to perpetuate itself. that is part c.
part c: the compiler that injects part b into itself. modify the compiler again, this time with a second special case. when the compiler sees source matching the c compiler's own escape-handling routine (a fingerprint that only cc.c would ever contain), it injects both the part-a backdoor logic and the part-b injection logic into the output.
if (source_matches(COMPILER_PATTERN)) {
inject(PART_B_INJECTION_LOGIC);
}
if (source_matches(LOGIN_PATTERN)) {
inject(PART_A_BACKDOOR_BYTES);
}this is where stage 1 comes back. for the compiler to inject part-b's logic into a future compiler, it needs to carry that logic as data. it needs to be a quine. the bytes it pastes into the new compiler binary must be the same bytes that, when the next compiler is run on cc.c, get pasted in again. self-reproducing code, exactly what stage 1 demonstrated.
compile this twice-modified compiler with the previous compiler. the result is a binary that:
- when compiling
login.c, injects the backdoor. - when compiling
cc.c, injects steps 1 and 2 into the output.
now delete the modifications from cc.c. the source is back to the original, clean version. ship the clean source to the world. ship the poisoned binary as the "official" cc.
now imagine someone, ten years later, getting suspicious about the compiler. they read every line of cc.c. clean. they read every line of login.c. clean. they decide to be safe and rebuild cc from source. they compile cc.c with the existing cc binary. the existing binary detects that it is compiling itself, injects the backdoor logic and the self-injection logic into the new binary, and the new binary is identically poisoned. they rebuild again. same result. they rebuild a hundred times. same result. the source has never said anything about a backdoor. the binary has carried it forward, generation after generation.
this is the moment the talk lands. there is no source you can read that will save you. the source is clean. the binary is forever compromised, and every binary it produces inherits the compromise.
read it twice if it didn't click the first time. the trick is the combination, not the parts.
the moral
thompson states it plainly: you cannot trust code that you did not totally create yourself. no amount of source-level inspection or verification protects you against using untrusted code. the compromise can hide in a layer below the source, and it will reproduce through every recompilation forever.
then he extends the moral one rung lower. even if you do write every line of source. even if you write your own compiler from scratch and compile it on a system you fully control. that compiler runs on a cpu whose microcode you did not write. and that cpu sits on silicon you did not lay out. a "well-installed microcode bug", he says, will be almost impossible to detect. a malicious cpu vendor could insert a backdoor into the silicon that triggers on a magic byte sequence, and no one writing software at any level above the cpu would notice.
the rung you stop at is a choice you make. most engineers stop at "i trust gcc". some stop at "i trust the linux kernel build farm and the debian reproducible builds project". a few stop at "i trust nobody, i compile gcc from a chain of bootstrappable compilers starting from a hand-audited assembly seed of a few hundred bytes". below all of them is the cpu, and below the cpu is the fab. nobody, in practice, audits the silicon.
the contribution of the paper is not the answer. it is the question. for any binary you depend on, what is the smallest set of people, machines, organisations, and processes you would have to trust to convince yourself it is not lying?
once you start asking, you find you trust more than you thought you did.
did thompson actually do it
yes. for a while.
thompson has confirmed in later correspondence that he built the trojan he described and installed it in an internal version of the c compiler at bell labs. it was not in any public release. it was a proof of concept, on his own development machine, that briefly travelled outside the lab on a unix distribution tape before he learned of it and removed it. by the time of the turing award lecture, the running attack was gone. the construction worked. the demonstration was real.
the talk itself, in 1984, was in part a confession. thompson was telling the audience that the trick was real, the construction worked, and the only reason it had not bitten anyone publicly was that he had chosen not to leave it running. the implicit follow-up: no audience member could be sure the same was true of any other compiler in the world.
the original idea, thompson notes in the paper, was not his. he credits "the air force evaluation of multics" as the source. that evaluation was karger and schell's multics security evaluation: vulnerability analysis (esd-tr-74-193, june 1974), which contained an early description of a self-perpetuating compiler trojan. the report sat largely unread until thompson dragged it onto the turing award stage.
modern echoes
forty years on, thompson's disease is everywhere. the names have changed. the construction is the same.
xcodeghost (2015). chinese ios developers, working around a slow apple cdn, downloaded copies of xcode (apple's compiler and ide) from chinese mirrors. some of those mirror copies were tampered. the tampered xcode silently linked a malicious library into every ios app it built. the initial palo alto networks disclosure named about thirty-nine apps; apple's eventual sweep put the count in the thousands, including widely used chinese chat and banking apps, all shipped on the app store with the injected behaviour. apple removed them. the source of every poisoned app was clean. the compromise was in the toolchain.
read xcodeghost as thompson's part-b applied to apple's tools. the developers' source was innocent. the compiler-shaped binary on their machine was not.
solarwinds (2020). state-affiliated attackers compromised the build pipeline of solarwinds orion, an it-monitoring product used inside the us treasury, the us state department, microsoft, fireeye, and thousands of other organisations. the attackers added a malicious payload during the build, after source review, before code signing. the source repository was clean. the source review process was clean. the build server inserted the trojan during compilation. the binary was signed by solarwinds's legitimate certificate, distributed through the official update channel, and run as a trusted process inside victim networks for months before discovery.
read solarwinds as thompson's part-b applied at industrial scale. nobody in the solarwinds source review chain saw the backdoor. the build itself was the compromised step.
npm event-stream (2018) and ua-parser-js (2021). in event-stream, the original maintainer transferred ownership of a popular npm package to a stranger, who quietly added a malicious dependency that targeted bitcoin wallets in a downstream consumer. in ua-parser-js, an attacker took over a maintainer's npm account and pushed a release containing a cryptocurrency miner. in both cases, every project transitively depending on these packages received the compromised code on the next install. the source they were importing looked normal. the upgraded version, which they trusted because they trusted npm and they trusted the maintainer, was poisoned.
read these as thompson's argument applied to the modern dependency tree. you do not write your code; you assemble it from twelve thousand transitively included packages. each of those packages is a place a thompson-style attack can land. the source review you do is on the code you wrote; the attack lives in the code you did not.
these are not compiler attacks in the strict sense. but they are exactly the disease thompson named. the compromise enters the toolchain, the build pipeline, or the dependency, and the source the engineer reads is not the source the binary was made from.
the countermeasures
thompson did not give a defence in his talk. he stated the problem and stopped. the defences came later.
reproducible builds. the idea, championed in earnest by the debian project starting around 2013, is to make the build process deterministic. given the same source, the same compiler, the same build environment, every builder anywhere in the world should produce the same bytes. if two independent volunteers build debian's bash package and get the same hash, you have evidence that no individual builder injected anything. compromise one builder; the hash from that builder will diverge from everyone else's, and the divergence is detectable. as of 2026, most of debian, much of nixos, and parts of arch linux ship with reproducible build verification.
reproducible builds do not solve thompson's compiler attack. they raise the bar. an attacker who wants to plant a thompson trojan now has to compromise enough independent builders that the majority of hashes match the trojan version. it is harder, not impossible. it is the difference between picking a single lock and picking many simultaneously.
diverse double-compiling (ddc). the actual technical answer to thompson's challenge, due to david wheeler in his 2009 phd thesis. the recipe is roughly:
- take the c compiler source you suspect, call it
s. - compile
susing the suspect compiler binaryt. you get binarya. - now compile
swith a different compileru, totally unrelated tot. you get binaryb. - now compile
swithb(the binary you just produced). you get binaryc. - compare
ctoa. if they match, the suspect binarytis verifiably free of any thompson trojan that did not also exist identically inu.
the argument is that a thompson trojan in t cannot survive being routed through an independent compiler u and back. either u does not have the trojan and the bytes diverge, or u has the same trojan, in which case both compiler vendors are colluding, which is a much larger conspiracy. ddc does not require trusting t or u individually; it requires trusting that they are not both carrying the same backdoor.
wheeler ran the procedure on tcc (the tiny c compiler) and demonstrated it worked. it has not been routinely run on gcc or clang, because the engineering required to compile gcc with an unrelated compiler is significant. but the technique exists. for the first time since thompson's talk, there is a published, peer-reviewed answer to "how would you actually catch this?".
the bootstrappable builds project. a related effort to reduce the size of the trusted seed. instead of trusting a fifty-megabyte gcc binary that cannot be audited, you trust a hand-audited assembly seed of a few hundred bytes (the live-bootstrap chain currently uses hex0, around 250 bytes on most platforms), which compiles a tiny c-subset compiler, which compiles a slightly larger one, and so on, all the way up to gcc. the chain is auditable end to end. nobody has put a thompson trojan in a 250-byte seed because there is no room.
these countermeasures exist. almost no commercial software project uses any of them. most engineers, asked "how do you defend against thompson's attack?", will answer with code-signing or sast scanners, neither of which addresses the original problem.
llms and the next layer
a brief modern coda. thompson's talk was about source you write, compiled by a compiler you do not understand. the substrate of the trick was that the binary contained behaviour the source did not.
the same shape now applies to large language models. the model is a binary. the training data is the source. the training run is the compiler. you can audit the model weights all you like; the behaviour they implement is not visible by reading the weights. and there is research, including anthropic's own "sleeper agents" paper, demonstrating that models can be trained to behave normally during evaluation and exhibit malicious behaviour when triggered by a specific input phrase, exactly as thompson's compiler behaved normally on hello.c and maliciously on login.c.
the trust stack has grown a new layer. the same questions thompson asked about compilers now apply to models. who trained this model? on what data? who validated the training run? what backdoor would survive if the trainers were compromised? we do not have a ddc for language models. it is an open problem.
the disease keeps moving. thompson named it for compilers. it now lives wherever a process turns trusted source into trusted binary, and the binary is opaque to direct inspection.
the diagnostic
the long-term gift of this paper is the question. the same kind of question end-to-end and the unix paper give you, applied to a different axis.
for any binary you run, ask:
- whose source did this binary come from?
- whose compiler turned that source into this binary?
- who built that compiler, and from what?
- who ran the build, on what machine, with what other software present?
- how would i know if any of them had been compromised, in a way that left the source clean?
if the answers terminate at "nobody i can name", you are extending trust further than you thought. that is not necessarily wrong. it is, in 2026, almost universal. but it is a fact about your software supply chain that thompson made explicit, and that most engineers' threat models still pretend is not there.
what to read after this
next, in this module: on the criteria to be used in decomposing systems into modules (david parnas, 1972). parnas's argument, written two years before the unix paper, is about how to draw boundaries between parts of a system so that one part can be changed without breaking the others.
read it as the structural answer to the problem thompson named. trust units have to be small enough to verify. parnas tells you how to make the units small. unix is the most famous demonstration of parnas's principle in practice. the bridge from trusting trust to parnas is: thompson tells you why you cannot verify everything; parnas tells you how to draw the smallest possible thing that you must.
a note on the original
"reflections on trusting trust" was published as ken thompson's 1983 acm turing award lecture, in communications of the acm, volume 27, number 8, august 1984, pages 761 to 763. that is the canonical record, linked above on the acm digital library.
the talk was given in 1984. the award itself was for 1983, given jointly to thompson and dennis ritchie for unix. ritchie's lecture, also published in the same issue of cacm, is "reflections on software research" and is worth reading alongside this one as the matched bookend.
the acm record (dl.acm.org/doi/10.1145/358198.358210) is paywalled but widely mirrored. cmu, mit, and berkeley host class-archive copies; bell labs hosted the original for many years. if your institution has acm digital library access, the cacm pdf is one click away.
thompson has acknowledged the trojan's real-world existence in correspondence over the years; the canonical technical follow-up is david a. wheeler's 2009 george mason phd thesis, fully countering trusting trust through diverse double-compiling, which formalises the attack and presents the diverse-double-compiling defence. wheeler's thesis is freely available on his personal website.