If I had to name the single most dangerous feature in the Rust build system, it would not be unsafe. It would be build.rs. Build scripts execute arbitrary code on the developer or CI machine before a single line of your application is compiled, they have full filesystem and network access, and they are triggered automatically by cargo build. You cannot opt out. You cannot easily audit them. Most developers do not read them. This essay is a risk profile, informed by a few months of looking hard at build scripts across a few hundred crates in production dependency graphs.
What build.rs Actually Is
A build script is a Rust source file, conventionally build.rs, that Cargo compiles to a native binary and runs before the main crate is compiled. The documented use cases are legitimate: linking against system libraries (libz-sys calls pkg-config), generating code from protobuf or flatbuffer definitions (prost-build, tonic-build), detecting CPU features for conditional compilation (cc crate feature probing), or reading environment variables into generated constants.
The key property from a security perspective is that build.rs runs with the same permissions as the user running cargo. If that is your laptop, it can read ~/.ssh/id_ed25519. If that is your CI runner, it can read whatever secrets the runner has mounted. If that is your release signing machine, it is game over.
The Historical Record
The Rust ecosystem has been lucky relative to npm. There have not been the kind of widely-publicized build-script incidents that PyPI and npm have suffered. "Lucky" is the right word, though, not "secure by design." A handful of near-misses are documented in the RustSec advisory-db.
RUSTSEC-2022-0056 covered a crate with a build script that phoned home to the author's server with OS and hostname. It was not strictly malicious, but it was not consented to either, and it was discovered only because someone ran the build in a sandbox and noticed the outbound request. The crate was pulled; the episode illustrated that build scripts can do telemetry entirely outside the developer's awareness.
In late 2023 a series of typosquatted crates with malicious build scripts appeared on crates.io. The payloads were fairly naive (shell out to curl and exfiltrate environment variables) but they would have worked against any CI runner that did not sandbox network egress. The crates were pulled within hours of community reporting. The vector itself is still open.
The 2024 xz-utils incident, while not a Rust incident, changed how I think about this class of risk. The xz backdoor was delivered partly through build-time scripts and build system complexity. The pattern of "attack at build time, not runtime" is now understood as a first-class technique and I expect the Rust ecosystem to see it more seriously.
Why It Is Structurally Hard
Build scripts are hard to defend against for several reasons that compound.
First, they run before your static analysis does. You cannot cargo-audit a crate you have not downloaded, and you cannot download a crate without also running its build script if you build it. Scan-then-build is not a flow Cargo supports out of the box.
Second, the permissions model is all-or-nothing. Cargo has no equivalent to a seccomp profile or a capabilities list. A build script can read any file the user can read, make any network connection the user can make, and spawn any process the user can spawn. There is no "this build script only needs to read /etc/ld.so.cache" declaration.
Third, transitivity. Your project might have ten direct dependencies and one hundred and sixty transitive ones. If even one transitive dependency has a build script, you execute it. A 2023 survey I ran against my own project's lockfile found 34 out of 148 crates had a build.rs, so roughly 23 percent. Reviewing all of them by hand is not realistic.
Fourth, the legitimate use cases genuinely need the power. openssl-sys has to find the system OpenSSL. bindgen has to invoke clang. You cannot strip the capability without breaking a significant part of the ecosystem, which is why nobody has.
Concrete Defensive Measures
What I actually do, in order of effort:
Pin your dependencies and review Cargo.lock diffs. A new build.rs appearing in your tree should be a review event. When a direct dependency adds a transitive dependency that brings in a build script, that is a policy decision and should be visible.
Build in a sandboxed environment. For CI, I use GitHub Actions runners with restricted egress. Outbound network is blocked by default except to crates.io and a specific list of hosts. Any build script that tries to curl somewhere unexpected fails the build. This caught a legitimate-but-undocumented telemetry build script in one of our dependencies in April 2024; we reported it upstream and they added an opt-out.
Use the --offline flag with a pre-warmed cache for release builds. This at least removes crates.io-level substitution as a live attack surface during the release itself.
Review build scripts for your critical-path dependencies. It is a chore but not infinite work. serde's build script is trivial. tokio does not have one as of 1.36. reqwest's is small. The crates I spent most time on were the -sys crates because they do the most.
Consider cargo --config build.rustflags and lockfile policy in cargo-deny to prevent silent addition of new build-scripted dependencies. cargo-deny's bans section can block specific crates, and I use it to block a handful of crates known to have aggressive build scripts when lighter alternatives exist.
For high-assurance builds, look at projects like cargo-sandbox and the nascent work on Cargo build-script sandboxing (discussed in Rust internals threads through 2024 but not yet landed). The kernel-level approach of firejail or bubblewrap around cargo build is heavyweight but it works.
The Attacker's View
If I were designing a supply chain attack against a Rust target, build.rs is where I would go. It gives me code execution on every developer and CI machine that builds the project, it runs before any application security controls are in place, and it is the part of the codebase developers are least likely to read. The Rust community's reputation for safety is actually a liability here because it creates an expectation that the ecosystem handles this for you. It does not.
How Safeguard Helps
Safeguard flags every crate in your dependency graph that ships a build script and ranks them by transitive-download popularity, maintainer churn, and historical advisory activity, so your review effort goes where it matters. We track build.rs content hashes across versions so a previously-trivial build script that suddenly grows to 400 lines triggers a review gate before the version is pulled into your lockfile. For CI, we integrate with egress policy enforcement so a build script making an unexpected outbound connection becomes a blocking finding rather than a line in a log nobody reads.