They yanked the safe versions first
On August 20 someone published a poisoned arrayref to crates.io and, sixteen seconds later, withdrew the safe versions behind it. Cargo's own warning then pointed at the only modern release left standing. The payload was a build script, so type-checking the project was enough to run it. Bromure Agentic Coding turns that whole operation into a stopwatch problem it loses: the poisoned crates lived 86 minutes, and the default age gate is two days.
The attacker did not have to trick anyone into installing malware. They made the clean versions disappear, and let the registry's own upgrade advice do the rest.
At 07:15:00 UTC on August 20, [email protected] appeared on crates.io under its real
maintainer's account. Twenty-four seconds later the attacker began yanking versions 0.3.5
through 0.3.9: sixteen seconds of scripted work to clear the shelf behind it.
Yanking is crates.io's recall mechanism, the lever a maintainer pulls when a release turns out broken. Cargo answers it the way you would want. It warns you that a version in your lockfile has been withdrawn, and it refuses to pick a yanked version on a fresh resolve. Every piece of advice that follows such a warning tells you to update.
So the attacker withdrew the safe versions, left one modern release standing, and let doing the responsible thing walk you into it.
Ninety minutes, start to finish
The Rust Security Response WG's advisory and the timeline StepSecurity reconstructed describe an operation the attacker had been staging for two days.
On August 18 at 01:25:58 they registered a crates.io account named dtolney, a letter
away from David Tolnay, whose name sits on most of the proc-macro crates in the ecosystem.
Half an hour later they published a clean [email protected], a typosquat of
proc-macro2 that did nothing at all. That release existed to give the account a history.
The real one came five hours later. [email protected] went up at 07:11:15 on August 20
carrying a malicious build.rs. Four minutes after that, [email protected] went out from
the legitimate owner's account with one change in it: a new dependency line pointing at
proc-macro1 ^1.0.107. The Security Response WG is careful about the maintainer here, and
correct to be. They "do not believe the author of arrayref to be acting maliciously," and
note that "their computer or credentials are likely compromised." [email protected]
followed at 07:34:07, [email protected] at 07:37:49.
Then it ended nearly as fast as it started. Nextron Systems' research team found the
malicious crate and reported it. crates.io pulled arrayref after 86 minutes, internment
after 90 and append-only-vec after 107, deleted six attacker-owned crates, and locked the
compromised account. RustSec recorded no evidence that anyone built against a poisoned
version.
Ninety minutes is a fast response. For anyone whose build ran inside the window, it still arrived afterwards.
Nothing in the library had to be called
The payload lives in a build script, and build scripts run at compile time with your
privileges. The Hacker News reports the payload firing during
cargo build, cargo check, and cargo test.
That middle one is the cautious option: what your editor runs on save, what you type when
you want the compiler's opinion without running anything. Here it was enough.
The script itself is small and unsubtle once you have it in front of you. It reassembles
https://23.254.165[.]112:9089/ from base64 fragments, turns off TLS certificate
verification, downloads a payload matched to your OS and architecture, and runs it
detached. The nice touch is std::mem::forget(child), which drops the handle so the
process escapes Cargo's job object. The implant keeps running and the compiler exits with
a green build.
What comes down next is an infostealer.
Wiz's analysis
has it beaconing over HTTPS POST to /49890878, collecting hostname, username, OS and
installed applications, and querying the Chrome, Brave and Edge SQLite login databases for
saved credentials. It persists as a Registry Run key on Windows, a LaunchAgent on macOS
and a systemd user service on Linux, and if its C2 goes dark it falls back to a domain
generation algorithm producing ten .com domains every five days. Wiz ties the
infrastructure to DPRK activity: the same /49890878 endpoint as the Mastra npm campaign,
a shared certificate issuer, and repeated use of one Hostwinds range.
The builds it shipped cover x86_64 Linux, Windows and macOS, plus aarch64 macOS. That last one is an Apple Silicon laptop. Somebody thought about who compiles Rust.
The dependency you never chose
arrayref has 245 million lifetime downloads, and almost nobody put it in a Cargo.toml.
StepSecurity counted 406 dependent crate versions, with arrayref sitting under winit
and therefore under egui and iced, under blake3 and blake2b_simd, and under a good
deal of Ethereum and Solana tooling. internment and append-only-vec add another 19
million downloads between them.
So the advice to review your dependencies before adding them does not reach this. Nobody added it. It arrived four levels down, under a crate you did add, in a lockfile diff that scrolls past. Reading the library code would not have helped either. Nobody touched the source of the three poisoned crates; the whole change was one dependency line naming a crate two days old, published by an account whose name looked right.
Why a coding-agent blog cares
Look at the trigger again. A yank warning appears in build output. Something clears it by
running cargo update.
That something is often no longer a person. Clearing a warning is the housekeeping an agent does on the way to whatever you asked for: it sees a withdrawn version, it knows the fix, it applies the fix. It works while you sleep, in a loop, across a backlog. It refreshes lockfiles because a build failed and adds crates because a feature needed one.
An agent working unattended is the actor most likely to have been inside an 86-minute window, and the one least likely to stop over an unfamiliar transitive dependency published by a name it recognises. Give it a compromised maintainer account, a typosquat a letter off David Tolnay, and a registry warning recommending the upgrade, and nothing in the chain is left for judgment to catch.
You can survive that, because judgment is a poor place to decide this anyway.
Bromure makes it a stopwatch problem
Every Bromure Agentic Coding profile ships with an age gate on, set to two days. The host MITM proxy sees every package fetch the VM makes, across npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go modules and Packagist, and versions younger than the cutoff do not come back.
Cargo gets the full treatment. Bromure recognises index.crates.io, crates.io and
static.crates.io, then rewrites the crate metadata on the way through: it strips
too-fresh versions out of the version list and sets max_version, max_stable_version and
newest_version to the newest release that survives the filter. Cargo's resolver never
learns that a newer version exists.
Now put the numbers side by side.
Nothing in this operation reached two days old. [email protected] was four minutes old
when arrayref named it as a dependency. Neither crate is visible to a resolver behind the
gate, so cargo update finds nothing new.
You will see that on your screen. With 0.3.5 through 0.3.9 yanked and 0.3.10 filtered out,
a fresh resolve of arrayref can come back unable to pick a version, which is an error
rather than a silent success. An existing Cargo.lock pinned to 0.3.9 keeps building,
because a yank blocks new resolution only. Either way you spend that Thursday morning
reading a resolver message instead of rotating every credential you own.
Bromure checks a second time, for the case where the resolver already had its answer. It
matches every artifact fetch, the .crate file itself, against the recorded publish time,
and returns a 451 that names the age: published 41 minutes ago, policy requires 2 days
minimum. A warm metadata cache buys the attacker nothing.
The dropper's first hop goes nowhere
Suppose you had turned the clock off. Someone exempts a package, or drops the cutoff for
an afternoon. The build script still has to reach 23.254.165.112 on port 9089.
Each Bromure profile carries its own egress policy, written as a short pf-style ruleset:
allow web crates.io
allow web static.crates.io
allow web index.crates.io
allow tcp api.anthropic.com:443
default deny
Your Mac enforces it at two layers that share one rule set: the virtual switch, matching on
destination IP and DNS-snooped hostname across every protocol, and the proxy, matching on
TLS SNI and HTTP method. A bare IP on port 9089 matches no rule in that file, so it falls
to default deny and the connection dies on the host side of the boundary. The stage-2
DGA, ten fresh .com domains every five days, meets the same wall, because a generated
name is still a name nobody put on the list. Every denial lands as an
egress.firewall row in Window → Security Timeline, with host, IP, port and verdict,
written where code inside the VM cannot reach it.
Nothing in there to steal
Read the recovery checklist that shipped with this incident: rotate SSH keys, cloud tokens, API tokens, signing keys. Purge the registry cache. Clear CI cache layers. Rebuild vendor directories. On every machine that built during the window.
A Bromure workspace answers most of that list with an empty room.
The real credentials never enter the VM. The ANTHROPIC_API_KEY in that guest is a
brm_… placeholder. The kubeconfig holds throwaway client certificates. The container
registry auth is a derived Basic string. The GitHub token is a fake, and the SSH key is a
per-profile ed25519 key generated for that workspace, not the one on your laptop. The host
MITM proxy substitutes the real value on the outbound request, and AWS goes a step further:
the host re-signs the SigV4 request with the real material, so anything routing around the
proxy gets InvalidSignatureException back from Amazon rather than a working call.
An infostealer sweeping that guest for credentials finds credentials. They parse, they look right, and they are worth nothing off the machine.
The rest of its shopping list sits on the wrong side of the hypervisor. Chrome, Brave and
Edge keep their login databases on your Mac, in a user profile the guest has no path to.
The macOS LaunchAgent it wants to write, the reason it shipped an aarch64 macOS build, has
no macOS to write to. It can install a systemd user service in the guest's own home, and
that layer has a button: Erase home… resets the profile's
/home/ubuntu to its post-clone state, and Reset to base… re-clones the workspace
system disk from the read-only base image.
And you already have the inventory
The last line of every remediation guide for this incident is a grep: search every
Cargo.lock on every machine for six crate names and three version strings, then search
~/.cargo/registry/cache for the tarballs. That is a fine instruction and a miserable
afternoon, and it only works on machines you still have.
Bromure records the answer as it happens. Every package fetch through the proxy writes a
supply_chain.fetch row into Window → Security Timeline with the ecosystem, package,
version, outcome and the reason behind the outcome, host-side, alongside every credential
swap and every firewall verdict. When the next advisory names a version and a two-hour
window, the question "did anything here fetch that?" is a search rather than an expedition.
Bromure Enterprise Manager rolls the same stream up across a fleet, which is the form the question arrives in: did any of the ninety machines running agents overnight build it.
The part worth keeping
crates.io did well here. Ninety minutes from report to removal, six attacker crates gone, the compromised account locked, a clear advisory, and no evidence anyone was hit. The Rust team also went out of its way not to blame a maintainer whose credentials were stolen, which is the right instinct and not always the common one.
Somebody will reuse the mechanism, though, because it is cheap and it is clever. A yank is a public, instant, one-line lever that makes a registry recommend an upgrade, and every ecosystem has one. Whatever runs your builds will see that recommendation and act on it, and the thing acting on it now often lacks a Thursday morning of context about which crate is which.
So put the decision somewhere that does not require context. Two days of daylight between a version's publication and your build is not a judgment about a package, a maintainer, or an account name that looks a letter off. It is a clock, and this operation had ninety minutes.
Sources: Rust Security Response WG, "Supply chain attack on arrayref" (Aug 20, 2026) · Wiz, "Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns" (Aug 20, 2026) · StepSecurity, "arrayref, internment, and append-only-vec Poisoned by the proc-macro1 Build-Time Dropper" (Aug 20, 2026) · The Hacker News, "Rust Supply Chain Attack Puts Build-Time Malware in Crates with 245 Million Downloads" (Aug 20, 2026) · BleepingComputer, "Hackers poison arrayref Rust crate to push infostealer malware" (Aug 20, 2026) · Semgrep, "Rust crates arrayref & append-only-vec compromised" (Aug 20, 2026)