Key Takeaways
- arrayref 0.3.10 executed a backdoor during
cargo buildthrough a typosquattedproc-macro1dependency, with no function call from the crate required. - The crates were live for 86 to 107 minutes on August 20, 2026, and were deleted rather than patched. Pin arrayref to 0.3.9 or earlier.
- Any developer workstation or CI runner that built an affected project should be treated as compromised, with credentials rotated and artifacts rebuilt.
- Static verification failed on both sides: legitimate publishing account, unchanged source, one-line manifest diff.
- Lockfiles answer exposure. Only runtime telemetry on the build host answers whether the payload executed.
Executive Summary
arrayref 0.3.10 is a hijacked release of a widely used Rust utility crate that added one dependency, proc-macro1, whose build script downloaded and executed a remote binary at compile time. The release was live on crates.io for 86 minutes on August 20, 2026, alongside [email protected] and [email protected] published from the same maintainer account, and building any project that resolved it was sufficient to detonate the payload without calling a single function from the crate. There’s no patch and no CVE, because the remediation is deletion and rollback rather than an upgrade, and the affected population is developer workstations and CI runners rather than production workloads.
What is the arrayref Supply Chain Attack?
arrayref is a four-macro utility crate with roughly 245 million all-time downloads that sits deep in Rust dependency graphs through tiny-skia, sctk-adwaita, and winit, which places it under most Rust GUI work and under blake3 and the Ethereum and Solana components that depend on it.
Up to version 0.3.9 the crate had no build script and no runtime dependencies. Version 0.3.10 kept the macro source byte for byte and added a single line to the manifest:
[dependencies]
proc-macro1 = "1.0.107"That one line is the entire change, at least as far as a reviewer can see. proc-macro1 is a typosquat of proc-macro2, a crate with more than 154 million downloads, and the malicious logic lives in its build.rs. Cargo compiles and runs build scripts automatically during compilation, so resolving the dependency was enough. Code review of arrayref itself would have turned up nothing, because there was nothing in arrayref to find.
The Rust Security Response Team credits the Research Team at Nextron Systems with discovering the malicious proc-macro1 crate and reporting it, and assesses that the legitimate maintainer’s machine or credentials were compromised rather than the maintainer acting maliciously.
The delivery mechanism was Cargo’s own safety warning
One detail deserves more attention than it’s receiving. Within the same minute as publishing 0.3.10, the attacker yanked arrayref 0.3.5 through 0.3.9 under the compromised owner account. Cargo responds to a yanked dependency by telling the developer to consider updating to a version that isn’t yanked, and the only unyanked version left was the malicious one. The ecosystem’s mechanism for steering people away from bad releases became the funnel toward this one. The researcher who filed the RustSec advisory for arrayref, jhobern, says that’s exactly how they hit it.
Treat a sudden yank of multiple stable versions of a long-lived crate as a reason to investigate rather than a reason to upgrade.
Is arrayref 0.3.10 Actively Exploited?
The honest answer is mixed. But it’s worth stating precisely rather than rounding in either direction.
The RustSec advisories for all three crates record no evidence that any malicious version was used. At the same time, a victim reported command and control traffic to 23.254.167[.]216 in the advisory-db issue thread, which means at least one host ran the payload. The exposure windows were short: 86 minutes for arrayref, 90 for internment, 107 for append-only-vec.
Short windows aren’t the same as small blast radius. Any CI pipeline that resolves dependencies fresh on every run, rather than building with --locked against a committed Cargo.lock, was rolling the dice against those windows continuously. Teams that pinned and committed their lockfile before 07:15 UTC on August 20 were not exposed through resolution.
One correction worth noting, because a lot of the coverage has it wrong. Wiz initially reported browser credential theft and has since amended that: the implant queries the Chrome, Brave, and Edge SQLite login stores to enumerate saved logins and extension settings, and it doesn’t retrieve the encrypted credential material itself. Reset those passwords anyway, since the site and username inventory is useful targeting on its own, but don’t build your incident narrative around exfiltrated passwords that the implant didn’t take.
Which Packages Are Affected?
Hijacked releases, all published August 20, 2026 and now deleted from crates.io:
[email protected], published 07:15 UTC, deleted 08:41 UTC. Pin to 0.3.9 or earlier.[email protected], published 07:34 UTC, deleted 09:04 UTC.[email protected], published 07:37 UTC, deleted 09:25 UTC.
Attacker-controlled crates, all versions deleted: proc-macro1, proc-macro-en, aovine, arone, aronenao, tinymember.
The Rust Security Response Team unyanked the legitimate arrayref versions the attacker had yanked, and locked the maintainer account as a precaution. Since the compromised releases were deleted rather than superseded, there’s no patched version to move to.
How Does the arrayref Build-Time Attack Work?
The chain runs entirely inside compilation:
- Resolution. A project resolves
arrayref 0.3.10, directly or transitively, which pulls inproc-macro1 1.0.107. - Build script execution. Cargo compiles and runs
proc-macro1‘sbuild.rswith the full privileges of the compiling user. - C2 reconstruction. The script rebuilds a payload URL from Base64 fragments, resolving to
https://23.254.165[.]112:9089/. - TLS validation disabled. A custom
AcceptAllcertificate verifier removes the check that would otherwise fail against attacker infrastructure. - Platform-matched download. The script selects a payload for the host OS and architecture, covering
x86_64Linux, Windows, and macOS plusaarch64macOS. - Drop and execute. It writes to
/tmp/rust-setupon Unix or%TEMP%\rust-setup.ps1on Windows, then launches it as a detached process with the C2 beacon address passed as an argument. On Windows,%TEMP%\rust-setup-launch.vbsruns the launcher throughwscript.exe. - Build completes normally. Compilation succeeds, the crate behaves correctly, and nothing in the output suggests a second process is now running.
The second stage is a full backdoor. It beacons over HTTPS POST to the path /49890878 with host details and collected data as Base64-encoded JSON, persists through a Registry Run key on Windows, a LaunchAgent on macOS, or a systemd user service on Linux, and supports four commands covering termination, C2 reconfiguration, persistence installation, and downloading and running further scripts. Configuration is encrypted with AES-128-GCM under a hardcoded key, commands are authenticated with an embedded RSA-2048 key, and a domain generation algorithm produces 10 algorithmic .com domains every five days as a fallback if the primary C2 goes dark.

