Skip to content

Withhold a vendored Cargo copy a member's other source bypasses - #2024

Draft
Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
fix/cargo-mixed-member-pinsfrom
fix/cargo-vendored-twins-and-split-patches
Draft

Mikola Lysenko (mikolalysenko) wants to merge 3 commits into
fix/cargo-mixed-member-pinsfrom
fix/cargo-vendored-twins-and-split-patches

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #2019 (fix/cargo-mixed-member-pins). It closes the vendored-mode Cargo false-attestation gap the #2019 author found.

The second gap, a root pinning a crate to one Socket patch while a member pins it to another, is now handled by #2019 itself (beside_the_pin emits the other patch's Cargo.toml ref, so the CLI gates it as wiring_conflict). This PR no longer touches it. I ran my earlier split-patch tests against #2019's code alone and they pass, so there was nothing left to keep.

Vendored copy bypassed by another source

vendor wires a crate through the root's [patch.crates-io], and cargo applies that only to crates.io declarations. A workspace member (or the root) can declare the crate from another registry, a registry-index or a repository URL, at a requirement that may select the patched version. That declaration resolves its own unpatched copy. The vendored ref still attested beside it, because the lock copy claim (vendored_copy_claim) only reads sourceless entries.

  • The ref now stays (rollback, remove and the ledgers still see the wiring), but it's marked unattested with a new UnattestedKind::CargoPatchBypassed. The detail names the declaration.
  • A Cargo.lock entry at exactly the patched version from any registry or repository source, beside the copy, is marked the same way. The detail names that lock entry.
  • Declarations are read through Stop attesting a Cargo pin a workspace member resolves from crates.io #2019's DeclSource classification:
    • Only live declarations count, so a [workspace.dependencies] template no manifest inherits doesn't.
    • A registry dependency is covered when a [patch.<registry>] table pointing at the same copy redirects it (Decl::patched_by). A git dependency is covered by a [patch."<repository URL>"] table pointing at the copy (see the follow-ups below).
    • A path declaration (the user's own fork) is still ignored, as a_same_version_fork_beside_the_tagged_copy_is_ignored already specifies.
    • A socket-patch-* registry dependency is hosted wiring, handled by Stop attesting a Cargo pin a workspace member resolves from crates.io #2019.
  • The CLI omits the ref as vex_cargo_patch_bypassed. It's documented in CLI_CONTRACT.md (Unattested references paragraph and the notes table).

I added a new variant rather than reusing CargoUnpinnedDeclaration. That variant's note tells the user to re-run scan, which is the wrong fix for vendored wiring.

Member gate. #2019 reads workspace members only when the lock holds a Socket-sourced package (or, with no lock, a manifest names a socket-patch- registry). A vendored copy's lock entry has no source, so the members are now also read whenever the root manifest or the project config wires a Socket copy through [patch], whatever the lock's state.

Cargo follow-ups (second commit)

  1. Lockless pins use the same classification (unresolved_manifest_pins, UnlockedPin, only_this_patch).
    • A socket-patch-<uuid> registry beside path, git or registry-index is no pin, matching the rewriter's pinned check. It no longer feeds an UnlockedPin; it counts as an unpinned declaration that excludes the versions it accepts (all of them, without a requirement).
    • A [workspace.dependencies] template no manifest inherits is no declaration.
    • A non-Socket [patch] registry entry no longer counts as an unpinned declaration (it redirects, it declares nothing).
  2. DeclSource::Git keeps its URL. Decl::patched_by matches it against a [patch."<repository URL>"] table (a trailing / or .git ignored). A git declaration whose repository table points at the same vendored copy no longer withholds it, while one from another repository still does. Hosted routing gets the same match for a Socket registry pin in such a table.
  3. The no-pin conflict branch checks the requirement. When no declaration pins the lock's patch, a pin to another patch makes the conflict pair only when it is live and cargo_req_selects says it may select the locked version. Otherwise the stale lock entry is diagnosed and dropped like any unrouted one.

Tests

  • New tests that fail when the check is disabled:
    • a_member_from_another_source_bypasses_the_vendored_copy (another registry, a renamed =1.0.4, a repository URL, a registry-index)
    • a_same_version_lock_entry_from_any_source_bypasses_the_vendored_copy
    • a_member_from_git_bypasses_the_config_wired_copy
    • members_are_read_for_a_vendored_copy_without_a_socket_lock_entry (with and without a lock)
  • Guard that stays attested: declarations_the_patch_redirects_keep_the_vendored_copy_attested. It covers crates.io members in every spelling, including registry = "crates-io" and a rename; an excluding requirement; [patch.my-corp] coverage; a path fork; and an un-inherited workspace template.
  • CLI: a_vendored_cargo_copy_beside_a_bypassing_source_is_gated.
  • Follow-up tests. Each one fails when only its own fix is switched off:
    • lockless_path_or_git_beside_a_socket_registry_is_no_pin (path / git / registry-index member beside the root's pin, and alone)
    • lockless_uninherited_template_is_not_a_declaration (Socket-pinned template uninherited vs inherited; an uninherited crates.io template beside the root's pin)
    • a_git_patch_table_for_the_copy_covers_that_repository_only (covered with a .git/ spelling and a branch; another repository still bypasses)
    • another_patch_pin_excluding_the_locked_version_is_no_conflict
  • Results:
    • cargo test -p socket-patch-core --lib -- vex::discover:: vendor::cargo patch::redirect:: utils::cargo_workspace (includes the golden corpus test): 1564 passed.
    • -p socket-patch-cli --lib vex_sources: 16 passed.
    • --test in_process_redirect cargo, --test in_process_cargo_apply cargo, --test mode_migration_cargo: 6 / 3 / 30 passed.
  • cargo clippy --locked --workspace --all-features -- -D warnings and cargo fmt --all -- --check are clean.

Review findings (/code-review high, third commit)

Each of findings 1–6 was confirmed first by a test that fails on the previous commit and passes now.

  1. False attestation from a shadowed [patch] entry: confirmed and fixed. The bypass check counted a manifest entry cargo ignores (the config redefines its crates.io key with a git fork) as redirecting crates.io declarations, so the copy's honored [patch.my-corp] entry was attested beside the fork. Only honored entries count now. Test: a_shadowed_manifest_entry_covers_no_declaration.

  2. registry-index coverage: confirmed and fixed. A [patch."<index URL>"] table now covers a declaration of the same index (a registry+ prefix, host case and a trailing / ignored). A registry-index naming the crates.io git index is crates.io. Test: a_registry_index_patch_table_covers_that_index.

    • Partly declined: the sparse crates.io index (sparse+https://index.crates.io/) is not treated as crates.io. Cargo.lock records it as a distinct source string from the git crates.io index, and the files don't show that [patch.crates-io] reaches it. Treating it as covered could attest a copy cargo doesn't build, so it stays withheld. The cost is a missed attestation on a rare hand-written shape.
    • Not done: a registry = "<name>" declaration is still matched only by a [patch.<name>] table, not by its index URL (the registry's URL may live in $CARGO_HOME).
  3. Remedy per evidence: confirmed and fixed. The detail now ends with the remedy for its evidence:

    • a declaration: re-running vendor doesn't help;
    • a crates.io lock entry beside a [patch.crates-io] copy: stale, so re-lock (re-run vendor or cargo update -p <crate>);
    • another lock entry: cargo tree -i <crate>@<version> names the dependency that declares it.

    The CLI note's fixed suffix no longer makes that claim. Test: the_bypass_remedy_follows_its_evidence.

  4. Canonical git URLs: confirmed and fixed. URLs are compared as cargo's CanonicalUrl spells them: one trailing / and .git dropped, a GitHub URL lowercased whole, any host lowercased. Test: a_git_patch_table_matches_cargos_canonical_url.

  5. Unreflected pin to another patch lost its diagnostic: confirmed and fixed. Once the lock held any Socket entry of the crate, unresolved_manifest_pins skipped it, so such a pin got no ref and no diagnostic. It now gets patched_ref_unattributable unless the lock resolves the crate from that patch or a ref already carries it. Test: another_patch_pin_excluding_the_locked_version_is_no_conflict (extended).

  6. Ineffective Socket registry gets no diagnostic: confirmed and fixed. A well-formed socket-patch-<uuid> beside path / git / registry-index, or on a [workspace.dependencies] entry no manifest inherits, is now diagnosed patched_ref_invalid ("pins nothing there"). Test: an_ineffective_socket_registry_is_diagnosed.

    • This changes one committed golden. The redirect/cargo/cargo/workspace-member/expected tree omits the unchanged inheriting member a/, so its pinned template reads as uninherited there and now carries that diagnostic. I regenerated the golden; no other corpus project changed.
  7. socket_copies computed twice: fixed. It's computed once, before the root manifest moves; vendored = !wired_all.is_empty().

  8. Shared helpers: done. Decl::describe and same_version_foreign_entry are used by both beside_the_pin and patch_bypassed.

  9. One classifier: done without widening route. route already takes (table, uuid) pins, so the vendored check calls it with the copy's honored [patch] tables as the pins and the lock's other versions of the crate. That is the same classifier hosted pins use, with no new parameter. The only vendored-specific rule left is that a path declaration (the user's own fork) is ignored, an existing documented rule.

Not done

  • The sparse crates.io index isn't treated as crates.io (see review finding 2). It can only cause a missed attestation.
  • No vendored planner warning. vendor::cargo already refuses the lock-twin state: locked_multi_source_conflict covers a same-version entry from two sources, and vendor_source_unsupported covers a non-crates.io entry. A member declaration whose other-source copy isn't locked yet is invisible to the planner without re-resolving.

Generated by Claude Code

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Burn-down agent: resolved the merge conflicts with fix/cargo-mixed-member-pins (merge cdf9d61, no force-push).

  • The base branch was rewritten (06d306c became 77a4d2d, plus 650143e and b2e171b), so I replayed 684c5fb on the new base. Decl keeps this PR's DepSource and describe() on top of the base's socket_shaped() (which replaces this PR's identical socket_pinned()) and its pinned-version Decl::new.
  • Behavior change to review: 650143e walks member manifests only when the lock has a Socket-sourced package, so a_member_from_another_source_bypasses_the_vendored_copy and a_member_from_git_bypasses_the_config_wired_copy failed (they found no member declarations). read_members now also walks the members when the root manifest or the config wires a vendored Socket copy through [patch].
  • Local: cargo check -p socket-patch-core -p socket-patch-cli --all-targets passes, and cargo test -p socket-patch-core --lib vex::discover passes 466/466. Waiting on CI.

Generated by Claude Code

`vendor` wires a crate through the root's `[patch.crates-io]`, which
cargo applies only to declarations from crates.io. A workspace member
(or the root) that declares the crate from another registry, a
`registry-index` or git, at a requirement that may select the patched
version, resolves its own unpatched copy. The vendored ref stayed
attested beside it because the lock copy claim only reads sourceless
entries.

The ref now stays (rollback, remove and the ledgers still see the
wiring) but is marked unattested with the new
`UnattestedKind::CargoPatchBypassed`, naming the declaration. A
same-version Cargo.lock entry from any registry or git source beside the
copy is marked the same way, naming the lock entry. The check reads the
declarations through #2019's classification: live declarations only, a
registry dependency counts as covered when a `[patch.<registry>]` table
pointing at the same copy redirects it (`Decl::patched_by`), a path
declaration (the user's own fork) keeps the existing "ignored" rule, and
a `socket-patch-*` registry dependency is hosted wiring. The CLI omits
the ref as `vex_cargo_patch_bypassed`; documented in CLI_CONTRACT.md.

The workspace member walk ran only when the lock held a Socket-sourced
package (or, with no lock, a manifest named a `socket-patch-` registry).
A vendored copy's lock entry has no source and it has no such pin, so
the walk now also runs whenever the root manifest or the project config
wires a Socket copy through `[patch]`, whatever the lock's state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the fix/cargo-vendored-twins-and-split-patches branch from 5b08b24 to 64d73a6 Compare November 6, 2026 05:27
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Withhold Cargo pins a member's other source or other patch bypasses Withhold a vendored Cargo copy a member's other source bypasses Nov 6, 2026
Three Cargo follow-ups to the declaration classification:

Lockless pins read declarations like locked ones. A
`socket-patch-<uuid>` registry beside `path`, `git` or `registry-index`
is no pin (cargo builds that source, and the rewriter's own pinned check
agrees), so it no longer adds its requirement to an `UnlockedPin` or
makes one on its own. It counts as an unpinned declaration instead,
excluding the versions it accepts (every version, without a
requirement). A `[workspace.dependencies]` template no manifest
inherits is no declaration: a Socket-pinned one makes no lockless pin,
and an unpinned one excludes nothing from the root's pin. A non-Socket
`[patch]` registry entry no longer counts as an unpinned declaration
either (it redirects declarations, it adds none).

`DeclSource::Git` keeps its URL, and `Decl::patched_by` matches a
`[patch."<repository URL>"]` table against it (a trailing `/` or `.git`
ignored). So a git declaration whose repository table points at the
same vendored copy no longer withholds that copy, while one from
another repository still does. Hosted routing gains the same match for
a Socket registry pin in such a table.

When no declaration pins the lock's patch, a pin to another patch now
makes the conflict pair only when it is live and its requirement may
select the locked version (`cargo_req_selects`). One that excludes it is
about another version; the stale lock entry is then diagnosed and
dropped like any unrouted one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
False attestation: the vendored bypass check counted a declaration as
redirected by any root or config `[patch]` entry pointing at the copy,
including a manifest entry cargo ignores (the project config redefines
its crates.io key, or a crates.io URL table replaces `[patch.crates-io]`).
Cargo then builds whatever replaced it for those declarations, while the
copy's other, honored entry was attested. Only honored entries count now:
the root's `[patch]` entries are collected once with their shadow reason
(a `Shadow` built from the root manifest and the config), and the bypass
check sees only the unshadowed ones. `vendored` (the member gate) is
derived from the same list instead of re-reading every `[patch]` table.

One classifier: the bypass check runs each declaration through `route`
with the copy's honored `[patch]` tables as the pins, as hosted pins do,
passing the lock's other versions of the crate. A path declaration (the
user's own fork) is still left alone.

`patched_by` now covers a `registry-index` declaration with a
`[patch."<index URL>"]` table for the same index (a `registry+` prefix,
host case and a trailing `/` ignored; `sparse+` is another source), and
treats a `registry-index` naming the crates.io git index as crates.io.
The sparse crates.io index stays a separate source. Git URLs compare as
cargo's `CanonicalUrl` spells them (one trailing `/` and a `.git` suffix
dropped, a GitHub URL lowercased whole, any host lowercased).

The bypass detail ends with the remedy for its evidence: a declaration
(re-running `vendor` does not help), a crates.io lock entry beside a
`[patch.crates-io]` copy (stale: re-lock), or another lock entry
(`cargo tree -i` names its declarer). The CLI note no longer claims
re-vendoring never helps.

Diagnostics: a Socket pin to another patch that the lock does not
reflect (its requirement excludes every locked version) used to vanish
once any Socket entry of the crate was locked; it is now diagnosed
`patched_ref_unattributable`. A well-formed `socket-patch-<uuid>`
registry that pins nothing (beside `path` / `git` / `registry-index`, or
on a `[workspace.dependencies]` entry no manifest inherits) is diagnosed
`patched_ref_invalid`. The committed `workspace-member` redirect golden
gains that diagnostic: its `expected/` tree omits the unchanged
inheriting member, so the pinned template reads as uninherited there.

`Decl::describe` and `same_version_foreign_entry` are shared by the
hosted and vendored checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant