· 7 min read ·

Rust's Safety Guarantees Stop at the Compiler's Edge

Source: lobsters

The safety story Rust tells is largely true. The borrow checker prevents entire categories of memory corruption bugs. The type system makes data races a compile-time error. For most developers, Rust genuinely does eliminate the security issues that plague C and C++ codebases. That reputation is earned.

But the reputation creates a different problem: it makes developers feel safer than they are. The supply chain threat in Rust isn’t about memory unsafety. It’s about code that runs before your program ever starts, code that executes with full system privileges during the build process, code embedded in a dependency graph that most developers have never fully read. This problem is real and underexplored, and Rust’s particular toolchain design makes it sharper than in most other ecosystems.

The build.rs Problem

Every Rust crate can include a build.rs file at its root. Cargo executes this script before compiling the crate, and it runs with the same permissions as the user invoking cargo build. There is no sandbox. There is no network restriction. The script can read files, write files, make outbound HTTP requests, and execute arbitrary binaries.

This is by design. Build scripts exist for legitimate reasons: linking against native libraries, generating code from protobuf schemas, detecting system capabilities. The openssl crate uses one to locate or build OpenSSL. The bindgen crate uses one to generate FFI bindings. These use cases are real and hard to address otherwise.

But the same mechanism is a perfect exfiltration vector. A malicious build script can read ~/.ssh/id_rsa, POST it to an external server, and proceed with the build as normal. The developer running cargo build sees no output indicating anything went wrong, because nothing went wrong from the build’s perspective. The binary compiles. The tests pass.

// build.rs in a malicious crate
fn main() {
    // Reads environment, can access home dir, secrets, tokens
    if let Ok(token) = std::env::var("GITHUB_TOKEN") {
        // An HTTP request is straightforward from here
        // ureq, curl, or even raw TcpStream all work
        let _ = send_somewhere(&token);
    }
    println!("cargo:rerun-if-changed=build.rs");
}

Cargo does cache build script outputs and won’t re-run them unless inputs change, but the first execution still happens. A targeted attack only needs one run.

Proc Macros Are Also Code That Runs at Compile Time

Procedural macros get less attention than build scripts in supply chain discussions, but they carry the same risk. A proc macro is a Rust function that takes a TokenStream as input and returns a TokenStream as output, but the function body can do anything a normal Rust program can do. It runs in the compiler process, with full access to the environment.

Popular derive macros like serde::Serialize and serde::Deserialize are proc macros. So is tokio::main. The ecosystem depends heavily on them, and there’s no practical way to use modern async Rust without proc macro dependencies.

The issue isn’t that serde is going to steal your credentials. The issue is that proc macro crates occupy a position of elevated trust by convention, and that convention isn’t enforced by any technical mechanism. A typosquatted serde_json variant or a subtly compromised transitive dependency in the proc macro graph can run arbitrary code during compilation without the developer having any obvious signal that this occurred.

The Dependency Graph Nobody Has Read

A fresh cargo new project with a single dependency on tokio with default features pulls in dozens of crates. Add serde, reqwest, and a few common utilities, and you’re looking at a dependency graph with hundreds of nodes. Tools like cargo tree surface this:

$ cargo tree | wc -l
342

Most developers have reviewed zero of those 342 crates. This isn’t negligence; it’s a structural feature of how modern software is built. The same is true in npm and PyPI. But Rust’s build-time code execution means that every crate in that graph, including transitive dependencies several levels deep, has the opportunity to run code on the developer’s machine before a single line of application code executes.

Typosquatting and Name Confusion on crates.io

The crates.io registry has limited protections against name confusion attacks. There’s some fuzzy matching that prevents registering names identical to popular crates, but variations are possible. Common patterns include swapping hyphens and underscores, inserting characters, or registering plausible alternative names for common utilities.

Unlike npm, which has had high-profile name confusion attacks like the event-stream incident in 2018 where a popular package received a malicious update via a maintainer handoff, Rust hasn’t had a widely publicized equivalent. That’s partly because the ecosystem is smaller and partly because crates.io does have some ownership verification. But smaller ecosystems don’t have smaller attack incentives as they grow in commercial use, and the history of npm and PyPI suggests that visibility in production software increases the value of compromising packages there.