Why Static Verification Didn’t Catch This
We published our analysis of the keyv npm compromise 16 days before this one. That release carried valid GitHub OIDC provenance and a SLSA attestation from the real repository. Every supply chain integrity signal said it was authentic.
arrayref defeats the same layer from the opposite direction. The publishing account was legitimate and the source never changed. The manifest diff is one line, and that line names a crate whose spelling is off by a single character from one of the most-used packages in the ecosystem. Provenance was real, review found nothing, and the malicious behavior lived in a file that ships with the dependency rather than with the package under review.
Two incidents in 16 days, both landing in build and developer environments, both clean against static verification. The pattern in both cases is that the only signal separating the malicious build from a normal one is what the build host actually did while it was running.
What Build-Time Detonation Looks Like in Runtime Telemetry
A cargo build has a narrow and predictable behavioral envelope. It reads source, writes object files and binaries into target/, and talks to crates.io. This payload steps outside that envelope in ways that are individually observable and jointly conclusive.
The behaviors worth hunting on any host that compiled Rust on August 20:
- A build-script process spawning a network client and reaching a raw IP literal on a nonstandard port (
23.254.165[.]112:9089), with certificate validation disabled, from a process tree rooted incargo. - An executable written outside the build tree, specifically
/tmp/rust-setupor%TEMP%\rust-setup.ps1, followed by execution of that file. - A child process that outlives the build. Compilation is bounded; a process that persists after the build exits is the single clearest signal here.
- HTTPS POST to
/49890878from a developer laptop or CI runner. - A process reading Chrome, Brave, or Edge SQLite profile stores on a machine whose job is compiling code.
- New persistence appearing on a build host: an unrecognized
HKCURun entry, a LaunchAgent, or a systemd user service. Build hosts don’t normally acquire persistence. wscript.exeexecuting%TEMP%\rust-setup-launch.vbson Windows.- Bursts of failed DNS lookups to algorithmic
.comdomains on a five-day cadence, which is the DGA fallback and remains useful after the primary C2 is taken down.
Now the caveat. We haven’t observed this campaign’s infrastructure in Upwind customer environments or in our own research, and we’re not going to sign off on that as an all-clear. Given an 86-minute exposure window and detonation on build hosts, quiet production telemetry is the expected result whether or not anyone’s developers were hit. To be more explicit: the absence of a signal from a sensor that was never pointed at the relevant host isn’t evidence of anything.
How to Check if Your Systems Are Affected
Search your repositories and local caches first:
Shell
find ~/.cargo/registry/cache -type f \( \
-name 'arrayref-0.3.10.crate' -o \
-name 'internment-0.8.7.crate' -o \
-name 'append-only-vec-0.1.9.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' \
\) -printThen grep every Cargo.lock in your organization for the three hijacked versions and the six attacker-controlled crate names.
Lockfiles and caches answer whether you resolved the malicious dependency. They don’t answer whether the payload ran, and those are different questions with different remediation. For execution, check build hosts for the artifacts and persistence entries above, and pull CI logs from August 20 for outbound connections to 23.254.165[.]112 or 23.254.167[.]107, or any POST to /49890878.
Mitigation and Patching
Immediate:
- Pin arrayref to 0.3.9 or earlier. Remediation here is a rollback. There’s no fixed release, because the malicious versions were deleted.
- Treat any host that built an affected project as compromised. Rotate every credential, token, and key reachable from it, including CI secrets, cloud keys, and signing keys. Rebuild artifacts produced after exposure from clean sources, and replace ephemeral runners outright rather than cleaning them.
- Remove the payload and its persistence. Delete
/tmp/rust-setup,%TEMP%\rust-setup.ps1, and%TEMP%\rust-setup-launch.vbs, then audit for unrecognizedHKCURun entries, LaunchAgents, and systemd user services. - Reset browser-stored passwords for accounts whose logins were enumerated, and revoke the associated sessions.
Longer term:
Build with --locked against a committed lockfile so resolution windows can’t reach you. Review any new or changed build-dependencies entry with the same scrutiny you’d apply to runtime code, particularly networking crates like ureq, reqwest, or rustls appearing in a package with no reason to make network calls. And extend runtime coverage to CI runners and developer environments, because the exposure question is answerable from a lockfile and the compromise question only from telemetry on the host that ran the build.
What none of this protects against: a maintainer account compromise on a crate you’ve already vetted, published under legitimate credentials with unchanged source. Pinning limits the window. It doesn’t remove the trust relationship.
Indicators of Compromise
Packages: [email protected], [email protected], [email protected], proc-macro1 (all versions), proc-macro-en, aovine, arone, aronenao, tinymember
Network:
23.254.165[.]112:9089(stage-2 payload host)23.254.165[.]112:443(C2 passed as argv[1])23.254.167[.]107:443(stage-2 C2, live as of August 20)23.254.167[.]216(victim-reported C2)23.254.164.0/23(Hostwinds range shared with Mastra and axios campaigns)hwsrv-798836.hostwindsdns[.]comPOST /49890878
Files: /tmp/rust-setup, %TEMP%\rust-setup.ps1, %TEMP%\rust-setup-launch.vbs, stage-2 binaries named rust-crate_0.1.0 through rust-crate_0.4.0
Accounts: dtolney (impersonation account, published proc-macro1), droundy (legitimate maintainer, machine or credentials likely compromised, account locked)
Behavioral: build-script process spawning a network client to a raw IP on a nonstandard port; executable written to a temp path and executed during compilation; a child process surviving build exit; credential-store reads on a build host; new persistence on a build host; periodic NXDOMAIN bursts to algorithmic .com domains
How Upwind Helps
Upwind’s runtime sensor observes process, file, network, and persistence activity on the hosts it covers, and the behaviors this campaign produces sit well outside a normal build profile: cargo spawning a process that downloads and runs /tmp/rust-setup, that process reaching an unfamiliar endpoint over a nonstandard port, credential-store reads on a machine that compiles code, and a service or LaunchAgent appearing where none belongs. Correlated against a workload’s own baseline, that chain surfaces with the process tree and identity attached rather than as four disconnected alerts.
The condition attached to that is the whole argument of this post. Detection depends on the sensor covering the host where the code actually ran, and in this incident that host was a developer laptop or a CI runner. Coverage scoped to production workloads may see nothing at all, not because the behavior was subtle but because the detonation happened somewhere the sensor wasn’t. Our recommendation to customers, and to anyone else reading, is to extend runtime coverage across production workloads, CI runners, and developer environments before the next compromised maintainer account. Our SBOM and supply chain visibility answers which environments resolved the dependency; runtime telemetry on the build host is what tells you whether it ran and what it touched afterward.
Rust’s guarantees are real, and they’re earned in the part of the problem the language addresses. Memory safety and a strict type system don’t stop a dependency from executing arbitrary code before your program exists. Neither does provenance, and neither does review, when the change is one line and the account is legitimate. What’s left is the behavior on the host, which means the practical question for most teams isn’t whether they’d have caught this. It’s whether anything was watching the machine where it would have happened.



