blog.rust.gay

Malicious crates and build-time supply-chain risk in Rust

Rust's package ecosystem makes third-party code easy to adopt, but dependency resolution and compilation also create execution paths that are hard to inspect by reading only application code. A malicious crate release does not need a memory-safety bug in the downstream program to cause damage. It can arrive through a normal cargo build, run code during compilation, or pull in another package that delivers a payload. The Rust Security Response Team's 20 August 2026 report on a supply-chain attack affecting arrayref is a concrete example of how that risk shows up in practice.

The incident is useful because the response published verifiable details: when the report arrived, which versions were published, how long they stayed on crates.io, what the registry did, and how maintainers could check local Cargo caches. The RustSec advisories for the affected crates add more measurable context, including download counts and unaffected version ranges. For Rust projects, supply-chain security is less about finding one perfect scanner and more about making dependency changes deliberate, reviewable, and fast to investigate.

What the 2026 arrayref incident demonstrated

On 20 August 2026 at 07:15 UTC, the Rust Security Response Team received a report that the proc-macro1 crate was malicious. According to the Rust blog post, the team verified that the crate had a build script that downloaded a malicious payload. Several related crates — proc-macro1, proc-macro-en, aovine, arone, aronenao, and tinymember — were deleted from crates.io.

The response also found that popular crates had been republished to depend on malicious packages. The Rust Security Response Team reported these malicious versions and windows of availability:

Crate and version Published (UTC) Deleted (UTC) Time on crates.io
arrayref@0.3.10 2026-08-20T07:15:00Z 2026-08-20T08:41:40Z 86 minutes
internment@0.8.7 2026-08-20T07:34:07Z 2026-08-20T09:04:11Z 90 minutes
append-only-vec@0.1.9 2026-08-20T07:37:49Z 2026-08-20T09:25:24Z 107 minutes

The team removed the malicious versions, unyanked versions that had been maliciously yanked, locked the affected account as a precaution, and said it did not believe the arrayref author was acting maliciously. The blog post stated that the author's computer or credentials were likely compromised and that the team was attempting to contact them.

RustSec advisory RUSTSEC-2026-0260 adds more detail for arrayref. It reports that arrayref 0.3.10 was published with a direct dependency on proc-macro1, which would execute a malicious build script. The advisory states that the compromised version was downloaded 2,285 times, constituting less than 10% of arrayref download traffic across all versions because most users had older versions in their lockfiles. It also records that versions <=0.3.9 were unaffected and that there was no patched version at the time the advisory was issued.

RUSTSEC-2026-0265 describes proc-macro1 as containing a build script that would download a malicious payload. It records two versions published on 20 August 2026, notes that the crate was removed and related accounts were locked, and links the crate to the malware campaign targeted at arrayref users. The advisory credits the Research Team at Nextron Systems GmbH for reporting the issue to the Rust Security Response Working Group.

The operational lesson is narrow and important: a trusted crate name can become a delivery vehicle when an account or development environment is compromised. The malicious behavior did not require downstream code to call a suspicious API in the normal path. It depended on dependency relationships and build-time execution, which makes dependency diffs and build environments part of the security boundary.

Where Rust builds execute dependency code

Cargo resolves dependencies from Cargo.toml and records exact selections in Cargo.lock. A lock file is valuable because it makes dependency selection explicit and prevents silent drift, but it does not make the selected versions safe. The security-relevant question during an update is what changed: which versions moved, which new packages entered the graph, and whether any new dependency introduces a build script or procedural macro.

Compilation adds more trust boundaries. Cargo's build script documentation explains that a package can place a build.rs file at its root; Cargo compiles and runs that script before building the package. Build scripts can perform legitimate tasks such as compiling bundled C code or generating bindings, but they also run with access to the build environment. Procedural macro crates execute compiler-side code as well. In practice, "I do not call this function" is not the same as "this dependency does no work on my machine or CI runner."

Transitive dependencies are part of the same problem. In the arrayref incident, the downstream effect came through a dependency added to a republished version. A direct dependency can look ordinary while a newly introduced package lower in the graph provides the payload. When reviewing a change, inspect the resolved graph rather than only top-level manifest edits:

cargo tree
cargo tree -i proc-macro1
cargo metadata --format-version 1

cargo tree -i <package> is useful during incident response because it shows which path pulled a package into the build. cargo metadata can help scripts and CI checks collect the exact resolved dependency graph in machine-readable form.

For CI and release jobs, locked builds keep dependency selection stable until a change is intentionally reviewed:

cargo build --locked
cargo test --locked

--locked causes Cargo to use the existing Cargo.lock and fail if the manifest would require changes to the lock file. That makes dependency updates visible in version control. When an update is intentional, change dependencies deliberately, inspect the Cargo.lock diff, run the project's normal checks, and treat a new build dependency, proc-macro, or unfamiliar transitive crate as a review item rather than background noise.

Advisories, yanking, and registry response

The Rust ecosystem has several different signals for dependency problems, and they do not mean the same thing. An advisory records that a crate or version has a known issue. Yanking prevents new projects from selecting a version by default, but it does not rewrite history for every existing lock file. Deletion from a registry is a stronger incident response action, but local caches and mirrored build environments may still contain the original files.