The RustSec Advisory Database tracks known vulnerabilities in crates. As of 2025 it contains hundreds of advisories, the majority covering correctness and safety bugs rather than malicious intent, but the infrastructure for reporting and tracking exists and is mature.

What the Existing Tools Actually Cover

The Rust ecosystem has three main tools for supply chain risk management, and understanding what each one covers is important for setting realistic expectations.

cargo-audit checks your Cargo.lock against the RustSec advisory database. It catches known vulnerabilities in specific crate versions. This is reactive: it only flags issues that have already been reported and reviewed. It’s still worth running in CI because known vulnerabilities are the most common immediate risk, but it provides no protection against novel malicious code or zero-day supply chain attacks.

cargo install cargo-audit
cargo audit

cargo-vet takes a more proactive approach. Developed by Mozilla for Firefox’s Rust dependencies, it maintains a database of human audit records for crate versions. When a new dependency appears in your build, cargo-vet requires that someone on your team, or a trusted auditor from another organization, has reviewed it. You can delegate trust: if Bytecode Alliance has audited a crate as safe for use in a no-network context, you can extend that trust to your own project without re-reviewing.

This is closer to what’s needed for serious supply chain security, but it requires organizational buy-in and ongoing maintenance. Every crate update potentially needs a new review, or at minimum a diff review against the previous audited version.

cargo-crev is a community-driven code review system. Developers publish cryptographically signed reviews of crate versions, and you can build a web of trust by following reviewers whose judgment you trust. This is decentralized and permissionless, which makes it resilient but also means coverage is uneven. Popular crates have reviews; obscure ones often don’t.

Cargo.lock and What It Actually Does

Cargo.lock ensures reproducibility. When you commit your lock file and run cargo build on another machine, you get the exact same dependency versions. This matters for supply chain security in one specific way: it prevents version-floating attacks where a newly published malicious minor version gets pulled in automatically.

But Cargo.lock doesn’t validate that the locked versions are trustworthy. It ensures consistency, not safety. A build with a compromised dependency and a committed Cargo.lock will consistently reproduce the compromised build. The lock file is a prerequisite for reasoning about your dependency state; it’s not itself a security control.

One underused feature is cargo verify-project, combined with checksums in the lock file. Cargo does record content hashes for crate downloads, which means a crates.io-level tampering attack would be detectable. But this protects against tampering with published versions, not against malicious code in a legitimately published version.

A Practical Mitigation Stack

Given what the tools cover and where the gaps are, a reasonable mitigation approach for a production Rust project looks like this:

Run cargo audit in CI and fail builds on unfixed advisories. This is low effort and catches the most common class of issues.

For internal tooling and CLIs, consider using cargo-supply-chain to surface dependency health metrics: number of maintainers, recent activity, download counts. Abandoned or single-maintainer crates with high transitive use are higher risk for a maintainer handoff attack.

For anything handling credentials, financial data, or running on customer infrastructure, evaluate cargo-vet. The upfront cost of auditing your current dependency graph is real, but Mozilla has published its own audit records and several other organizations have followed. You can bootstrap from those shared audits and limit your own review work to the gap.

Specifically review every build.rs and proc macro in your direct dependencies. This is a tractable scope even in moderately complex projects. The transitive dependency graph is harder, which is exactly why cargo-vet’s delegation model exists.

For open source projects that accept contributions, consider pinning dependencies in CI using --locked and reviewing dependency bumps as a distinct review category. A PR that bumps ten transitive dependencies alongside a feature change is harder to audit than a dedicated dependency update PR.

The Honest Assessment

Rust isn’t uniquely bad here compared to npm or PyPI. It’s subject to the same structural pressures that make supply chain attacks viable across modern software ecosystems: deep dependency graphs, widespread build-time code execution, and limited practical capacity for developers to audit everything their projects consume.

What Rust does have is a community that takes security seriously, tooling that’s further along than what most ecosystems had at equivalent maturity, and a smaller package registry that’s easier to monitor for anomalies. The Rust Secure Code Working Group maintains active advisories and is reachable for reporting.

The risk is real, and the appropriate response isn’t to treat it as a reason to avoid Rust. It’s to stop conflating memory safety with overall security, understand which threat models the type system addresses and which it doesn’t, and invest in the tooling and process controls that cover the remaining surface. The compiler can’t audit your dependencies for you. That part is still your job.

Was this interesting?