The Toolchain Is Your Trusted Computing Base
Not just the compiler. The preprocessor, assembler, linker, libc, startup objects, build system, every plugin and every downloaded dependency all execute during or inside your build, and a compromise in any of them is a compromise of the output.
What am I actually trusting when I run a build?
The build as a set of programs that execute with your privileges and can influence the artifact, plus the set of files those programs read. That is the trusted computing base, and stating it as an explicit set is the point of the lesson — most engineers can name one element of it and the real set has dozens, several of which were downloaded during the build itself.
An artifact may be said to correspond to a source tree only if every program that ran during the build behaved as documented and every input it read was the intended one. That is a conjunction over a large set, so the correspondence is only as strong as its weakest element — which means adding a build-time code generator, a compiler plugin or a dependency with an install hook weakens the claim about the output, regardless of what the source says.
Key points
- The trusted base of a build includes the preprocessor, compiler, plugins, assembler, linker, startup objects, libc, build system, generators, every dependency, the image and the kernel.
- In several ecosystems, building a project executes code supplied by its dependencies —
build.rs, procedural macros, annotation processors, install scripts. - Adding a dependency in those ecosystems is a grant of code execution on the build machine, before anyone runs the artifact.
- Pinning by content makes the input set known; hermeticity makes it enumerable; reproducibility makes it verifiable by others.
- Each of those properties requires the previous one, and most organisations have the first and claim the last.
- Dependency compromise is far more probable than toolchain compromise, and the same measures address both.
Enumerate it once and the number is startling
build.rs scripts and procedural macros, both of which execute arbitrary code at compile time. No mainstream ecosystem has a small trusted base.Ask what a build trusts and the usual answer is "the compiler". Enumerate it properly and a mundane C++ build trusts, at minimum: the preprocessor, the compiler proper, any compiler plugins or sanitizer runtimes, the assembler, the linker, the archiver, the startup objects from the C runtime, libc itself, the standard library implementation, the build system, every build-time code generator, every downloaded dependency including transitive ones, the package manager that fetched them, the container image the build ran in, the kernel, and the hardware.
Every item on that list either executes during the build or contributes bytes to the output. A compromise in any one of them is a compromise of the artifact, and several of them are things most teams have never inspected, pinned or even enumerated.
The asymmetry with source review is the point. Enormous care goes into reviewing the application's own source, which is a small fraction of what determines the artifact, while the toolchain that determines the rest is typically whatever the base image happened to contain.
| Component | How it can affect the artifact | How often audited |
|---|---|---|
| Preprocessor | Chooses which source is compiled at all, via macros and includes | Effectively never — it is part of the compiler |
| Compiler | Determines all generated code; [[trusting-trust]] applies here | Rarely, and the source is what gets read |
| Compiler plugins and sanitizers | Run inside the compiler with full access to the IR | Rarely, and they are frequently third-party |
| Assembler and linker | Encode instructions, lay out the image, can insert or remove sections | Almost never |
| Startup objects and libc | Contribute code that runs before main and implements every primitive | Almost never; usually whatever the base image has |
| Build system | Executes arbitrary commands, decides what is built and with what flags | Its rules are reviewed; the tool itself is not |
| Build-time generators | Produce source that is compiled into the artifact | Sometimes, and their own dependencies rarely |
| Dependencies and install hooks | Contribute code, and in many ecosystems execute during install | Direct ones sometimes; transitive ones essentially never |
| Container image and kernel | Provide every binary above and can intercept everything | Pinned by digest at best |
Compile-time code execution is now normal
The item most often missed is that in several modern ecosystems, building a project executes code that the project's dependencies supplied. This is not an exotic configuration; it is the default.
Rust's build.rs runs as a program during the build of any crate that has one, and procedural macros are compiled and executed inside the compiler to generate code. Java annotation processors run inside javac with access to the compilation. npm packages have historically run postinstall scripts on installation. C++ builds routinely invoke code generators, and templates plus constexpr mean the compiler is executing substantial computation supplied by headers even without any of that.
Each of these is a good feature with a real use. Collectively they mean that "I read the source" and "I built the project" are entirely different acts with entirely different trust implications, and that adding a dependency can grant it code execution on the build machine before anybody runs the resulting artifact. [[hermetic-compilation]] is the containment strategy: run the build with no network, a declared and pinned input set, and no ambient state, so that what executes is at least enumerable.
- Rust:
build.rsand procedural macros both execute arbitrary code during compilation. - Java: annotation processors run inside the compiler with access to the program being compiled.
- JavaScript: package install scripts run by default in many configurations, before anything is built.
- C++: code generators, and template and
constexprevaluation executing header-supplied computation inside the compiler. - Every one of these has a legitimate use, and every one is compile-time code execution granted by adding a dependency.
Narrowing it
The trusted base cannot be eliminated, so the work is to make it smaller, enumerable and stable. Four measures do most of it and they compose.
Pin everything by content. A pinned digest for the container image, lockfiles with hashes for every dependency including transitive ones, a specific toolchain version rather than "whatever is installed". This does not make the inputs trustworthy; it makes them *the same every time*, which converts an unknown into a known and is the prerequisite for everything else.
Make the build hermetic. No network access during the build, a declared input set, no reads from outside it. Bazel and Nix are the industrial versions. The property this buys is that the set of things that can affect the output is finite and stated — see [[hermetic-compilation]].
Make it reproducible. If the same inputs yield byte-identical output, a compromised builder is detectable by anyone who rebuilds. This is the practical form of the trusting-trust countermeasure and is covered in [[reproducible-compilation]].
Reduce what runs. Disable install scripts where the ecosystem allows, vendor and review build scripts, prefer dependencies without compile-time code execution, and treat adding one as the privilege grant it is.
- Unpinned buildbuild timeWhatever tools and versions the machine happens to have.Nothing. The input set is unknown and varies between machines and days.
- Pinned by digestbuild timeA fixed, content-addressed set of inputs.The same build twice means the same inputs twice, which makes every later property checkable.
- Hermeticbuild timeA declared input set with no network and no ambient reads.An enumerable trusted base — you can now state what the build trusts.Convenience: every implicit dependency has to be made explicit, which is real work.
- Reproduciblebuild timeByte-identical output from identical inputs, on any machine.Independent verifiability: a second builder can confirm or contradict the artifact.
- Independently rebuiltbuild timeThe same artifact produced by parties who do not share infrastructure.Detection of a single compromised builder, which is the practical answer to
[[trusting-trust]].
Read it asRead this as a ladder where each rung requires the one below it. Reproducibility is meaningless without pinning, because two builds with different inputs are supposed to differ. Independent verification is meaningless without reproducibility, because two builders would never agree. Most organisations are on the second rung and describe themselves as being on the fourth.
Proportion
It is worth being explicit about relative risk, because this material invites disproportionate responses. A compiler backdoor is an elaborate, targeted attack. A malicious or compromised dependency is a commodity attack that happens constantly, and a leaked credential in a build system is more common still. Effort should follow probability.
The good news is that the mitigations coincide. Pinning, hermeticity and reproducibility defend against the common cases and the exotic one with the same work, because all of them are versions of "know and fix what your build consumes". Investing in them is justified by dependency risk alone, and the toolchain benefit comes along with it.
The framing to keep is that a build is a program that runs with your privileges and produces something other people execute. That description makes the trusted base obvious, and it is a more useful mental model than treating the build as a pure function of the source it visibly reads.
How it works
The steps, in the order the compiler takes them.
- Enumerate every program that executes during the build and every file each one reads, including those fetched during the build itself.
- Pin each by content — image digests, lockfiles with hashes, explicit toolchain versions — so the input set is identical across runs.
- Remove ambient inputs: no network during the build, no reads outside the declared set, no dependence on environment variables that are not declared.
- Eliminate sources of non-determinism so that identical inputs produce identical output.
- Have independent parties rebuild and compare, so a compromise of any single builder is detectable.
- Reduce the set where possible: disable install hooks, vendor and review build scripts, and treat compile-time code execution as a reviewable privilege.
How it breaks
What the engineer observes when it goes wrong — not what goes wrong internally.
- A dependency's install script exfiltrates credentials from the build environment, and nothing in the application's source or its review would have shown it.
- A build succeeds on CI and fails locally, or produces different artifacts, because an unpinned tool version differs — the same mechanism that would hide a substitution.
- A compromised base image supplies a modified linker, and every artifact built from it is affected while every source file is clean.
- A build-time code generator pulls a dependency at build time over the network, so the artifact depends on the state of a remote registry at build time and is not reproducible even in principle.
- A security programme audits application source thoroughly and never enumerates the toolchain, leaving the majority of what determines the artifact unexamined.
- A team declares its builds reproducible, and two builders have never actually compared outputs because no mechanism exists to do so.
When it helps
- Threat modelling a build pipeline, where the first useful output is simply the enumerated list of what executes.
- Diagnosing "works on my machine" at the artifact level, which is the benign form of exactly the same unpinned-input problem.
- Justifying investment in hermetic and reproducible builds, since the argument from dependency risk alone is usually sufficient.
When it hurts
- As a counsel of despair. The base cannot be reduced to nothing, and the useful question is which reductions are cheap relative to the risk they remove.
- When it displaces attention from likelier problems. Credential handling and dependency review typically deserve effort before compiler provenance does.
What it costs
Every one of these is paid by something.
- Pinning everything buys a known input set and costs maintenance: pinned things go stale, and stale things carry unpatched vulnerabilities, so pinning creates an upgrade obligation rather than removing one.
- Hermetic builds buy an enumerable trusted base and cost the work of declaring every implicit dependency, plus a build system that most teams find harder to use than the one they had.
- Disabling install scripts and compile-time code execution buys a smaller attack surface and costs compatibility with a large amount of real-world tooling that depends on them.
- Vendoring dependencies buys control over exactly what is built and costs the ongoing work of tracking upstream security fixes yourself.
What else you could do
What a different compiler or language does instead, and when that is better.
- Nix or Guix, which make the input set content-addressed and the build hermetic by construction, at the cost of adopting an entire packaging model.
- Bazel and similar hermetic build systems, which enforce declared inputs and sandboxed actions within a conventional ecosystem, at the cost of significant migration effort.
- Building inside a minimal, digest-pinned container with no network — most of the benefit of hermeticity for a fraction of the adoption cost, and a good default.
- Provenance attestation frameworks such as SLSA and sigstore, which record what built an artifact from what inputs. They establish provenance rather than correctness, which is weaker and much cheaper.
- Reducing the ecosystem surface deliberately: fewer dependencies, no install hooks, no compile-time code execution. Restrictive, effective, and unavailable in some ecosystems without giving up common tooling.
See it for yourself
The flag, dump or tool that shows you this directly.
- Enumerate what ran:
strace -f -e trace=execveon a build prints every program it executed, which is usually the first surprising thing anyone learns here. - Enumerate what was read:
strace -f -e trace=openatshows every file the build touched, including the ones outside the source tree. - Check pinning: does the lockfile carry hashes for transitive dependencies, is the container referenced by digest rather than tag, is the toolchain version explicit?
- Check compile-time execution: in Rust, look for
build.rsand proc-macro crates in the dependency graph; in npm,npm install --ignore-scriptsand see what breaks. - Check reproducibility: build twice and run
diffoscopeon the artifacts. The first difference found is usually a timestamp or a path, and finding it is the start of the work.
Plausible wrong readings
Stated the way a confident engineer states them.
- "The toolchain is the compiler." The compiler is one of a dozen programs that determine the artifact, and several of the others are never looked at by anyone.
- "Building from source means I know what I am running." Building from source runs a large amount of software you did not review, some of it supplied by the dependencies you are building.
- "A lockfile makes the build reproducible." It makes the dependency versions fixed. Timestamps, paths, parallelism and unpinned tools all remain, and any one of them is enough to break byte-identity.
- "Adding a dependency is a runtime decision." In several ecosystems it grants code execution at build time, on the build machine, before the artifact ever runs.
Misconceptions
The claim, and what is actually true.
Go deeper
The same idea at increasing depth. Stop wherever it stops being useful.
overview
When you build a program, you are trusting far more than the compiler: the preprocessor, assembler, linker, C library, build system, every dependency and the image the build ran in all get to influence what comes out. In several ecosystems, dependencies also run their own code during the build. Reviewing your own source covers a small fraction of what determines the artifact.
practical
Run strace -f -e trace=execve on a build once and read the list — it is the fastest way to see the actual trusted base. Then pin what you can by content: image digests, lockfiles with hashes, an explicit toolchain version. Turn off the network during the build if you can. And find out whether your ecosystem runs dependency-supplied code at install or compile time, because if it does, adding a dependency is a privilege grant and should be reviewed as one.
advanced
The useful reframing is that a build is a distributed system whose components happen to run sequentially on one machine, and the trusted base is its failure domain. Seen that way, the standard techniques transfer directly: pin your dependencies by content rather than by name, eliminate ambient state, make the process deterministic so that disagreement is a signal, and get independent replicas so that a single failure is detectable rather than authoritative. That is exactly the ladder from pinning to hermeticity to reproducibility to independent rebuilds, and it is why the reproducible-builds community and the supply-chain-security community converged on the same practices from opposite motivations. The compiler is one component in that domain — an important one because of [[trusting-trust]], and by no means the most probable point of failure.
How much this depends on
Nothing in this domain is true of every compiler. These say how much.
build.rs unconditionally, and Bazel sandboxes actions to a configurable degree. Verify the current behavior of your own tooling rather than relying on a remembered default.constexpr mean the compiler evaluates substantial computation supplied by headers, and the standard requires it to. That is compile-time code execution mandated by the language, not an optional feature, and it is one reason a C++ build has an irreducible amount of dependency-supplied computation in it.If you were asked this in an interview
- List everything a mundane C++ build trusts. How many of those has your team ever inspected?
- What does adding a Rust crate with a
build.rsgrant, and when does that grant take effect? - Why is pinning a prerequisite for reproducibility rather than an alternative to it?
Connections
- DevOps / Production Engineering — CI/CD pipeline hardening and build-environment isolationEvery mitigation named here — pinning, network isolation, sandboxed build steps, independent rebuilds — is implemented in the pipeline rather than in the compiler. The compiler-side facts are here; designing and operating the pipeline that contains them is owned there.
- Testing & Reliability Engineering — Detecting environmental divergence between build environmentsAn unpinned input producing a different artifact is the benign version of a substituted input producing a malicious one, and the same detection — compare artifacts across environments — covers both. Building that comparison into a pipeline is owned there.