· 6 min read ·

Rust's Compile-Time Code Execution Is a Supply Chain Liability Nobody Talks About

Source: lobsters

Rust has a well-earned reputation for safety, but that reputation applies to a specific and narrow domain: memory management at runtime. The language prevents use-after-free bugs, data races, and buffer overflows with impressive reliability. What it does not prevent is a malicious crate running arbitrary code on your machine the moment you type cargo build.

Sylvain Kerkour’s recent post lays out the core problem clearly. The Rust ecosystem has the same structural supply chain vulnerabilities as npm or PyPI, with one additional wrinkle that makes it distinctly uncomfortable: the attack surface is partially hidden inside a toolchain that developers have been trained to trust implicitly.

What Actually Happens When You Run cargo build

Most developers understand that cargo build compiles their code. Fewer think carefully about everything else it does first.

Before compilation begins, Cargo executes build scripts. Any crate in your dependency tree can include a build.rs file, and that file runs with full access to your system: filesystem, network, environment variables, and the ability to spawn subprocesses. This is not a security hole or an edge case. It is a documented, intentional feature used by hundreds of widely deployed crates.

The ring cryptography crate uses a build script to compile C and assembly code. openssl-sys uses one to locate or build OpenSSL. The cc crate, which itself is a transitive dependency of enormous swaths of the ecosystem, provides utilities that build scripts use to invoke C compilers. All of this is legitimate. All of it involves executing code from your dependencies before your compiler has touched a single line of your application.

The analogy in npm is the postinstall script, which has been the delivery mechanism for multiple high-profile attacks. The difference is that the Rust ecosystem has not yet had its event-stream moment, so the threat feels theoretical.

Procedural Macros Are the Second Front

Build scripts get occasional scrutiny. Procedural macros get almost none.

Proc macros are a form of compiler plugin. They run inside the Rust compiler during compilation, receiving a stream of tokens from your source code and returning transformed tokens. The serde ecosystem depends on them, tokio uses them, async-trait is implemented as one. They are ubiquitous.

What proc macros can do during compilation is essentially unrestricted. They can read files from your filesystem. They can make network requests. They can inspect your source code and exfiltrate it. They can emit arbitrary code into your compiled binary, including code that has no textual representation in any file you wrote or reviewed.

The security community has demonstrated proof-of-concept proc macros that phone home during compilation, that embed backdoors in the compiled output, and that selectively inject malicious behavior based on the surrounding code context. None of this requires unsafe. None of it violates Rust’s memory safety model. It just runs in the compiler.

A well-resourced attacker who compromises a popular proc macro crate, or who introduces a malicious crate via a typosquat or dependency confusion attack, has a remarkably clean path to either compromising developer machines during the build process or shipping backdoored binaries to production.

The Dependency Tree Problem

Rust projects accumulate dependencies quickly. A minimal async HTTP server using axum pulls in approximately 80 to 120 crates depending on feature flags. Add a database client, some serialization, and a few utilities, and you are past 200 crates without doing anything unusual.

Each of those crates can have a build script. Each can include proc macros. The total audit burden for a complete security review of a production Rust application is enormous, and almost nobody does it.

crates.io does not scan uploaded crates for malware. It does not verify that the code in a published crate matches any source repository. It makes 2FA mandatory for publishers of high-download crates, which is an improvement, but account compromise is only one attack vector. A crate author can publish malicious code deliberately, or a legitimate crate can be taken over through ownership transfer, or a typosquatting crate can sit dormant for months waiting for a developer to make a typo.

The RustSec Advisory Database tracks known vulnerabilities in crates, and cargo-audit will scan your Cargo.lock against it. This is useful and worth running in CI. It catches vulnerabilities that have been publicly reported and added to the database. It does not catch malicious code that has not yet been discovered, which is the more dangerous category.

How Other Ecosystems Have Handled This

The npm ecosystem learned about supply chain risk through a series of painful incidents. The 2018 event-stream attack, in which a widely used utility package was handed off to a new maintainer who added a targeted payload against Bitcoin wallet software, demonstrated that transitive dependencies from trusted authors were not safe. The 2021 ua-parser-js compromise, where a legitimate crate was taken over and modified to install cryptocurrency miners and credential stealers, showed that account compromise was a reliable vector. The 2022 colors.js incident showed that even deliberate sabotage by the original author was possible.

The Python ecosystem has had analogous incidents, with PyPI regularly discovering typosquatting packages that mimic popular libraries and ship malicious install scripts.

What the Rust ecosystem does not yet have is an equivalent institutional memory. The npm community has developed a certain baseline paranoia about postinstall scripts, a reflex to check dependency counts, and tooling like socket.dev that does behavioral analysis on published packages. The Rust community has not had enough high-visibility incidents to develop the same reflexes.

The XZ Context

The 2024 XZ Utils backdoor is worth keeping in mind here, even though it was not a Rust crate. Jia Tan spent approximately two years as a legitimate, productive contributor to the xz compression library before inserting a carefully obfuscated backdoor targeting SSH authentication. The attack succeeded because the contributor had built genuine trust through sustained, high-quality work.

This pattern, sometimes called a “long game” supply chain attack, is harder to defend against than simple typosquatting or account compromise. It does not require stealing credentials or registering a lookalike package. It requires patience. The Rust ecosystem is not immune to this. Any sufficiently popular crate with a small maintainer team is a candidate.

What Actually Helps

cargo-vet, developed by Mozilla, is the most serious tool for this problem. It requires that every dependency in your tree has been audited and that the audit is recorded in a versioned file. It supports importing audit records from trusted organizations, so you can leverage Mozilla’s audits if you share dependencies. The friction is real. Auditing every crate you depend on is not a casual afternoon project. But the friction is proportional to the actual risk.

cargo-deny provides policy enforcement: it can block crates you have explicitly banned, restrict license usage, and require that dependencies come from specific sources. Used alongside cargo-audit, it gives you defense in depth against known-bad packages.

Vendoring dependencies with cargo vendor removes the registry as a runtime dependency and makes your build reproducible against registry changes. It does not help if you vendor a malicious crate, but it does prevent a crate from being silently swapped underneath you between builds.

Network sandboxing during builds is underused. If your CI pipeline has the ability to block outbound network connections from the build process, a build script or proc macro attempting to phone home will fail visibly. This is straightforward in containerized CI environments and catches a significant class of attack.

The most durable mitigation is also the most culturally difficult: minimize your dependency footprint. The Rust community has some healthy instincts here, with experienced developers pushing back on unnecessary crates for small utilities. But the ecosystem also contains crates that pull in dozens of transitive dependencies to accomplish things that could be done with the standard library or a small amount of code. Every dependency you do not take is a crate you do not have to audit.

The Structural Problem

Rust’s memory safety gives developers a true guarantee about a specific category of bug. That guarantee is real and valuable. The problem is that it has created an ambient sense that Rust code is safe in a more general sense, that the language’s rigor extends somehow to the ecosystem around it.

Build scripts and proc macros execute before memory safety is even in scope. The attack surface they represent is not a failure of Rust’s design. It is a necessary feature of a compiled systems language that needs to interoperate with C, generate code, and build complex native dependencies. But it means that Rust’s supply chain security problem is structurally similar to every other language’s supply chain security problem, and the solution space is similar too: tooling, auditing, minimal dependencies, and institutional vigilance.

The Rust ecosystem will probably have a high-profile supply chain incident at some point. The question is whether the community develops better defensive habits before or after that happens.

Was this interesting?