A Rust procedural macro is not a macro in the textbook sense. It is a Rust program that runs at compile time with the full privileges of whoever invoked cargo build. It can open files, make network connections, spawn processes, and emit any Rust code it likes into the final binary — all before the compilation you think of as "the build" has begun. This is the least-examined attack surface in a typical Rust project, and it gets less examined because most developers do not think of proc macros as code that runs. They think of them as #[derive(Serialize)] — syntax. The gap between perception and reality is exactly where supply chain risk accumulates.
How does a proc macro actually execute?
When the Rust compiler encounters a proc macro invocation, it loads the macro crate as a dynamic library and calls into it with the token stream of the annotated code. The macro function runs in the compiler's process, with whatever privileges the compiler has (which is to say, whatever privileges the developer or CI agent has). Whatever Rust it emits gets substituted into the token stream and compiled normally.
Two things about this are worth internalising. First, the macro runs before compilation of your crate really begins in earnest, so a compromised macro can influence everything downstream. Second, there is no sandbox. The macro can std::fs::read_to_string("/etc/passwd") if it wants. On a CI runner with broad credentials, that is not hypothetical.
What can a malicious proc macro realistically do?
Five categories of misbehaviour, in rough order of how straightforward they are:
- Exfiltrate source code. Walk the workspace, tar it up, POST it to an attacker endpoint.
- Exfiltrate environment variables. CI environments commonly have
GITHUB_TOKEN,NPM_TOKEN, cloud credentials, deployment keys available as environment variables. - Inject backdoor code. Emit Rust that adds a hidden authentication bypass, adds telemetry, or creates a subtle logic flaw.
- Modify the local filesystem. Write to
~/.ssh/authorized_keys,.bashrc,.git/hooks/, or any other persistence location. - Phone home with build telemetry. Less dramatic but enables targeted second-stage attacks on specific organisations.
Each of these has been demonstrated in proof-of-concept form by security researchers. None requires novel exploitation technique — they are just proc macros doing what proc macros are allowed to do.
What makes the risk worse than build.rs?
build.rs is also compile-time-executed Rust, and it has the same privileges. The difference is visibility. A build.rs is a file in the crate root. Most reviewers notice it. Proc macros, by contrast, are invoked as attributes or function-like macros deep inside user code. A dependency three transitive hops away can ship a proc macro that runs on your CI without anyone in your org having looked at it.
The other difference is update cadence. build.rs scripts change rarely; proc macro crates iterate more often, and each new version ships fresh code that runs with privilege the moment you update the lock file.
Real incidents and near-misses
The public record is thinner than it should be, partly because proc-macro-based supply chain attacks are hard to attribute — the malicious code does not appear in the final binary; it just influenced what got built. Notable-adjacent events:
- RUSTSEC-2022-0077 documented a compromised maintainer scenario where a proc macro could have been used to distribute backdoors via
Cargo.tomldependency updates. - 2024 Hugging Face model hub incidents showed parallel ecosystem dynamics — the lesson transfers to Rust.
- Multiple proof-of-concept blog posts (notably by James Munns and others in 2022-2023) demonstrated working malicious proc macros that exfiltrated CI secrets.
The absence of a big public incident is not evidence of safety. It is evidence of insufficient detection capability in most organisations.
What mitigations actually work?
Four that are practical in 2024:
Audit proc macro crates explicitly. Treat any crate that exports proc macros as a privileged dependency. When you add one to Cargo.toml, read its source. When you update one, read the diff. cargo-vet supports distinguishing proc macro audits from regular audits.
Pin proc macro versions aggressively. A patch version bump on a proc macro crate is a code change that runs with developer privileges. Handle it with the same gravity as a dependency update on a runtime-critical crate.
Constrain the build environment. CI runners should not have production credentials, broad cloud access, or unfettered filesystem/network access available during build. Many organisations still use CI runners with credentials intended for deployment also available during build — a proc macro is exactly the shape of attack that exploits this.
Consider build isolation. Tools like cargo-crev, vetted dependency profiles, and hermetic build environments (Bazel, Nix) materially raise the bar for proc-macro-based attacks.
Is there any language-level fix coming?
Probably not in a soon time frame. The Rust working groups have discussed sandboxing proc macros, and cargo-crux and other experimental tools have explored specific restrictions, but none of this has landed in stable Rust as of late 2024. The design space is hard — proc macros need some privileges to do their job (reading adjacent source, accessing build metadata) — and the compatibility implications of retrofitting a sandbox are considerable.
Plan for the next several years as if the current model persists, because it will.
A practical checklist for teams
- Enumerate every proc macro crate in your dependency tree (
cargo tree -e proc-macrohelps). - Add them to a "privileged dependencies" list reviewed on every change.
- Verify CI runners have scoped, minimum-necessary credentials during build.
- Adopt
cargo-vetwith distinct audits for proc macro crates. - For regulated workloads, consider hermetic build environments.
The effort is modest. The delta between "we noticed" and "we did not notice" for a proc-macro-based attack is the difference between a near-miss and a story that shows up in an incident report two years later.
How Safeguard Helps
Safeguard identifies proc macro crates specifically in the Rust dependency graph and flags them as privileged dependencies in the reachability view. Policy gates can require explicit audit attestation (via cargo-vet or equivalent) for every proc macro crate before a PR is allowed to merge, and Griffin AI summarises proc-macro-specific changes across version updates so the review surface is the code that actually runs at build time, not the whole crate. For teams running Rust in environments where compile-time code execution with developer privileges is a real threat, Safeguard surfaces the risk where most tooling currently treats proc macros as ordinary dependencies.