[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
In agent mode the cargo crawler only looks for a cargo vendor tree at <cwd>/vendor. It never reads the project's .cargo/config* source replacement ([source.<name>] directory = "..."). When the directory cargo actually builds from is somewhere else, apply patches the wrong copy and still reports success. vex then attests not_affected, but cargo build --frozen --offline compiles the unpatched vendored sources.
I reproduced three shapes of this on the current main:
- Vendor directory with a custom name.
cargo vendor third_party (or crates-vendored, etc.) with the matching [source.vendored-sources] directory = "third_party". apply patches $CARGO_HOME/registry/src/.../cfg-if-1.0.0 (a copy cargo no longer reads for this project), and leaves third_party/cfg-if untouched.
apply run from a workspace member. The workspace root's vendor/ is wired through the root .cargo/config.toml. Running apply in member/ looks for member/vendor, doesn't find it, falls back to the registry cache and patches that instead.
- Stale or unwired
vendor/ directory (the reverse case). vendor/ exists but no source replacement uses it, so cargo builds from the registry cache. apply patches only vendor/cfg-if and the build links the unpatched registry copy.
On a fresh checkout with an empty CARGO_HOME, shape 1 reports package_not_installed and exits with partialFailure, even though the crate is sitting in the project's committed vendor directory.
Impact
apply reports success and VEX says not_affected, but the built binary contains the vulnerable code. This is the "VEX attestation for a patch that isn't actually applied" failure. Custom vendor directory names (third_party/, vendor/rust/, crates/vendor/) are common in monorepos and in distro and Bazel-adjacent setups, and running from a member directory is routine.
Repro (shape 1)
The patch is a hand-staged .socket/manifest.json plus blobs, the same way tests/e2e_vendor_cargo_build.rs does it. It appends pub fn socket_patched() -> u32 { 1 } to cfg-if's src/lib.rs, and main.rs calls it, so the build compiles only if cargo links the patched bytes.
W=$(mktemp -d); cd "$W"; export CARGO_HOME=$W/cargo-home
cargo init -q --name app --vcs none && cargo add -q cfg-if@=1.0.0 && cargo generate-lockfile -q
mkdir -p .cargo && cargo vendor -q third_party >/dev/null
printf '[source.crates-io]\nreplace-with = "vendored-sources"\n\n[source.vendored-sources]\ndirectory = "third_party"\n' > .cargo/config.toml
echo 'fn main(){ println!("{}", cfg_if::socket_patched()); }' > src/main.rs
python3 stage.py . pkg:cargo/[email protected] third_party/cfg-if src/lib.rs # writes .socket/manifest.json + blobs, setup.manual=["cargo"]
socket-patch apply --json --offline # "status": "success", action "applied"
grep -c socket_patched third_party/cfg-if/src/lib.rs # 0 (the copy cargo builds)
grep -c socket_patched $CARGO_HOME/registry/src/*/cfg-if-1.0.0/src/lib.rs # 1 (a copy cargo ignores here)
cargo build --frozen --offline # error[E0425]: cannot find function `socket_patched`
socket-patch vex --offline -O vex.json # statement "status": "not_affected"
stage.py:
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)
- Shape 2: a virtual workspace
[workspace] members = ["m"], with cargo vendor vendor and the config at the root, and .socket/ in m/. Running cd m && socket-patch apply gives success, vendor/cfg-if patched=0, cache patched=1, and cargo build --frozen --offline fails with E0425.
- Shape 3: the same as shape 1 with
cargo vendor vendor but no .cargo/config.toml. apply gives success and patches vendor/cfg-if only. cargo build --offline uses the registry copy and fails with E0425.
- Control:
cargo vendor vendor plus the matching config, run from the root. apply patches vendor/cfg-if, rewrites .cargo-checksum.json, and the build prints 1. That passes.
Expected vs actual
- Expected. CLI_CONTRACT.md says "
apply patches the crate in place wherever the crawler finds it: the project vendor/ directory or the shared registry cache", and the .cargo-checksum.json rewrite exists precisely so that cargo build of a cargo vendor tree accepts the patch. So apply should patch the copy cargo actually builds, which is the directory that .cargo/config* source replacement names, looked up from the cwd and its ancestors the way cargo does. Failing that, it should refuse or warn, not report applied. vex must not attest a copy the build doesn't use.
- Actual.
apply hard-codes <cwd>/vendor, patches some other copy, and exits 0 with success. vex attests not_affected.
Matrix
Cargo is run with --frozen --offline after apply, then vex.
| OS |
cargo |
third_party/ (shape 1) |
control vendor/ |
| Linux (sandbox) |
1.93.1 |
fail (apply success, build E0425, vex not_affected), reproduced 2× |
pass |
Linux (GH ubuntu-latest) |
1.56.1 |
fail (apply success, dir 0 / cache 1, build FAIL, vex not_affected) |
pass |
Linux (GH ubuntu-latest) |
stable |
fail |
pass |
macOS (GH macos-latest) |
1.56.1 |
fail |
pass |
macOS (GH macos-latest) |
stable |
fail |
pass |
Windows (GH windows-latest) |
1.56.1 |
fail |
(not captured in log tail) |
Windows (GH windows-latest) |
stable |
fail |
pass |
Shapes 2 and 3 were reproduced on Linux, cargo 1.93.1.
Versions
- main
f6b7fb9 (4.0.0): fails as above.
- Release 4.0.0: identical.
- Release 3.3.0:
apply reports partialFailure with no events and patches nothing, so it doesn't claim success there.
Suspect code
crates/socket-patch-core/src/crawlers/cargo_crawler.rs:177-183: get_crate_source_paths returns <cwd>/vendor if it exists, otherwise $CARGO_HOME/registry/src/*. It never consults .cargo/config* [source.*].directory / replace-with, and never walks up to the workspace root.
crates/socket-patch-core/src/vendor/cargo.rs:178 is_vendored has the same hard-coded vendor/ assumption. As a result --mode vendored refuses already_vendored_in_tree for a stray, unwired vendor/ dir, yet proceeds for a wired third_party/.
Probe run (the workflow on the throwaway branch bughunt/cargo/20260930-vendor-dir): https://github.com/SocketDev/socket-patch/actions/runs/36742276728
[agent] Found by the scheduled Cargo bug-hunt routine (ledger #315).
Summary
In agent mode the cargo crawler only looks for a
cargo vendortree at<cwd>/vendor. It never reads the project's.cargo/config*source replacement ([source.<name>] directory = "..."). When the directory cargo actually builds from is somewhere else,applypatches the wrong copy and still reportssuccess.vexthen attestsnot_affected, butcargo build --frozen --offlinecompiles the unpatched vendored sources.I reproduced three shapes of this on the current main:
cargo vendor third_party(orcrates-vendored, etc.) with the matching[source.vendored-sources] directory = "third_party".applypatches$CARGO_HOME/registry/src/.../cfg-if-1.0.0(a copy cargo no longer reads for this project), and leavesthird_party/cfg-ifuntouched.applyrun from a workspace member. The workspace root'svendor/is wired through the root.cargo/config.toml. Runningapplyinmember/looks formember/vendor, doesn't find it, falls back to the registry cache and patches that instead.vendor/directory (the reverse case).vendor/exists but no source replacement uses it, so cargo builds from the registry cache.applypatches onlyvendor/cfg-ifand the build links the unpatched registry copy.On a fresh checkout with an empty
CARGO_HOME, shape 1 reportspackage_not_installedand exits withpartialFailure, even though the crate is sitting in the project's committed vendor directory.Impact
applyreports success and VEX saysnot_affected, but the built binary contains the vulnerable code. This is the "VEX attestation for a patch that isn't actually applied" failure. Custom vendor directory names (third_party/,vendor/rust/,crates/vendor/) are common in monorepos and in distro and Bazel-adjacent setups, and running from a member directory is routine.Repro (shape 1)
The patch is a hand-staged
.socket/manifest.jsonplus blobs, the same waytests/e2e_vendor_cargo_build.rsdoes it. It appendspub fn socket_patched() -> u32 { 1 }tocfg-if'ssrc/lib.rs, andmain.rscalls it, so the build compiles only if cargo links the patched bytes.stage.py:[workspace] members = ["m"], withcargo vendor vendorand the config at the root, and.socket/inm/. Runningcd m && socket-patch applygivessuccess,vendor/cfg-ifpatched=0, cache patched=1, andcargo build --frozen --offlinefails with E0425.cargo vendor vendorbut no.cargo/config.toml.applygivessuccessand patchesvendor/cfg-ifonly.cargo build --offlineuses the registry copy and fails with E0425.cargo vendor vendorplus the matching config, run from the root.applypatchesvendor/cfg-if, rewrites.cargo-checksum.json, and the build prints1. That passes.Expected vs actual
applypatches the crate in place wherever the crawler finds it: the projectvendor/directory or the shared registry cache", and the.cargo-checksum.jsonrewrite exists precisely so thatcargo buildof acargo vendortree accepts the patch. Soapplyshould patch the copy cargo actually builds, which is thedirectorythat.cargo/config*source replacement names, looked up from the cwd and its ancestors the way cargo does. Failing that, it should refuse or warn, not reportapplied.vexmust not attest a copy the build doesn't use.applyhard-codes<cwd>/vendor, patches some other copy, and exits 0 withsuccess.vexattestsnot_affected.Matrix
Cargo is run with
--frozen --offlineafterapply, thenvex.third_party/(shape 1)vendor/ubuntu-latest)ubuntu-latest)macos-latest)macos-latest)windows-latest)windows-latest)Shapes 2 and 3 were reproduced on Linux, cargo 1.93.1.
Versions
f6b7fb9(4.0.0): fails as above.applyreportspartialFailurewith no events and patches nothing, so it doesn't claim success there.Suspect code
crates/socket-patch-core/src/crawlers/cargo_crawler.rs:177-183:get_crate_source_pathsreturns<cwd>/vendorif it exists, otherwise$CARGO_HOME/registry/src/*. It never consults.cargo/config*[source.*].directory/replace-with, and never walks up to the workspace root.crates/socket-patch-core/src/vendor/cargo.rs:178is_vendoredhas the same hard-codedvendor/assumption. As a result--mode vendoredrefusesalready_vendored_in_treefor a stray, unwiredvendor/dir, yet proceeds for a wiredthird_party/.Probe run (the workflow on the throwaway branch
bughunt/cargo/20260930-vendor-dir): https://github.com/SocketDev/socket-patch/actions/runs/36742276728