field note / 2026 / guix + reproducible-builds A package-manager security workbench with printed Guix substitute diagrams, narinfo metadata sheets, archive extraction traces, daemon restart notes, Ethernet cables, and a terminal showing a patched build-store verification run.

field dossier

Guix Substitutes Broke the Reproducible-Build Trust Pipeline

Guix's substitute vulnerabilities hit the uncomfortable layer under reproducible builds: the build graph can be pure while the delivery path still depends on metadata binding, archive extraction order, daemon authority, and a safe way to update the updater.

Guix’s July 2 advisory lands in the worst possible place for a reproducible-build system: the path between a trusted store item and the bytes that actually enter the machine. The project disclosed four issues across guix substitute, guix pull, and guix time-machine, with impact ranging from remote privilege escalation and store corruption to local file disclosure and denial of service. That is the delivery pipe under the purity story, cracked open in public.

The useful lesson is blunt. Reproducibility gives you a way to compare what should have been built. It does not automatically make the transport, metadata, daemon boundary, archive parser, channel cache, or emergency upgrade path trustworthy. Those are separate machines. They need separate paranoia.

The Guix advisory names the ugly bit directly: a malicious substitute server, or a man in the middle, could exploit guix substitute when a vulnerable system attempted to download a binary substitute. HTTPS did not save the model because the substitute URL inside the narinfo metadata was not signed. If the daemon ran with enough authority, the vulnerable archive restore path could write files wherever that daemon user had permission, including root-owned places on systems where the daemon runs as root.

The advisory also describes a narinfo binding bug. fetch-narinfos failed to verify that the received narinfo matched the specific store item requested. That means the client could be pushed toward a different authorized store item. The attack shape is nastier than random corruption because the substituted thing can still look authorized. Supply-chain systems tend to fail hardest when their failure state resembles their success state.

The fix set in Guix pull request #9665 is the kind of boring patch stack that usually matters more than the headline CVE label. The Nar parser now rejects invalid names such as empty strings, ., .., names containing /, and names containing null bytes. Directory entries must be ordered. Recursion gets a limit. Substitutes restore into a temporary directory and move into the store only after verification. Untrusted file:// URIs are blocked. Store path syntax gets validated before it can propagate downstream.

That is the correct smell: make every implicit assumption executable, then make the dangerous path physically harder to reach.

The local disclosure issue has the same shape in miniature. guix substitute allowed file:// URIs and followed symbolic links without a clean distinction between daemon-level and client-level substitute URLs. A local user who could connect to the daemon socket could make the daemon read files it could access, and parse failures could leak content back in a backtrace. Once again, the bug sits in the plumbing between intent and authority. A user asks for package machinery. The daemon owns wider filesystem reach. The error channel becomes a side channel. Security loves humiliating elegant abstractions with dumb pipes.

guix pull and guix time-machine added a fourth class of failure. Channel authentication derived cache filenames from channel names. A malicious channel name shaped like a path could create or overwrite files in the user’s home directory, with content constrained enough to make denial of service the main impact. The fix changes the cache key to derive from the introductory commit ID, a hex string rather than attacker-shaped naming. Names are interfaces. Treating them as paths is how software keeps stepping on garden rakes.

That updater problem is the article. Guix recommends updating guix-daemon immediately, restarting it after the pull or reconfigure step, and optionally using --no-substitutes during the initial upgrade depending on threat model and available hardware. LWN’s short writeup captured the operator wrinkle cleanly, including a public comment noting that systems using offloading may also need --no-offload during a cautious upgrade path. Tiny flag, big boundary. If your build farm participates in substitution or offload, your emergency procedure has more than one intake valve.

This is where the culture around reproducible systems gets too smug. The pitch often sounds like content-addressed purity will dissolve supply-chain anxiety. It helps. It gives operators a stronger invariant than “trust the vendor CDN.” But the invariant has to survive a route through metadata, mirrors, daemons, parsers, sockets, caches, offload workers, and restart semantics. Every one of those layers can turn a clean graph into a dirty operational state.

Guix deserves credit for publishing a detailed advisory, a check script, a timeline, and concrete fixes. That is serious maintenance. The incident still punctures a fantasy that technical communities keep reviving because it feels mathematically satisfying: if the build is pure, the system is safe. No. Purity buys auditability. Safety still depends on adversarial handling of names, files, signatures, daemon authority, failure output, and the first bootstrapping step after bad news drops.

The pattern reaches beyond Guix. Nix, Lix, Guix, container registries, language package indexes, CI caches, model artifact stores, browser extension stores, and plugin managers all carry the same concealed bargain. Users want fast binary delivery. Maintainers want shared caches. Operators want automatic updates. Attackers want the seam where “authorized artifact” and “specific requested artifact” drift apart.

The best systems make that seam boring. Bind metadata to the requested object. Verify before placing. Never follow attacker-shaped paths. Treat URI schemes as authority changes. Keep daemon privilege narrow. Make emergency update paths slower but legible. Restart the actual daemon because yesterday’s daemon can keep yesterday’s bug alive. And when the fix says “use --no-substitutes if your threat model demands it,” read that as a confession from reality: the content-addressed future still needs a hand-cranked escape hatch.

The postmortem sentence is simple enough to tape above a build server: reproducibility checks the artifact, then operations decides whether the artifact arrived through a pipe clean enough to matter.