Repository navigation
Agent-mode cargo patches the unused registry copy of a crate the user overrides with [patch.crates-io], and VEX attests not_affected while the build links the user's unpatched fork #506
Description
[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
The project's root Cargo.toml overrides a crate with [patch.crates-io] cfg-if = { path = "local/cfg-if" }, and Cargo.lock records that crate with no source, because it resolves to the local path. A copy of cfg-if-1.0.4 from crates.io is still extracted under $CARGO_HOME/registry/src (from an earlier build, or from any other project on the machine).
scan --mode agent matches pkg:cargo/cfg-if@1.0.4 against that registry-cache copy, patches it, records the patch and reports applied: 1. Cargo never builds that copy: cargo build --locked --offline compiles the user's local/cfg-if. socket-patch vex --product … then verifies the patched registry copy and emits a not_affected / inline_mitigations_already_exist statement for pkg:cargo/cfg-if@1.0.4. The code that actually ships is the user's fork, which socket-patch never looked at.
Impact
- The VEX document suppresses the advisory for a crate whose built code is unpatched. A path fork made with
[patch.crates-io]is usually a copy of the upstream (vulnerable) version with local edits, so the claim is false in the common case. - scan/apply report success and give no hint that the override bypasses the patch.
- Vendored mode already refuses this exact situation (
user_authored_patch_entry, docs/ecosystems.md "Your entries"), and hosted mode has its own defect for it (Hosted cargo scan redirects a crate the user overrides with[patch.crates-io], silently dropping the override and breaking--locked#480). Agent mode is the only one that claims it succeeded.
Repro
This uses a local stand-in for the patch API (--api-url, serving /v0/orgs/<org>/patches/{batch,by-package,view} with one patch that appends pub fn socket_patched() to cfg-if-1.0.4/src/lib.rs, plus one GHSA). It's the same fixture shape as tests/in_process_agent_reapply.rs.
export CARGO_HOME=$PWD/home
cargo new -q --bin proj && cd proj
printf 'cfg-if = "=1.0.4"\n' >> Cargo.toml
cargo build -q # extracts registry/src/*/cfg-if-1.0.4
mkdir -p local/cfg-if/src
printf '[package]\nname = "cfg-if"\nversion = "1.0.4"\nedition = "2018"\n' > local/cfg-if/Cargo.toml
echo 'pub fn user_fork() -> u32 { 7 }' > local/cfg-if/src/lib.rs
printf '\n[patch.crates-io]\ncfg-if = { path = "local/cfg-if" }\n' >> Cargo.toml
cargo build -q && rm -rf target # Cargo.lock: [[package]] name = "cfg-if" version = "1.0.4" (no source)
A="--api-url http://127.0.0.1:18767 --org test-org --api-token fake --no-telemetry"
socket-patch scan --mode agent --json --yes $A # status: success, applied: 1
tail -1 $CARGO_HOME/registry/src/*/cfg-if-1.0.4/src/lib.rs # pub fn socket_patched() -> u32 { 1 } (patched, but unused)
echo 'fn main(){ println!("{}", cfg_if::user_fork()); }' > src/main.rs
cargo run -q --locked --offline # 7: the build links local/cfg-if, the unpatched fork
socket-patch vex --product pkg:cargo/consumer@0.1.0 --output doc.json $A --patch-server-url http://127.0.0.1:18767
# Wrote OpenVEX document with 1 statement
# statement: not_affected / inline_mitigations_already_exist, subcomponent pkg:cargo/cfg-if@1.0.4, GHSA-test-cfgifExpected vs actual
- Expected: a crate whose
Cargo.lockentry has no registrysource(a[patch]/ path resolution) isn't the crates.io packagepkg:cargo/cfg-if@1.0.4that the build consumes. Agent mode should skip it with a warning, or at leastvexmust not attest it. CLI_CONTRACT.md saysvexattests an agent-mode patch when "verification finds it applied", and verification has to look at the copy the build consumes. That's the rule spelled out for hosted references in the same table ("The installed copies the build consumes … are hash-verified"). Vendored mode refuses the same project withuser_authored_patch_entry(docs/ecosystems.md, "Your entries"). - Actual:
applied: 1, no warning, and anot_affectedstatement for a crate whose built code was never patched.
Matrix
| OS | cargo | Agent + user [patch.crates-io] path override of the patched crate |
|---|---|---|
| Linux | 1.93.1 (repo toolchain) | fail (3/3) |
| Linux | 1.97.0 (stable) | fail (1/1) |
| macOS / Windows | any | untested (crawler and VEX logic are OS-independent) |
Tested on main 61cfb9b. Not bisected. The crawler fallback has looked like this since before v5.
Suspect code
crates/socket-patch-core/src/crawlers/cargo_crawler.rs:136-190(get_crate_source_paths) and:219(find_by_purls): in local mode the crawler falls back to every$CARGO_HOME/registry/src/<index>dir and matches by<name>-<version>only. It never checks thatCargo.lockresolves that name@version to the crates.io registry, rather than leaving it sourceless because of a[patch]path override.- The agent-mode VEX verification reuses that crawler result, so it hash-verifies the unused registry copy.
Related: the ledger's open maintainer question on agent scope versus Cargo.lock. This case is narrower: the crate is in Cargo.lock, but resolved to a different source. Also related: #480 (the hosted twin) and #501 (the same "patched the shadowed copy, VEX attests" shape in Python).
Backlog review — 2026-10-08
Priority: P2 → P1. Cargo builds the user replacement while agent mode patches an unused registry copy and attests safety. This is an incorrect security result.
Activity
mikolalysenko commentedon Oct 1, 2026
[agent] Triaged as
priority:p2(Cargo). Confirmed on main61cfb9b: in local modeCargoCrawler(crates/socket-patch-core/src/crawlers/cargo_crawler.rs) falls back to every$CARGO_HOME/registry/src/<index>dir and matches by<name>-<version>alone, never consulting whetherCargo.lockgives that name@version a crates.iosource. Not a duplicate. Related to #480 (the hosted twin), but not clustered: #480 is in the hosted lock/manifest rewriter (formats/cargo/hosted.rs,patch/redirect), this one is in the crawler. A shared "lock entry is crates.io-sourced" helper could serve both, but each needs its own fix and tests. No open fix PR.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Cargo bug-hunt run 7, main
61cfb9b. The same defect fires without any[patch]: a plain git dependency is enough. Two more shapes reproduce it, both on Linux with cargo 1.93.1. I used the same local--api-urlstand-in (/v0/orgs/<org>/patches/{batch,by-package,view,blob}), andcfg-if-1.0.4was extracted in$CARGO_HOME/registry/src/index.crates.io-*/from an earlier build..cargo/config.toml:[patch.crates-io] cfg-if = { path = "<fork>" }, with nothing inCargo.toml. The lock hascfg-if 1.0.4with no source. Reproduced 2/2.[patch]at all:cfg-if = { git = "file:///…/gitfork" }. The lock hassource = "git+file:///…/gitfork#<sha>". Reproduced 2/2. This is a common shape, and it isn't really a user "override" at all.In both shapes,
scan --mode agent --jsonreturnsstatus: success,applied: 1, and the unusedregistry/src/…/cfg-if-1.0.4/src/lib.rsgets patched.cargo build -v --locked --offlinethen compiles<fork>/src/lib.rs(shape 1) or$CARGO_HOME/git/checkouts/gitfork-*/<sha>/src/lib.rs(shape 2), so the patched copy is never built.vexstill emitsnot_affected/inline_mitigations_already_existforpkg:cargo/cfg-if@1.0.4.Fix note: shape 2 has a lock
source, just not a crates.io one. A check like "skip entries whose lock block has no source" would miss it. The crawler, and the VEX verification built on it, should only match registry copies whoseCargo.lockentry is sourced from the crates.io index.Generated by Claude Code
mikolalysenko commentedon Oct 9, 2026
v5 triage: P2, not a release blocker. Retain Cargo agent-mode selection of overridden sources at P2. The default hosted source-preservation defect is separately blocked in #480.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.