SECURITY ADVISORY / RUST TOOLING
A newly disclosed cargo-nextest flaw turns a shared portable test recording into a code-execution path during the build phase. Teams using the experimental recording feature should upgrade to version 0.9.150 or later before accepting another recording.
RustSec issued RUSTSEC-2026-0412 on October 27, 2026, one day after it was reported. The advisory covers cargo-nextest 0.9.125 through 0.9.149 when experimental run recording is enabled. A crafted portable recording supplied to cargo nextest run --rerun can inject Cargo build options and execute code with the privileges of the person or CI job running nextest. The project released 0.9.150 with a redesigned recording format and argument handling.
The decision in 60 seconds
- Affected: cargo-nextest 0.9.125 through 0.9.149, experimental recording enabled, and an untrusted portable recording is rerun.
- Fixed: upgrade to 0.9.150 or later and set a repository minimum version.
- Priority: treat CI runners with repository, registry, signing, or deployment credentials as the highest-risk assets.
- Do not: inspect, replay, or rerun a suspicious recording on a production workstation or CI runner.
What changed and why it matters
Portable recordings are meant to move test-run information between machines so another developer can investigate a failure. That workflow creates a trust boundary: data from one machine becomes input to a build on another. In the affected releases, nextest recorded the original run’s build scope as raw Cargo arguments and later passed those arguments to Cargo without sufficient validation.
A malicious recording can therefore add an option such as --config. Cargo configuration can control executable build components, including build.rustc-wrapper, the compiler, target linkers and runners, environment variables, and dependency source replacements. The injected behavior runs during cargo build, before a test starts.
This timing is the crucial operational detail. Previewing recorded tests with replay does not make a hostile recording safe because the build has already happened by the time tests would run. A payload can also invoke the legitimate compiler afterward, allowing the build and tests to appear normal. The only expected nextest clue may be a line that says the rerun is inheriting its build scope.
Affected-version matrix
| cargo-nextest version | Recording state | Assessment | Action |
|---|---|---|---|
| Earlier than 0.9.125 | Any | Unaffected by this advisory | Use normal lifecycle policy |
| 0.9.125 to 0.9.149 | Disabled | Vulnerable code present, exploit condition absent | Keep disabled and upgrade |
| 0.9.125 to 0.9.149 | Enabled | Affected when rerunning an attacker-supplied recording | Stop reruns and upgrade immediately |
| 0.9.150 or later | Any | Patched for RUSTSEC-2026-0412 | Verify the minimum-version policy |
The RustSec advisory assigns a CVSS 3.1 score of 7.8. That score is advisory context, not proof that every installation is exposed. Exploitation still requires the experimental feature and user interaction with an attacker-controlled recording. No source reviewed for this article reports exploitation in the wild.
Find affected developer and CI systems
Start with the places that install developer tooling: workstation bootstrap scripts, CI images, tool-version managers, containerfiles, and workflow setup steps. Check the effective binary, not only a repository manifest.
cargo nextest --version
cargo install --list | sed -n '/cargo-nextest/,+1p'
rg -n 'NEXTEST_EXPERIMENTAL_RECORD|--rerun|-R|nextest-version|cargo-nextest' \
.github .gitlab-ci.yml .circleci .config scripts Dockerfile* 2>/dev/nullDo not assume the absence of NEXTEST_EXPERIMENTAL_RECORD in the repository proves the feature is disabled. CI platform variables, organization-level secrets, reusable workflows, base images, and shell profiles can supply it outside source control. Record the binary path and image digest so remediation can target the actual runner.
Patch and contain the trust boundary
1. Upgrade the executable
Move every affected installation to cargo-nextest 0.9.150 or later using the installation method your organization already controls. The nextest project notes that 0.9.150 uses recording store format 3.0. Recordings from 0.9.149 and earlier can no longer be replayed or rerun and should be discarded.
# Example for a cargo-installed binary. Follow your pinned toolchain policy.
cargo install cargo-nextest --locked --version 0.9.150
cargo nextest --version2. Enforce a repository minimum
Add the minimum to .config/nextest.toml. This gives contributors and CI a clear version gate instead of relying on documentation alone.
nextest-version = "0.9.150"3. Disable and isolate recording intake
- Remove
NEXTEST_EXPERIMENTAL_RECORD=1until all runners are patched. - Do not accept recordings through pull-request artifacts, issue attachments, chat links, or process substitution from untrusted contributors.
- If external recordings are operationally necessary after patching, download them as inert artifacts, scan and retain hashes, and process them on a least-privileged runner without production secrets.
- Separate untrusted pull-request jobs from release, signing, package-publishing, and deployment jobs.
This is the same trust-minimization principle behind moving npm publishing to short-lived OIDC credentials: an input-processing job should not automatically inherit release authority. Teams maintaining GitHub integrations can also review the site’s GitHub App token migration guide for reducing long-lived credential exposure.
Detection and incident response
There is no single log message that proves exploitation. Build a timeline around any run that consumed an external portable recording on an affected version. Preserve workflow logs, artifact hashes, runner image identifiers, process telemetry, network logs, and identity-provider events before rotating credentials.
- Search nextest output for
rerun: inheriting build scopeand correlate it with the recording source. - Look for unexpected Cargo configuration such as
build.rustc-wrapper,build.rustc,target.*.linker,target.*.runner,env.*, or dependency source replacement. - Review child processes and outbound connections created during the build step, including activity that precedes all test output.
- Identify credentials available to the job: repository tokens, cloud identities, package registry tokens, signing keys, and deployment credentials.
- If execution is plausible, revoke or rotate exposed credentials, invalidate runner state, and rebuild from a known-good image.
A successful test result is not exculpatory. The advisory explicitly notes that malicious behavior can continue into a normal compiler invocation. Treat the build phase as the event of interest.
Hands-on lab: classify exposure without opening recordings
This lab is intentionally non-exploitative. It reads a small CSV inventory and classifies versions and exposure conditions. Run it offline in a disposable directory with Python 3.9 or later. It never invokes cargo, nextest, or a recording.
Prerequisites
- Python 3.9 or later
- No network access is required
- Inventory facts collected from your asset or CI configuration system
asset,version,experimental_recording,accepts_external_recordings,ci_secrets
runner-prod,0.9.149,yes,yes,yes
dev-laptop,0.9.130,yes,no,no
legacy-runner,0.9.124,yes,yes,yes
fixed-runner,0.9.150,yes,yes,yes
custom-image,0.9.149-custom,yes,yes,yes#!/usr/bin/env python3
import csv, re, sys
def version_tuple(value):
match = re.fullmatch(r"\s*(\d+)\.(\d+)\.(\d+)\s*", value)
return tuple(map(int, match.groups())) if match else None
def classify(row):
version = version_tuple(row["version"])
if version is None: return "MANUAL_REVIEW"
if version < (0, 9, 125): return "UNAFFECTED_VERSION"
if version >= (0, 9, 150): return "CURRENT"
recording = row["experimental_recording"].lower() == "yes"
external = row["accepts_external_recordings"].lower() == "yes"
secrets = row["ci_secrets"].lower() == "yes"
return "UPDATE_CRITICAL" if recording and external and secrets else "UPDATE"
with open(sys.argv[1], newline="", encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
print("asset,version,status")
for row in rows:
print(f'{row["asset"]},{row["version"]},{classify(row)}')Save the files as inventory.csv and nextest_inventory.py, then run:
python3 nextest_inventory.py inventory.csvExpected output
asset,version,status
runner-prod,0.9.149,UPDATE_CRITICAL
dev-laptop,0.9.130,UPDATE
legacy-runner,0.9.124,UNAFFECTED_VERSION
fixed-runner,0.9.150,CURRENT
custom-image,0.9.149-custom,MANUAL_REVIEWTroubleshooting and cleanup
If a packaged version includes a suffix or does not follow three-part semantic versioning, the script returns MANUAL_REVIEW instead of guessing. Confirm that build through its package manager or image manifest. When finished, delete the disposable lab directory. Keep the resulting inventory only in your approved evidence store; do not add real secret values to the CSV.
Verify the fix
- Confirm every effective binary reports 0.9.150 or later.
- Confirm
.config/nextest.tomlsetsnextest-version = "0.9.150"or a newer approved minimum. - Remove old portable recordings because the fixed release intentionally rejects the older format for replay and rerun.
- Test a normal nextest workflow using repository-owned test inputs only.
- Re-run the inventory and require
CURRENTfor all standard runners.
Conclusion
RUSTSEC-2026-0412 is a useful reminder that developer diagnostics are executable supply-chain inputs once they cross into a build. Upgrade cargo-nextest to 0.9.150 or later, enforce the minimum in the repository, discard legacy recordings, and keep untrusted artifacts away from credential-bearing runners. If an affected runner processed an external recording, investigate the build phase even when the tests finished successfully.
Primary references
- RustSec Advisory Database: RUSTSEC-2026-0412, issued October 27, 2026
- cargo-nextest 0.9.150 changelog, released October 26, 2026
- nextest security-fix commit 456818b
- nextest security-fix commit 59ef7af





