[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-6f17d22bba15001f is used by cargo 1.70–1.84.
index.crates.io-1949cf8c6b5b557f is used by cargo ≥ 1.85, where the source-id hash changed.
github.com-1ecc6299db9ec823 is 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/[email protected] "$(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/registry cache: the patch affects every project on the machine." So every extracted copy of name@version under registry/src/* should be patched, the way merge_variant_copies already does for gem's coexisting stores. At a minimum, apply should warn that other copies remain, and vex must not attest when the copy the project's cargo reads is unpatched.
- Actual. Only the first listed copy is patched.
apply reports success, and vex attests not_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,
apply reports partialFailure and 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 cargo scan_ecosystem! uses on_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_paths returns the index dirs in unsorted read_dir order.
Probe run (the workflow on the throwaway branch bughunt/cargo/20260930-index-dirs): https://github.com/SocketDev/socket-patch/actions/runs/36742610070
[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_findmerges withmerge_first_wins. Soapplypatches only the first copy inread_dirorder, and reportsapplied/success. A project whose cargo reads one of the other directories keeps building the unpatched crate, andvexstill attestsnot_affected.This happens on any machine where two cargo versions share a
CARGO_HOME. A typical case is a project pinned byrust-toolchain.tomlto ≤ 1.84 on a machine whose default stable toolchain is ≥ 1.85.Impact
applyexits 0 withsuccess, and VEX saysnot_affected, butcargo build --lockedfor 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.pyis below). It appendspub fn socket_patched()tocfg-if, andmain.rscalls it.stage.pywrites.socket/manifest.json(setup.manual = ["cargo"], one filesrc/lib.rswithbeforeHash/afterHashas git-blob SHA-256) plus both blobs: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
$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.applyreportssuccess, andvexattestsnot_affected.Matrix
The project is pinned with
rust-toolchain.toml. The sharedCARGO_HOMEwas filled by 1.93.1 and then by 1.84.1.ubuntu-latest)ubuntu-latest)macos-latest)macos-latest)windows-latest)windows-latest)On every OS, one of the two cargos is left unpatched while
applysayssuccess. 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
f6b7fb9(CLI 4.0.0): fails as above.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