Repository navigation
Agent-mode cargo apply patches only the first of several registry/src index dirs (cargo 1.84 vs 1.85+ hashes), so one cargo keeps building the unpatched crate while apply and VEX report success #339
Description
[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
$CARGO_HOME/registry/src/ routinely holds more than one extracted copy of the same crates.io crate, one per registry-source directory:
index.crates.io-6f17d22bba15001fis used by cargo 1.70–1.84.index.crates.io-1949cf8c6b5b557fis used by cargo ≥ 1.85, where the source-id hash changed.github.com-1ecc6299db9ec823is the git index, the default before 1.70.
The agent-mode crawler lists every one of those directories, but the cargo arm of dispatch_find merges with merge_first_wins. So apply patches only the first copy in read_dir order, and reports applied/success. A project whose cargo reads one of the other directories keeps building the unpatched crate, and vex still attests not_affected.
This happens on any machine where two cargo versions share a CARGO_HOME. A typical case is a project pinned by rust-toolchain.toml to ≤ 1.84 on a machine whose default stable toolchain is ≥ 1.85.
Impact
apply exits 0 with success, and VEX says not_affected, but cargo build --locked for the pinned-toolchain project compiles the vulnerable sources. The docs promise the opposite ("the patch affects every project on the machine", docs/ecosystems.md "Cargo: shared registry cache", README.md and CLI_CONTRACT.md).
Repro
The patch is a hand-staged .socket/ (stage.py is below). It appends pub fn socket_patched() to cfg-if, and main.rs calls it.
W=$(mktemp -d); cd "$W"; export CARGO_HOME=$W/cargo-home; mkdir src
printf '[package]\nname = "app"\nversion = "0.1.0"\nedition = "2018"\n\n[dependencies]\ncfg-if = "=1.0.0"\n' > Cargo.toml
printf '[toolchain]\nchannel = "1.84.1"\n' > rust-toolchain.toml
echo 'fn main(){ println!("{}", cfg_if::socket_patched()); }' > src/main.rs
cargo +1.93.1 fetch -q # another project / the default stable toolchain filled the shared cache
cargo fetch -q # this project's pinned 1.84.1
ls cargo-home/registry/src/ # index.crates.io-1949cf8c6b5b557f index.crates.io-6f17d22bba15001f
python3 stage.py . pkg:cargo/cfg-if@1.0.0 "$(ls -d cargo-home/registry/src/*/cfg-if-1.0.0 | head -1)" src/lib.rs
socket-patch apply --json --offline # "status": "success", 1 applied
grep -c socket_patched cargo-home/registry/src/*/cfg-if-1.0.0/src/lib.rs
# index.crates.io-1949cf8c6b5b557f/...: 1
# index.crates.io-6f17d22bba15001f/...: 0
cargo build --locked --offline # (cargo 1.84.1) error[E0425]: cannot find function `socket_patched`
socket-patch vex --offline -O vex.json # "status": "not_affected"stage.py writes .socket/manifest.json (setup.manual = ["cargo"], one file src/lib.rs with beforeHash/afterHash as git-blob SHA-256) plus both blobs:
import sys, json, hashlib, os
proj, purl, src, rel = sys.argv[1:5]
def g(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
before = open(os.path.join(src, rel), "rb").read()
after = before + b"\n/// socket marker\npub fn socket_patched() -> u32 { 1 }\n"
s = os.path.join(proj, ".socket"); os.makedirs(os.path.join(s, "blobs"), exist_ok=True)
m = {"setup": {"manual": ["cargo"]}, "patches": {purl: {"uuid": "11111111-2222-4333-8444-555555555555",
"exportedAt": "2026-01-01T00:00:00Z", "files": {rel: {"beforeHash": g(before), "afterHash": g(after)}},
"vulnerabilities": {"GHSA-xxxx-xxxx-xxxx": {"cves": ["CVE-2024-1"], "summary": "s", "severity": "high", "description": "d"}},
"description": "m", "license": "MIT", "tier": "free"}}}
json.dump(m, open(os.path.join(s, "manifest.json"), "w"), indent=2)
for b in (before, after): open(os.path.join(s, "blobs", g(b)), "wb").write(b)Which copy gets patched depends only on the directory listing order, not on which cargo the project uses. With the order above, a project pinned to 1.93 is patched and one pinned to 1.84 isn't. If the order were reversed, it would be the other way round.
Expected vs actual
- Expected. docs/ecosystems.md says "Agent mode patches the crate in place wherever the crawler finds it. For a non-vendored crate that means the shared
$CARGO_HOME/registrycache: the patch affects every project on the machine." So every extracted copy ofname@versionunderregistry/src/*should be patched, the waymerge_variant_copiesalready does for gem's coexisting stores. At a minimum,applyshould warn that other copies remain, andvexmust not attest when the copy the project's cargo reads is unpatched. - Actual. Only the first listed copy is patched.
applyreportssuccess, andvexattestsnot_affected.
Matrix
The project is pinned with rust-toolchain.toml. The shared CARGO_HOME was filled by 1.93.1 and then by 1.84.1.
| OS | project cargo | result |
|---|---|---|
| Linux (sandbox, ext4) | 1.84.1 | fail: apply success, 1949cf… patched, 6f17d2… unpatched, build E0425, vex not_affected. Reproduced 2× |
| Linux (sandbox, ext4) | 1.93.1 | pass (its copy happens to be listed first) |
Linux (GH ubuntu-latest) |
1.84.1 | pass: 6f17d2… listed first and patched, 1949cf… unpatched |
Linux (GH ubuntu-latest) |
1.93.1 | fail: build FAIL, vex not_affected |
macOS (GH macos-latest) |
1.84.1 | pass (6f17d2… first) |
macOS (GH macos-latest) |
1.93.1 | fail: build FAIL, vex not_affected |
Windows (GH windows-latest) |
1.84.1 | fail: 1949cf… first, build FAIL, vex not_affected |
Windows (GH windows-latest) |
1.93.1 | pass |
On every OS, one of the two cargos is left unpatched while apply says success. Which one it is depends on the filesystem's listing order: on the GitHub Linux and macOS runners it's the current 1.93 toolchain that ends up unpatched.
Versions
- main
f6b7fb9(CLI 4.0.0): fails as above. - Release 4.0.0: identical (reproduced).
- Release 3.3.0: with the same staged manifest,
applyreportspartialFailureand patches nothing, so there was no false success there. I didn't bisect further.
Suspect code
crates/socket-patch-cli/src/ecosystem_dispatch.rs:290-302: the cargoscan_ecosystem!useson_match = merge_first_wins, so only one path per purl is kept across source roots.crates/socket-patch-core/src/crawlers/cargo_crawler.rs:272-283:get_registry_src_pathsreturns the index dirs in unsortedread_dirorder.
Probe run (the workflow on the throwaway branch bughunt/cargo/20260930-index-dirs): https://github.com/SocketDev/socket-patch/actions/runs/36742610070
Backlog review — 2026-10-08
Priority: P2 → P1. Multiple Cargo registry cache hashes leave a consumed copy unpatched while VEX reports success.
Activity
mikolalysenko commentedon Sep 30, 2026
[agent] Triaged:
priority:p2(Cargo). Not a duplicate, and no open or merged PR fixes it. Confirmed on mainf6b7fb9: the cargo arm ofdispatch_find(ecosystem_dispatch.rs) merges withmerge_first_wins, andget_registry_src_pathsreturns the index dirs inread_dirorder, so only one extracted copy per purl is patched. #338 is in the same crawler (get_crate_source_paths), but its cause is different: it never reads.cargo/config*source replacement. So the two are cross-linked, not clustered. A fix to one should keep the other in mind, since both change which copiesapplytargets.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Re-triage on main
2463257(after the v5 consolidation in #277): still reproduces. It also affects global mode.I set
CARGO_HOMEto a dir with two index dirs,registry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4andregistry/src/index.crates.io-6f17d22bba15001f/cfg-if-1.0.4, then ransocket-patch scan -g --mode agent --ecosystems cargo --json. It reportsapplied: 1withstatus: success, but only the1949cf…copy has the patchedsrc/lib.rs. The6f17d22…copy, which cargo ≤1.84 builds from, is untouched.CargoCrawler::crawl_allstill dedupes by PURL across index dirs (seenset,crates/socket-patch-core/src/crawlers/cargo_crawler.rs), so-g/--global-prefixruns hit the same first-dir-wins behaviour as project runs.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Re-checked on main
045d7ec, after #517 ("Fix agent vex checking only one installed copy", #516). This issue still reproduces: with tworegistry/src/index.crates.io-*dirs that each hold an unpatchedcfg-if-1.0.0,apply --offlinereports "1 of 1 targeted patch applied" but writes only the-1949cf8c6b5b557fcopy (the-6f17d22bba15001fcopy stays at the original bytes), andvex --offlinestill emitsnot_affected. The #516 every-copy rule doesn't reach cargo, which suggests the cargo crawler still returns only one copy per crate@version.Generated by Claude Code
mikolalysenko commentedon Oct 8, 2026
[agent] Re-checked on main
9472be4. This still reproduces, and it now also reaches the CI gate that #1029 extended to cargo:apply --checkpasses over the unpatched copy.Repro (Linux, cargo 1.93.1, private
CARGO_HOME, agent mode via a local patch-API stand-in):scan --mode agentpatchesregistry/src/index.crates.io-1949cf8c6b5b557f/cfg-if-1.0.4/src/lib.rs.registry/src/index.crates.io-6f17d22bba15001f/cfg-if-1.0.4with the original bytes. That's the dir cargo ≤1.84 builds from.socket-patch apply --check --jsonreturnedstatus: success, one eventskipped/already_patched, exit 0, twice.For comparison,
apply --checkon a single-copy cache correctly fails when that one copy'ssrc/lib.rsis put back to the original (failed/not_applied, exit 1). So the new check works per copy, but it only sees the copy the crawler's first-dir-wins dedupe returns (CargoCrawler::crawl_all'sseenset), the same root cause as above. A cargo ≤1.84 CI job that gates onapply --checkpasses, then builds the unpatched crate.Generated by Claude Code
cargo: run 16 ledger (#1135 filed, #864 verified, #339 commented)
mikolalysenko commentedon Oct 9, 2026
v5 triage: P2, not a release blocker. Retain multi-toolchain agent cache-copy selection at P2. This is real, but not a blocker for the default hosted/vendored first-run workflow.
This follows the maintainer's release scope: one normally completing CLI instance, prioritizing valid-lockfile patch/install behavior, compatibility, and actionable CLI UX.
mikolalysenko commentedon Oct 11, 2026
[agent] Claiming this issue (single-issue cluster; root cause: the cargo arm of
dispatch_findmerges registry index dirs first-wins, so only one extracted copy per crate@version is patched, checked or rolled back). Branch: agent/fix-cargo-every-registry-copy. Claim-ID: 2026-10-11T17:20:59Z-164d17Generated by Claude Code
mikolalysenko commentedon Oct 11, 2026
[agent] Draft PR: #1394
Generated by Claude Code
Patch every registry index copy of a crate