Skip to content
The Crates.io Hijack: What We Compile When We Trust
Article

The Crates.io Hijack: What We Compile When We Trust

The illusion of safety

We treat our package managers like magic boxes. We type cargo build or npm install, watch the terminal fill with green text and progress bars, and assume we are safe because we haven’t actually run the binary yet. We think of compilation as a read-only phase, a translation of source code into machine instructions that sits quietly on disk until we choose to execute it. It is an easy habit to fall into. We write our code, we capture our thoughts, and we let the tools handle the plumbing.

That is a dangerous assumption.

On August 20, 2026, the Rust supply chain took a direct hit that proved just how fragile this trust is. An attacker hijacked a prominent maintainer account to publish compromised versions of three widely used crates, including arrayref, a library with over 245 million all-time downloads. The payload didn’t wait for a runtime function call. It executed the moment developers or CI/CD pipelines compiled their projects.

The hijacked maintainer

The compromise began at the source of trust: a maintainer’s credentials. The Rust Security Response Team and the crates.io operations team discovered that the account of David Roundy (droundy), a respected contributor in the Rust ecosystem, had been compromised.

With access to Roundy’s account, the attacker published malicious updates to three of his packages: arrayref version 0.3.10, internment version 0.8.7, and append-only-vec version 0.1.9. The primary target, arrayref, is a foundational crate used to take references to arrays. It sits deep in the dependency trees of popular libraries like winit, tiny-skia, and sctk-adwaita, which means it is transitively pulled into most GUI applications built on the egui or iced frameworks.

To make the attack harder to trace, the upstream GitHub repositories for these projects were wiped. The entire github.com/droundy organization and its repositories vanished into 404 errors, leaving security teams to analyze the compiled package artifacts directly from the crates.io registry.

Reassembling the payload

The malicious version of arrayref did not contain any exploit code in its own source. Instead, the attacker added a single line to the crate’s manifest file, declaring a dependency on proc-macro1 version 1.0.107.

This was a typosquatted package designed to impersonate proc-macro2, the ubiquitous library that almost every macro-heavy Rust project relies on. To keep builds from failing and raising immediate alarms, the attacker mechanically renamed every instance of proc-macro2 to proc-macro1 in the library’s source. If you compiled a project using it, your code built perfectly.

The magic happened in the build script. The manifest of proc-macro1 declared three unusual build-time dependencies: base64, rustls, and ureq (a lightweight HTTP client). When Cargo compiled proc-macro1, it ran build.rs, which reassembled a remote payload host from base64 fragments:

const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];
const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"];

Decoded, these fragments pointed to https://23.254.165.112:9089/ for the payload download and 23.254.165.112:443 for command and control. The script implemented a custom TLS verifier that accepted any certificate, bypassing standard HTTPS validation. It then queried the host’s operating system and architecture to fetch a specific binary:

  • Linux (x86_64): Fetched rust-crate_0.1.0, wrote it to /tmp/rust-setup, and executed it.
  • Windows (x86_64): Fetched rust-crate_0.2.0, wrote it to %TEMP%\rust-setup.ps1, and launched it silently using a VBScript helper.
  • macOS (x86_64 / aarch64): Fetched rust-crate_0.3.0 or rust-crate_0.4.0, wrote it to /tmp/rust-setup, and ran it.

To prevent the compiler from hanging while the malware ran, the script detached the spawned process. On Windows, it routed the execution through wscript.exe to escape Cargo’s job object, then leaked the child process handle using std::mem::forget. The build finished instantly, leaving the secondary payload running silently in the background.

Bending Cargo to force the update

An attacker publishing a malicious version of a package is a common threat, but usually, existing lockfiles protect developers from automatically pulling it in. To bypass this, the attacker exploited a core feature of Cargo’s package resolution: yanking.

Immediately after publishing arrayref 0.3.10, the attacker yanked versions 0.3.5 through 0.3.9. When a crate version is yanked, Cargo will no longer resolve to it for new builds or updated lockfiles. Furthermore, if a developer runs a build that depends on a yanked version, Cargo prints a warning recommending that they upgrade to a version that is not yanked. By yanking the safe older releases, the attacker left 0.3.10 as the only valid, unyanked version in the 0.3.x range. Any new project or lockfile update was forced to resolve to the poisoned release.

The malicious versions remained live on crates.io for roughly 86 to 107 minutes before the Rust Security Response Team and registry operators intervened. They deleted the malicious releases, removed the typosquatted proc-macro1 crate, and un-yanked the legitimate older versions of arrayref to restore the ecosystem to a safe state. The incident was cataloged under advisory RUSTSEC-2026-0260. While the exposure window was short, the sheer volume of projects that pull in arrayref transitively means that any active CI runner or developer environment compiling Rust code during those two hours was potentially exposed.

Cleaning the cache

If you compiled any Rust projects on August 20, 2026, between 14:00 and 16:00 UTC, you need to audit your environment immediately. Because the malicious build script ran silently in the background, your compiler would not have thrown any errors, and your build would have completed successfully.

To secure your local machine and your build pipelines, follow these three steps:

  1. Purge your local Cargo cache. Cargo caches downloaded crate packages locally. Even though the malicious versions have been removed from crates.io, they may still reside in your local cache directory. Clear them by running:
rm -rf ~/.cargo/registry/cache/*
  1. Audit your lockfiles. Check Cargo.lock across all of your repositories. Ensure that your builds are pinned to known safe versions of arrayref (<= 0.3.9), internment (<= 0.8.6), and append-only-vec (<= 0.1.8). If you find a reference to the compromised versions, delete the lockfile and regenerate it.

  2. Isolate and rotate credentials. Any developer workstation or CI/CD runner that compiled a project during the exposure window must be treated as potentially compromised. The secondary payload had the ability to run arbitrary code with the privileges of the compiling user. Rotate all SSH keys, API tokens, and cloud credentials associated with those environments.

This incident is a reminder that our build tools are not sandboxes. When we run cargo build, we are not just compiling code. We are running it. We prune our own vaults and keep our notes clean, but we must start pruning our dependency graphs with the same skepticism.

Sources

Related