Rust Is Memory Safe. Your Build Button Is Not.
Every Rust pitch starts the same way: memory safety. No buffer overflows, no use-after-free, the compiler catches it for you. The malicious crate found on crates.io this week hit the one place that guarantee doesn’t reach. The developer never ran the code. They pressed cargo build once.
Code That Runs Without Being Called
The target was a micro-utility crate in the mold of arrayref — a single-file library whose entire job is a couple of macros that turn a slice into a fixed-size array reference. Trivial stuff. The catch is that blake2 and curve25519-family crypto libraries pull it in by default. You never typed its name, but it’s sitting three levels down your dependency tree.
That’s exactly the seam the attacker went for. Publish a crate whose name differs by one character, then plant the payload not in the library body but in a proc-macro. Library code has to be called by someone to fire. A proc-macro doesn’t wait for that. It runs the moment the compiler parses your code.
A caveat worth stating plainly: there’s no proper postmortem yet, so blast radius and exact timeline are still moving. But the skeleton of this attack is not new at all. If anything, that’s the uncomfortable part — the same trick keeps working, year after year.
Cargo Runs Other People’s Code at Compile Time. On Purpose.
This is a design property, not a bug. Building a Rust project executes arbitrary code through two main paths.
The first is build.rs. Build scripts get compiled and executed before your actual crate compiles. They exist for good reasons: linking C libraries, detecting platform-specific config, generating code. And a build script is a plain Rust program with full ambient privileges. It can open sockets, read your home directory, spawn a shell. Nothing constrains it.
The second is the proc-macro path above. If you’ve ever written #[derive(Serialize)], you’re already using it. Procedural macros behave like compiler plugins, but under the hood they’re ordinary Rust code executing inside the compiler process. The contract says: take a syntax tree, return a syntax tree. Nothing enforces what happens in between.
So building a Rust project means running code from some subset of the hundreds of crates in your tree — on your laptop, or on your CI runner. There is no sandbox.
Here’s where the confusion sets in. std::process::Command requires no unsafe whatsoever. Memory safety keeps a program from touching addresses it shouldn’t. It does not keep a program from doing something malicious on purpose. Memory safety and supply chain safety are orthogonal axes, and the reflexive “it’s Rust, so it’s fine” is precisely what lowers the guard.
The Holes in the crates.io Trust Model
crates.io uses a flat namespace. npm has scopes — @org/package — and crates.io has nothing equivalent. Names are first-come, first-served. arrayref, array-ref, and array_ref can belong to three different people. Cargo treats hyphens and underscores as loosely interchangeable in places, which makes eyeballing a Cargo.toml even less reliable. It’s hard to design a better environment for typosquatting.
There’s no review at publish time either. That’s not to say nothing has improved: publisher 2FA became mandatory in 2023, and Trusted Publishing later arrived so CI systems like GitHub Actions can publish with short-lived tokens instead of long-lived secrets. Genuinely good changes. But what they defend against is account takeover. Someone who registers a clean account and uploads malware from day one walks right past all of it.
Immutability cuts both ways too. Published versions can’t be deleted, which is what makes reproducible builds possible — but when a malicious version lands, yank only blocks new resolution. Any project that already pinned it in Cargo.lock keeps pulling the same version.
Treating download counts as a trust signal is its own trap. A large share of the crates.io counter comes from CI, not humans. A big number means the crate is popular. It does not mean anyone read the code.
We’ve Been Here Several Times
In May 2022, a crate called rustdecimal appeared — the widely used rust_decimal minus one underscore. It looked normal, but hidden code checked for the GITLAB_CI environment variable and, if found, fetched a payload from an external server. The target wasn’t developer laptops. It was CI pipelines, where the deploy keys and cloud credentials live.
In September 2025, faster_log and async_println were caught doing something nastier: posing as logging libraries while scanning project source files for Ethereum and Solana private key patterns and shipping matches to a remote server. The chilling detail is that the advertised logging worked correctly. Users had no reason to suspect anything.
Look outside Rust and you get the xz-utils backdoor of March 2024. Different language, different ecosystem, identical lesson. The malicious code didn’t live in the source tree humans read — it lived in the build system. It was split across m4 scripts and binary test fixtures, invisible when browsing the repo on GitHub, present only in the release tarball.
All three incidents converge on one point. Attackers avoid the places humans review and plant themselves where machines execute automatically. build.rs and proc-macros are exactly that place.
What You Can Actually Do Today
The real fix is sandboxed build scripts. Tools like cackle are pushing that direction, letting you declare per-crate permissions and flagging violations, and proposals to build sandboxing into cargo itself have been floating around for years. The problem is that none of it is the default yet. Until it is, the perimeter is manual.
Commit Cargo.lock, and pass --locked in CI. Just preventing dependencies from silently shifting overnight buys you response time. Then wire cargo-audit or cargo-deny into the pipeline to check against the RustSec advisory database — that at least filters out crates already known to be malicious.
If you want to go further, there’s cargo-vet, built by Mozilla. It’s a tool for sharing dependency audit results across organizations, so when you add a new crate you can ask whether anyone has actually looked at this thing. Auditing everything yourself is impossible; the point is to borrow other people’s audits.
Two habits are worth internalizing. Never hand-type a dependency name — copy it from the official docs, or use cargo add. And occasionally walk your tree with cargo tree, counting how many crates ship a build.rs and how many are proc-macros.
The Takeaway
Rust’s strength is catching dangerous code at compile time. What this incident exposes is that compile time is itself an attack surface. The compiler is both your shield and an execution environment.
Run cargo tree on whatever project you have open right now. Do you know how many dependencies you have, and how many of them can execute code on your machine the instant you build? I counted mine for the first time recently, and the number was not comfortable.
Comments
Loading comments...