Skip to content
HN On Hacker News ↗

Malicious Rust Crate arrayref Runs a Build-Time Payload

▲ 548 points 497 comments by abhisek 2d ago HN discussion ↗

Pangram verdict · v3.3

We believe this text is mainly AI, with some human-written content.

88 %

AI likelihood · overall

AI
9% human-written 91% AI-generated
SEGMENTS · HUMAN 0 of 2
SEGMENTS · AI 1 of 2
WORD COUNT 937
PEAK AI % 91% · §1
Analyzed
Aug 20
backend: pangram/v3.3
Segments scanned
2 windows
avg 469 words each
Distribution
9 / 91%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 937 words · 2 segments analyzed

Human AI-generated
§1 AI · 91%

Summary On August 20, 2026, a compromised release of the popular Rust crate arrayref appeared on crates.io. Version 0.3.10 added a dependency on a typosquatted crate called proc-macro1, whose build script downloads and runs a remote binary while a project compiles. The code runs at build time, so simply compiling a project that pulled the bad versions is enough to trigger it. The crates.io team has since removed the malicious versions. Packages involved The genuine arrayref and append-only-vec crates are maintained by droundy, whose account appears to have been compromised. The corresponding GitHub repositories are no longer available. github.com/droundy/arrayref, github.com/droundy/append-only-vec, and the entire github.com/droundy account all return 404, so the upstream code is no longer available for inspection. A separate account, dtolney, published proc-macro1. The username closely resembles David Tolnay’s real dtolnay account. Its metadata forges authors = ["David Tolnay <[email protected]>"] and points repository at a dtolnay/proc-macro1 path that returns 404. CrateVersionPublisherStatusarrayref0.3.10droundy (compromised)Malicious, removedproc-macro1all versionsdtolney (impersonation)Malicious typosquat, entire crate removedappend-only-vec0.1.9droundy (compromised)Flagged by reporters, same actorarrayref0.3.9 and earlierdroundyClean Note that proc-macro1 is not proc-macro2. The real crate that macro authors depend on is proc-macro2. The src/ of the malicious proc-macro1 is a genuine copy of proc-macro2, so builds kept working while the build script ran. What the build script does The payload lives in the build script of proc-macro1 1.0.107. It stores its server address as base64 fragments and reassembles them at build time, quoted in the advisory: // proc-macro1-1.0.107/build.rs (quoted in rustsec/advisory-db#3161)const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"]; Decoded, those fragments produce the payload host hxxps://23[.]254[.]165[.]112:9089/ and the command and control address 23[.]254[.]165[.]112:443. The script fetches an architecture-specific binary over a TLS connection that accepts any certificate without validation, then runs it detached from the build. On Unix it drops and runs /tmp/rust-setup. On Windows it writes a PowerShell script and a VBScript launcher under %TEMP% and starts them hidden, then abandons the child process so the compiler does not wait for it. How it spread The owner account yanked the older arrayref releases 0.3.5 through 0.3.9. Yanking a crate makes Cargo print a “consider updating to a version that is not yanked” warning, which nudges developers toward the only non-yanked release, the malicious 0.3.10. The reporter who filed the RustSec advisory noted this is how they hit it. arrayref is widely used as a transitive dependency. It sits deep in common Rust graphs through tiny-skia, sctk-adwaita, and winit, which places it under most GUI work built on egui, eframe, and iced. The crate has about 245 million all-time downloads (244,989,384 at time of writing), with the clean 0.3.9 release accounting for roughly 152 million. Those numbers measure how widely the crate is used rather than a count of affected builds. Indicators of compromise TypeIndicatorDetailNetwork23.254.165.112:9089Payload host (HTTPS)Network23.254.165.112:443C2, passed to the payload as argv[1]File (Unix)/tmp/rust-setupDownloaded executableFile (Windows)%TEMP%\rust-setup.ps1Downloaded PowerShell scriptFile (Windows)%TEMP%\rust-setup-launch.vbsVBScript launcherSecond-stage namesrust-crate_0.1.0, _0.2.0, _0.3.0, _0.4.0Chosen by OS and architecture SHA256 of the removed crate artifacts: ArtifactSHA256arrayref 0.3.1025ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373aeproc-macro1 1.0.10761198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4proc-macro1 1.0.106b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436 Part 2: Technical Analysis Our technical analysis covers the two crates behind this incident, arrayref 0.3.10 and proc-macro1 1.0.107. arrayref 0.3.10 pulls in a dependency called proc-macro1. The malicious code is in the build script of proc-macro1, not in arrayref itself. The injection point in arrayref arrayref is a small crate of four macros. Up to 0.3.9 it has no build script and no runtime dependencies. Version 0.3.10 keeps that macro source and adds one line to the manifest: [package]name = "arrayref"version = "0.3.10"build = false[dependencies.proc-macro1]version = "1.0.107" This [dependencies.proc-macro1] entry is sufficient to introduce the malicious crate. The requirement 1.0.107 is a caret range, and with only 1.0.106 and 1.0.107 ever published it resolves to the malicious 1.0.107. The crate’s own src/lib.rs is the ordinary macro code, for example the array_ref! macro: #[macro_export]macro_rules! array_ref { ($arr:expr, $offset:expr, $len:expr) => {{ { #[inline] const unsafe fn as_array<T>(slice: &[T]) -> &[T; $len] { &*(slice.as_ptr() as *const [_; $len]) } let offset = $offset; let slice = &$arr[offset..offset + $len]; #[allow(unused_unsafe)] unsafe { as_array(slice) } } }};} Nothing in the arrayref source references proc-macro1, and it does not need to. Cargo builds every declared non-optional dependency, whether or not the code uses it. So the manifest entry alone makes Cargo fetch and build proc-macro1 whenever a project pulls in arrayref 0.3.10, and building it runs the malicious build script. proc-macro1 is a renamed copy of proc-macro2 The src/ of proc-macro1 is proc-macro2 with a mechanical find-and-replace of proc-macro2 to proc-macro1. The rename reaches into documentation links and even copied issue references, for example html_root_url = "https://docs.rs/proc-macro1/1.0.107" in src/lib.rs and a github.com/dtolnay/proc-macro1/issues/235 link in src/fallback.rs. Because the library code is real proc-macro2, the crate works as a drop-in. This makes the malicious crate less noticeable during a normal build. The package metadata forges an identity: authors = ["David Tolnay <[email protected]>"]repository = "https://github.com/dtolnay/proc-macro1" The email [email protected] is not David Tolnay’s, and the dtolnay/proc-macro1 repository returns 404.

§2 Mixed · 46%

The suspicious difference is in the build dependencies, which real proc-macro2 does not have: [build-dependencies.base64]version = "0.22"[build-dependencies.rustls]version = "0.23"features = ["ring", "std", "tls12"]default-features = false[build-dependencies.ureq]version = "2"features = ["tls"]default-features = false Those three crates give the build script base64 decoding, a TLS stack, and an HTTP client. These dependencies are unusual for a token-parsing library, and the malicious build script uses them. The build script payload The build script splits the server address into base64 fragments and rebuilds it at compile time, so the raw string never appears in the source: const SRC_URL_PARTS: &[&str] = &["aHR0cHM6Ly8=", "MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "OTA4OS8="];const END_URL_PARTS: &[&str] = &["MjMuMjU0Lg==", "MTY1Lg==", "MTEyOg==", "NDQz"]; Decoded, SRC_URL_PARTS is hxxps://23[.]254[.]165[.]112:9089/ and END_URL_PARTS is 23[.]254[.]165[.]112:443.