Skip to content

Agent-mode cargo apply patches the wrong copy when cargo vendor uses a custom directory (or apply runs from a workspace member), yet reports success and VEX attests not_affected #338

Description

[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:

  1. 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.
  2. 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.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions