Skip to content

Stop VEX attesting over yarn PnP, pnpm bundled and deno.lock copies - #1033

Merged
Mikola Lysenko (mikolalysenko) merged 19 commits into
mainfrom
arch-fix/vex-false-attest
Oct 8, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 19 commits into
mainfrom
arch-fix/vex-false-attest

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #940. This branch now contains #940's current head (acbac79a), merged in at b83da9c4, because the yarn same-lock rule used here comes from #940's Discovery::unpatched_copy. Until #940 merges, this diff also shows #940's commits. Merge #940 first. This PR's own commits are 176327d1, a0f253a5, 5e5d88d7, 7fb32e8f and, after the merge, 9ac96fc6, 98d9b0b2, e9a33b85.

Problem

Audit workstream vex-false-attest (B05, B04): standalone vex attested not_affected from a lock's hosted pin while the copy the build runs stayed unpatched.

Change

  • PnP (In a yarn classic Plug'n'Play project, vex attests a hosted patch as not_affected while the copy .pnp.js loads is still unpatched #519). crawlers::pkg_managers::YarnPnpLoader (new; reuses detect_npm_pkg_manager) reads .pnp.cjs, .pnp.js, .pnp.loader.mjs and .pnp.data.json. resolves_patch(uuid, urls) checks whether the loader names the patch:
  • Unattested refs (pnpm bundled copies, deno.lock). These two cases now keep the ref and only block the attestation. They are recorded as a Discovery::unwired_copy (UnwiredCopy). Once every extractor has run, the orchestrator turns each one into an Unattested mark on every ref it may sit next to. This is the mechanism Gradle already used.
    • The ref stays a ref, so list, rollback, remove and the ledgers' liveness gates (vendor --check, scan) still see the wiring as live.
    • Only vex omits it, with vex_pnpm_bundled_copy or vex_deno_lock_copy and a remedy specific to each case. Unattested gains an UnattestedKind so the Gradle code (vex_gradle_lock_above_base) is no longer used for every case.
    • pnpm: formats::pnpm::grammar::entry_bundled reads bundledDependencies:: the block list and true that real pnpm 11.27 writes, plus a flow list. Any other value fails closed to "all". A ref whose name a list in the same lock names is marked whatever its version, because the lock doesn't record the bundled copy's version. true marks every ref of that lock. The mark never reaches another lock.
    • Deno: the deno.lock npm section (lockfile v2/v3/v4/v5 layouts, with Deno's _peer suffix stripped) marks a ref of the same name@version in any lock. deno.lock still never yields a ref.
  • yarn berry same-lock rule. Berry plain-registry and custom-registry entries now go through Discovery::unpatched_copy, as yarn classic registry blocks already do (Yarn classic VEX attests not_affected when yarn.lock also has a registry block for the patched name@version (e.g. after yarn add -W <pkg> --exact), though yarn installs only the unpatched registry copy #938). This does drop the ref. That is correct here: re-running scan / vendor rewires every copy, just like the npm / Bun / classic same-lock rule.
  • Docs: CLI_CONTRACT.md gets a new "Unattested references" bullet, the deno row, two new rows in the vex code table, and the yarn same-lock sentence. docs/testing/yarn-berry-compatibility.md gets the PnP contract.

Liveness impact, stated explicitly: an earlier revision of this PR fed the pnpm bundled and Deno evidence into unpatched_copy / resolved_elsewhere. Those drop the ref, so any pnpm lock with one bundledDependencies: true package failed vendor --check for every vendored npm entry in it, and a Deno project failed for every same-version entry. Both failures came with a "re-run socket-patch vendor" or "delete the stale lock" remedy that could never clear them. With this revision, neither case affects vendor --check or scan liveness. Only the new berry same-lock contest affects liveness, and re-running scan / vendor does clear it.

Duplicate copies deleted: none. This is a behavior fix. The pnpm bundled and Deno cases reuse the existing Unattested mechanism (previously Gradle-only), and the matching runs once in the orchestrator (unattest_unwired_copies), not in a per-extractor loop over out.refs. The yarn fix reuses #940's unpatched_copy. One pre-existing duplication remains: npm (push_uncontested) and vlt (contest_bundled_copies) each still emit their own bundled-copy diagnostic instead of using unpatched_copy (see Deferred).

Fixes #519
Refs #406: standalone vex no longer attests. The in-run scan --mode hosted --vex path (assume_applied) and a scan warning about deno.lock are still open.

Review response

Finding Verdict Resolution
major: pnpm bundled kills ledger liveness / vendor --check confirmed Moved to Unattested (9ac96fc6). New test e2e_vex_vendor::pnpm_bundled_copy_blocks_vex_but_not_vendor_check (control, true, list): vendor --check verifies and vex omits with vex_pnpm_bundled_copy, naming the bundling entry. The pnpm lock never records a bundled version, so the "exact name@version" hard-contest path the reviewer offered doesn't apply.
minor: deno.lock contest kills liveness confirmed (in part) Moved to Unattested too. The nodeModulesDir: "manual" skip is not taken: in manual mode deno install still populates node_modules from deno.lock, so the files can't tell which installer ran. Documented as fail-closed for vex only.
minor: PnP substring match with both locators confirmed, and a real gap The berry same-lock rule was missing, so this case attested. Fixed in 98d9b0b2, pinned by yarn::berry_registry_locator_beside_a_hosted_one_contests_it and e2e_vex_lockfile::yarn::pnp_loader_naming_hosted_and_registry_copies_is_not_attested (berry 4 + yarn 1).
minor: per-extractor loop / cross-lock leak / wording confirmed The loop over out.refs is gone (the orchestrator matches). The pnpm mark is scoped to its own lock, with a test that a package-lock.json twin is unaffected. The PR wording is corrected above. The npm/vlt bundled duplication is deferred.
minor: bundledDependencies: true → whole lock accepted as-is It now only blocks attestation, not liveness. Narrowing via snapshots is deferred.
minor: stale #940 base confirmed Merged #940's current head acbac79a (clean).

Testing

Each new regression test fails on the code before its fix and passes now:

Fix Test
#519 e2e_vex_lockfile::yarn::pnp_layout_contract (yarn 1 + berry, fresh vs stale loader); unit crawlers::pkg_managers::tests::yarn_pnp_loader_resolves_patch_only_when_it_names_it
B04 vex::discover::npm::tests::pnpm_bundled_copy_marks_the_ref_unattested, pnpm_bundled_copy_does_not_reach_another_lock; formats::pnpm::grammar::tests::entry_bundled_reads_every_spelling
B04 liveness e2e_vex_vendor::pnpm_bundled_copy_blocks_vex_but_not_vendor_check
#406 vex::discover::deno::tests::deno_lock_marks_an_npm_lock_pin_of_the_same_version_unattested, deno_npm_keys_cover_every_lock_version
berry same-lock vex::discover::yarn::tests::berry_registry_locator_beside_a_hosted_one_contests_it, e2e_vex_lockfile::yarn::pnp_loader_naming_hosted_and_registry_copies_is_not_attested

Commands run (macOS, CARGO_INCREMENTAL=0, through /private/tmp/claude-501/heavy-job.sh, -j4):

  • cargo test -p socket-patch-core --lib -- vex:: crawlers:: formats:: patch::redirect vendor::: 4379 passed. The vex-discover-golden/redirect-npm.json golden was regenerated; the only change is new unpatched_copies rows for 9 berry fixtures (no ref changed).
  • cargo test -p socket-patch-cli --lib --test e2e_vex_lockfile --test e2e_vex_vendor --test e2e_vex --test e2e_vex_redirect --test e2e_embedded_vex --test covgap_commands_vex --test e2e_safety_yarn_pnp --test vex_terminal_output --test e2e_vex_build --test contract_gradle_codes: all passed (867 lib, 324 lockfile, 29 vendor, 19, 31, 15, 12, 36, 12, 1; vex_build cells ignored locally).
  • cargo test -p socket-patch-cli for every suite that touches yarn.lock (in_process_vendor, in_process_redirect, in_process_rollback_hosted, in_process_get_modes, covgap_commands_{vendor,rollback,scan_mod}, e2e_{redirect,vendor}_yarn_berry_build, e2e_yarn4_workspaces_build, e2e_yarn_legacy_cachekey_refusal_build, hosted_memory_{parity,engine}, global_scope_project_state, cli_parse_list, e2e_{hosted,vendored}_production, mode_migration_npm): all passed except mode_migration_npm::berry_vendored_then_hosted_takeover_leaves_pure_hosted. That test fails identically with this PR's yarn change reverted (a hosted resolutions entry is left in package.json with the local real yarn), so it is not caused by this PR; CI will show whether it is local-only.
  • cargo clippy -p socket-patch-core -p socket-patch-cli --tests -- -D warnings: no findings in any touched file. The local run fails only on lints already in files this PR doesn't touch (python_crawler.rs unused unix_default on macOS, jvm_jar.rs, maven_repo.rs, nuget_feed.rs, test prebuilt_common, …).
  • rustfmt was run only on the touched files.

Deferred

🤖 Generated with Claude Code


Note

Medium Risk
Changes VEX attestation and discovery semantics for npm-family locks (security-relevant), but the behavior is intentionally conservative—fewer false positives, more omitted patches when evidence is ambiguous.

Overview
Tightens standalone vex so it no longer issues not_affected from lock pins when the build can still run unpatched bytes beside the wiring.

Yarn Plug'n'Play (#519): Adds YarnPnpLoader to read .pnp.cjs / .pnp.js (and related files) and check whether the loader actually resolves through the Socket patch. Lockfile-only attestation for hosted npm purls is allowed only when the loader names the patch; stale loaders after a rewire stay package_not_found.

Unattested wiring (pnpm + Deno): Introduces UnwiredCopy → Unattested with kinds BundledCopy and DenoLock (alongside existing Gradle LockAboveBase). pnpm bundledDependencies and matching deno.lock npm keys block attestation only (vex_pnpm_bundled_copy, vex_deno_lock_copy) while list / rollback / vendor --check / scan still treat the pin as live.

Yarn Berry same-lock contest: Plain registry locators beside a hosted locator of the same name@version now go through unpatched_copy, dropping the contested ref (aligned with npm/Bun) so PnP substring matches cannot attest when both copies exist.

CLI_CONTRACT.md and yarn compatibility docs document the new rules; e2e and discover tests cover each case.

Reviewed by Cursor Bugbot for commit 314038b. Configure here.


Generated by Claude Code

Claude (claude) and others added 11 commits October 6, 2026 13:28
Assisted-by: Claude Code:claude-opus-5-5
A lockfile-only `vex` attested a package as not_affected while the same
lock also installed an unpatched copy of that exact name@version:

- pnpm: a `file:` directory or tarball copy (#935)
- yarn classic: a registry block left beside the Socket block, e.g.
  after `yarn add -W left-pad --exact` (#938)
- yarn berry: a `file:` / url copy locked under another dependency
  name, which the `resolutions` pin never reaches (#939)

The cross-lock contest only weighs OTHER locks, and each extractor
wrote its own same-lock rule (npm and yarn classic git only). Discovery
now has one shared same-lock rule: extractors record an unpatched copy
and every ref of the same name@version in that lock is dropped with a
patched_ref_unattributable diagnostic naming the copy. Yarn classic's
git-copy filter moves onto it. The berry and pnpm extractors read the
copy's real package from its package.json (directory or tarball) or
from the registry tarball url.

Assisted-by: Claude Code:claude-opus-5-5
main has failed socket-patch-core's lib tests since Gradle support
(#646) and the digest helpers (#865) both landed. The guard test
production_digests_go_through_the_helpers flags three files #646 added
that still hash inline: crawlers/gradle_cache.rs, patch/jvm_jar.rs and
patch/sidecars/maven.rs. That breaks test, test-release and coverage on
every open PR.

Each inline sha1/sha256 call now goes through sha1_hex_of or
sha256_hex_of, which compute the same lowercase hex. Behaviour is
unchanged.

Assisted-by: Claude Code:claude-opus-5-5
(cherry picked from commit 659ac2c)
Every non-Socket yarn classic block is now recorded as a possible
unpatched copy, and each record scanned the whole list for a duplicate
first. On a 3000-package lock that made hosted scans and rescans about
17% slower in the scan benchmark. The list is already sorted and
deduplicated once when discovery finishes, so the per-record scan goes.

Assisted-by: Claude Code:claude-opus-5-5
Conflicts:
- vex/discover/mod.rs: kept both main's ContestedRef/`contested` and
  this branch's UnpatchedCopy/`unpatched_copies` (structs, fields,
  finalize); `contest_within_locks` still runs before
  `contest_across_locks`, after main's new sbt extractor.
- vex/discover/testing/golden.rs: import and render both `contested`
  and `unpatched_copies`.
- vex/discover/yarn.rs: kept main's ClassicBlockSource match and
  classic_block_purl; main's #921 `file:` directory copies and the git
  copies now go through the shared `Discovery::unpatched_copy` rule
  instead of main's local post-filter loop (same diagnostic: names the
  lock entry and "file: directory").

Co-Authored-By: Claude <noreply@anthropic.com>
Standalone vex excused a hosted npm purl the crawler could not find
(package_not_found) by attesting it from the lock's integrity pin. Under
yarn Plug'n'Play the crawler cannot look at all: packages are zips the
.pnp loader resolves. A hosted lock pulled over an existing PnP install
(the loader still resolving the registry copy) was attested not_affected
while the running copy stayed unpatched.

The excuse now treats a yarn PnP project like the pnpm out-of-project
store: the lock basis applies only when the loader itself resolves the
package through the patch. yarn berry keeps each locator's reference
verbatim in .pnp.cjs (or .pnp.data.json), so the hosted url's uuid is in
the text after a fresh install (checked against real yarn 4.12.0 and the
real-yarn pnp-linker cell). yarn classic names the cache folder
npm-<name>-<version>-<hash>, whose hash is the resolved url's #sha1
fragment. A stale loader names neither and the purl is omitted.

PnP detection reuses detect_npm_pkg_manager, so the linker-aware
detection in #978 applies here unchanged once it lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
pnpm unpacks a package's bundledDependencies from that package's own
tarball into its store directory and never locks them, so no Socket
wiring reaches that copy. The npm, bun and vlt VEX extractors already
contest a ref beside a bundled copy; the pnpm extractor had no bundled
handling at all, so a lockfile-only vex attested not_affected while the
bundled copy installed unpatched.

formats/pnpm/grammar gains entry_bundled, which reads the
bundledDependencies field in the shapes real pnpm writes (block list,
true; checked with pnpm 11.27) plus a flow list, failing closed on any
other value. The pnpm extractor feeds each bundled name into the shared
same-lock rule (Discovery::unpatched_copy, from #940). The lock does not
record a bundled copy's version, so a ref of the same name is contested
whatever its version (a missed attestation, never a false one), and
bundledDependencies: true contests every ref of that lock.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deno installs a package.json project's npm dependencies from deno.lock
and never reads package-lock.json. The deno extractor was deliberately
empty, so a hosted pin in package-lock.json beside a deno.lock registry
entry of the same version attested not_affected from the lockfile basis
while Deno ran the unpatched copy.

The extractor now reads deno.lock's npm section (top-level npm in
lockfile v4/v5, npm.packages in v2, packages.npm in v3; Deno's _peer
suffix stripped) as resolved_elsewhere evidence, so the existing
cross-lock contest drops the npm-family ref with a diagnostic naming both
files. The read is advisory: deno.lock still never yields a ref, and an
unreadable or unparseable lock contests nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) added the arch-refactor PR opened by the scheduled architecture refactor routine label Oct 7, 2026
The pnpm bundled rule and the deno.lock read fed Discovery::unpatched_copy
and resolved_elsewhere, which drop the ref. Its uuid stays recognized, so
the shared liveness rule called the vendor-ledger entry or redirect
record dead and vendor --check failed with a rewire remedy that could
never clear it (any pnpm lock holding one bundledDependencies: true
package failed every vendored npm entry in it).

Both now record an UnwiredCopy. After every extractor has run, the
orchestrator turns it into an Unattested mark on each ref it may stand
beside, so the ref stays a ref (list, rollback, remove, ledger liveness)
and only vex omits it. Unattested gains an UnattestedKind so vex reports
vex_pnpm_bundled_copy / vex_deno_lock_copy with their own remedy instead
of the Gradle code. The pnpm mark stays inside its own lock and is no
longer cross-lock evidence. record_pnpm_bundled_copies no longer walks
out.refs.

Regression test: a pnpm vendored entry beside a bundledDependencies
package still passes vendor --check, while vex omits it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A berry registry locator (left-pad@npm:1.3.0) beside the hosted
__archiveUrl one of the same name@version was only cross-lock evidence,
so the same lock never contested the hosted ref. yarn installs both, and
vex's PnP loader check matches the patch anywhere in the loader text, so
a loader naming both locators attested the hosted pin. Plain and
custom-registry berry entries now go through Discovery::unpatched_copy,
as yarn classic registry blocks already do (#938).

Adds a core test and a PnP e2e test (berry 4 and yarn 1) where the
loader names both locators: nothing attests. The golden only gains the
new unpatched_copies rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Resolve conflicts in vex/discover: keep main's same-lock contest
(#940) alongside this branch's unwired-copy unattestation, pnpm bundled
copies and berry registry-locator contest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 314038b. Configure here.

@mikolalysenko

Mikola Lysenko (mikolalysenko) commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

[agent] Ready for review at 314038b67efce94ce292ee49e2cc01a31cfdf257.

  • CI: required ci-ok green. 529 success / 7 skipped / 0 failing of 564 check runs. 28 non-required macOS/Windows native / install-proof legs are still queued on the runner backlog.
  • Bugbot reviewed 314038b67e with no unresolved findings; no open review threads.
  • Mergeable, no conflicts.
  • Reviewer focus: the PnP / pnpm-bundled / deno.lock refusals in commands/vex.rs. Stacked base Fix VEX attesting beside an unpatched same-lock copy (#935, #938, #939) #940 is merged (c5be5d1).

Generated by Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) removed the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 8, 2026
…test

# Conflicts:
#	crates/socket-patch-core/src/crawlers/pkg_managers.rs
#	docs/testing/yarn-berry-compatibility.md
@mikolalysenko
Mikola Lysenko (mikolalysenko) added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 80c9438 Oct 8, 2026
558 of 563 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the arch-fix/vex-false-attest branch October 8, 2026 05:05
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve golden.rs Discovery destructure: keep this PR's read/withheld
fields and main's unwired_copies (#1033).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Oct 8, 2026
Resolve conflicts with #1035 (supersede lifecycle), #1038 (shared repo-root
walk), and #1033 (unwired VEX copies):

- #1035 added `vex::discover::same_release` and a second
  `canonical_base_purl` there. `PurlKey::same` already treats composer
  version spellings as one release, so every `same_release` call
  (vex_sources, rollback, discover, and `ledgers::hosted_pins_matching`)
  now uses `PurlKey::same`, and the duplicate helpers are dropped.
- Both sides' new ledgers tests are kept.
- The repo-root lookup in `policy` takes main's `utils::repo_root` version.
  `canonical_pypi_purl` stays deleted (nothing calls it now).
- `vex` re-exports main's `UnattestedKind`. `UnwiredCopy::covers` compares
  purls with `PurlKey::same` rather than raw string equality.

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

Labels

arch-refactor PR opened by the scheduled architecture refactor routine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

In a yarn classic Plug'n'Play project, vex attests a hosted patch as not_affected while the copy .pnp.js loads is still unpatched

3 participants