The August 2026 response combined several of these actions. The Rust blog states that the malicious crates were deleted, the malicious versions of affected popular crates were removed, maliciously yanked versions were unyanked, and the affected account was locked. RustSec published advisories with the categories and references needed for automated tooling and follow-up investigation. RUSTSEC-2026-0260 marked arrayref 0.3.10 as affected, listed <=0.3.9 as unaffected, and linked to the Rust blog post. RUSTSEC-2026-0265 recorded the malicious proc-macro1 crate and connected it to the same campaign.

For defenders, the useful habit is to read these signals together. A yanked crate version in a dependency update deserves review, but it is not automatically proof of malice; maintainers also yank versions for accidental breakage. A RustSec advisory, especially one categorized as malicious, is stronger evidence that a specific version or package should be removed from builds. The arrayref incident shows why the exact version matters: the response named specific versions and time windows, and the RustSec advisory for arrayref identified a narrow affected range.

Checks that catch known problems

Automated checks do not prove that a crate is trustworthy, but they reduce the chance of shipping known-bad dependencies and make dependency policy explicit. The right mental model is layered defense: each tool answers a different question.

cargo-audit checks Cargo.lock against the RustSec Advisory Database and reports vulnerable or reported crates. It is useful in CI because it can turn a known advisory into a failing build or a visible warning that requires triage. Its README documents installation and basic use:

cargo install cargo-audit
cargo audit

cargo-deny covers broader dependency policy. The cargo deny check command can run advisories, bans, license checks, and source checks over the dependency graph. That makes it useful for questions cargo-audit does not answer on its own: whether dependencies come from approved registries or git sources, whether duplicate versions are acceptable, which crates are banned, and how a team wants to handle advisories during upgrades.

cargo deny check

cargo-vet supports a review workflow rather than a vulnerability scan. It helps teams record audits, trust decisions, and policy for third-party crates, including the difference between a first audit and a version-to-version review. This is most useful when a project wants dependency changes to carry explicit human review evidence. In the context of incidents like arrayref, vetting records can make it easier to answer whether a newly published version was examined, trusted through an organization policy, or simply pulled in because it satisfied a semver range.

These tools are complementary. Advisory scanning answers, "Is this exact dependency known to be vulnerable or reported?" Policy checks answer, "Is this dependency allowed by our rules?" Vetting workflows answer, "Has someone we trust examined this dependency or the delta between versions?" None of them replace reviewing a suspicious Cargo.lock diff, but together they make risky changes easier to detect and discuss.

Incident response for Cargo projects

After a crates.io incident report, teams need fast local answers. The Rust blog post for the August 2026 attack recommended checking ~/.cargo/registry/cache for the deleted malicious .crate files. The command published by the Rust Security Response Team was:

find ~/.cargo/registry/cache -type f \( \
  -name 'append-only-vec-0.1.9.crate' -o \
  -name 'arrayref-0.3.10.crate' -o \
  -name 'internment-0.8.7.crate' -o \
  -name 'proc-macro1-*.crate' -o \
  -name 'proc-macro-en-*.crate' -o \
  -name 'aovine-*.crate' -o \
  -name 'arone-*.crate' -o \
  -name 'aronenao-*.crate' -o \
  -name 'tinymember-*.crate' \
\) -print

A clean cache does not prove that no system was affected. The same crate may have been fetched on CI runners, build containers, developer laptops, release builders, or mirrored caches outside the default Cargo home. Treat the local cache check as a first pass, then widen the search to every environment that can run Cargo for the affected project.

Lock-file history is the next place to look. If a repository committed Cargo.lock, use its history to see whether any of the named versions appeared during the publication windows reported by the Rust Security Response Team. If the project does not commit a lock file, CI logs, release artifacts, and preserved build outputs may be the only record of resolved versions. The goal is to connect a named crate version to a specific build or deployment decision.

Dependency graph inspection can answer a different question: not only whether a malicious crate file is present, but how it would have entered the build. cargo tree -i arrayref or cargo tree -i proc-macro1 can show the path from a workspace member to the package. For older incidents, reproducing the historical lock state in an isolated environment is safer than upgrading dependencies during the investigation.

Finally, follow project-specific guidance from the Rust blog, RustSec advisories, and maintainers of the affected crates. In this incident, the response made clear that some legitimate crate names were involved and that maliciously yanked versions were unyanked after cleanup. That kind of detail matters: an automated response that treats every yanked version as equivalent can erase important context. The immediate priority is to remove exposure, preserve evidence such as lock-file diffs and CI logs, and rebuild from a known-good dependency set.

Conclusion

Supply-chain attacks in Rust often exploit ordinary workflows: publishing a crate version, changing dependency edges, or running code during cargo build. The 2026 arrayref incident showed that even widely used crate names can be involved when an account or environment is compromised, and the RustSec advisories added version-level details such as download counts and unaffected ranges. Practical defense is layered: keep dependency selection reproducible with Cargo.lock and --locked, review dependency graph changes, run advisory and policy checks in CI, record dependency review decisions, and have a fast path for checking local caches and build environments when an incident report names exact versions.

Sources


← back to posts