Skip to content

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

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-cfgif

Expected vs actual

  • Expected: a crate whose Cargo.lock entry has no registry source (a [patch] / path resolution) isn't the crates.io package pkg:cargo/cfg-if@1.0.4 that the build consumes. Agent mode should skip it with a warning, or at least vex must not attest it. CLI_CONTRACT.md says vex attests 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 with user_authored_patch_entry (docs/ecosystems.md, "Your entries").
  • Actual: applied: 1, no warning, and a not_affected statement 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 that Cargo.lock resolves 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

  1. mikolalysenko commented on Oct 1, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Cargo). Confirmed on main 61cfb9b: in local mode CargoCrawler (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 whether Cargo.lock gives that name@version a crates.io source. 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

  2. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [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-url stand-in (/v0/orgs/<org>/patches/{batch,by-package,view,blob}), and cfg-if-1.0.4 was extracted in $CARGO_HOME/registry/src/index.crates.io-*/ from an earlier build.

    1. Override in .cargo/config.toml: [patch.crates-io] cfg-if = { path = "<fork>" }, with nothing in Cargo.toml. The lock has cfg-if 1.0.4 with no source. Reproduced 2/2.
    2. Git dependency, no [patch] at all: cfg-if = { git = "file:///…/gitfork" }. The lock has source = "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 --json returns status: success, applied: 1, and the unused registry/src/…/cfg-if-1.0.4/src/lib.rs gets patched. cargo build -v --locked --offline then 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. vex still emits not_affected / inline_mitigations_already_exist for pkg: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 whose Cargo.lock entry is sourced from the crates.io index.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 9, 2026

    @mikolalysenko
    CollaboratorAuthor

    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.

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