Skip to content
← Blog

Technical explainer

Cargo supply chain security: build.rs runs code at build time

3 min read

Rust's reputation for safety is about memory, not supply chain. cargo build will compile and execute arbitrary code on your machine before your own main ever runs — and most Rust developers don't think about it, because the mechanism is quieter than npm's postinstall.

That mechanism is build.rs. This post explains the Cargo install-time surface, what Cargo.lock does and doesn't protect, and how a registry gate intercepts crate downloads.

build.rs is the install script you didn't notice

A crate can ship a build.rs file. Cargo compiles it and runs it as a native binary during the build of any project that depends on the crate — to detect system libraries, generate code, or set link flags. It runs with your user's privileges, with network and filesystem access, before the crate's actual code is compiled into your program.

Procedural macros are a second compile-time execution vector: a proc-macro crate runs at compile time in the compiler's process. Both are legitimate, widely used features. Both are also exactly the foothold a supply-chain attacker wants — code execution triggered by a routine cargo build, no runtime required.

What Cargo.lock protects

Cargo.lock pins each dependency to an exact version and records a checksum — the SHA-256 of the .crate file. On resolve, Cargo verifies the downloaded .crate against that checksum. So Cargo.lock gives you reproducibility and tamper-evidence: you get the same bytes every time, and a registry serving different bytes is caught.

What it does not give you is safety. A typosquatted crate, a malicious new version of a real crate, or a compromised maintainer's release all produce a perfectly valid checksum. Integrity ≠ trust.

crates.io is a two-host registry

This matters for gating. Cargo's modern registry protocol splits across two hosts:

  • The sparse index (index.crates.io) serves per-crate metadata — versions, dependencies, the cksum for each version, and a config.json whose dl field tells Cargo where to download .crate files.
  • The CDN (static.crates.io) serves the actual .crate artifacts.

A single-upstream proxy can't serve a Cargo build, because the metadata and the bytes live on different hosts. An install-time gate has to be dual-origin: forward index requests to index.crates.io, and .crate downloads to static.crates.io — and rewrite the index config.json dl template so downloads route back through the gate at a path it can score.

How install-time gating fits

With the dual-origin gate in place and cargo pointed at it via a source replacement ([source.crates-io] replace-with), every .crate flows through one place before it lands:

  • OSV lookup against the crates.io ecosystem.
  • Threat-feed match for known-malicious crates.
  • SHA-256 verification of the downloaded .crate against the Cargo.lock checksum — an independent integrity check at the wire.

And because build.rs is the real execution risk, the network gate is only half the story: the build itself runs inside an OS sandbox so a malicious build.rs can read only the project and the package cache, and can reach only the gate on the network — not a credential store, not an exfiltration endpoint.

With Veln, veln safe cargo build writes an isolated CARGO_HOME config that source-replaces crates-io with a dual-origin gate (index + CDN), scores every .crate, and runs the build — build.rs and all — inside the sandbox. veln verify reads Cargo.lock directly for CI and pre-commit gating.

The practical takeaway

Treat cargo build as code execution, because it is. Commit Cargo.lock and let Cargo enforce checksums. But remember the two things Cargo.lock can't do: it can't tell you a crate is malicious, and it can't stop build.rs from doing whatever it wants on the machine that runs it. OSV plus threat-feed answer the first; a contained build answers the second.