Skip to content

Vendored pnpm: unwinding a vendored package whose dependency is also vendored clobbers the child's lock wiring — remove <parent> breaks frozen installs, the hosted takeover silently unpatches the child, and rollback fails forever #830

Description

[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).

Summary

When two vendored packages are a parent and its dependency (here debug@4.3.4 → ms@2.1.2, both patched), vendoring the parent records its whole pre-vendor snapshots: block as the revert original. Vendoring the child afterwards rewrites the ref inside the parent's rekeyed snapshot (debug@file:… → ms: file:.socket/vendor/…) and records that ref against the parent's file: key.

Reverting the parent splices its recorded pre-vendor block back over the live one (revert_block, crates/socket-patch-core/src/vendor/pnpm_lock.rs:3167). The child's live ms: file:… ref is discarded and replaced with ms: 2.1.2. After that:

  1. remove pkg:npm/debug@4.3.4 exits 0 with a broken lock. ms stays vendored (ms@file:… packages/snapshots entries plus the override), but debug@4.3.4's snapshot now points at ms: 2.1.2, which has no entry. The next pnpm install --frozen-lockfile fails with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY Broken lockfile: no entry for 'ms@2.1.2' in pnpm-lock.yaml on every pnpm major from 7 to 12.
  2. The vendored → hosted takeover (scan --mode hosted) silently unpatches the child. debug gets a hosted pin. ms is skipped with vendored_revert_failed, yet its vendored wiring has already been reverted to the registry entry. Status is success, exit 0, and a fresh frozen install gets unpatched ms.
  3. rollback fails forever (exit 1). The child's revert sees snapshot ref \debug@file:…|ms` no longer exists (revert_snapshot_ref, pnpm_lock.rs:3255), counts it as drift, and keeps the msledger entry and artifact. The lock is already byte-exact, so there's nothing left to undo. Every laterrollbackrepeatsKept vendored state for pkg:npm/ms@2.1.2: lockfile wiring drifted(exit 1).vendor --revertleaves the same residue but exits 0.listkeeps listingms`.

The trigger is ordering. Vendor and revert both process entries in the same (purl-sorted) order, so it fires whenever the parent sorts before its patched dependency (debug < ms, JSONStream < jsonparse, express < qs, …). When the child sorts first (e.g. to-regex-range → is-number, which earlier runs tested), the child's ref is restored before the parent's block is replaced, and everything is clean. Removing only the child (remove pkg:npm/ms@2.1.2) also works.

Impact

  • remove <parent> reports success and leaves a lock that pnpm refuses on every frozen install (CI is red until someone hand-edits pnpm-lock.yaml or re-vendors).
  • The hosted takeover reports success while a package that was patched under vendored mode now installs the vulnerable registry release. VEX does stay honest here: it omits ms with vendor_unwired.
  • rollback can never finish, and the .socket/vendor/npm/<uuid>/ artifact plus the state.json entry are stranded. No documented remedy applies: the advice to "undo the drift" has nothing to undo.
  • debug → ms is a common real-world pair, and both have published advisories.

Repro (main 045d7ec, Linux, Node 22, pnpm 12.8.1)

mkdir proj && cd proj
echo '{"name":"proj","version":"1.0.0","dependencies":{"debug":"4.3.4"}}' > package.json
pnpm install
cp pnpm-lock.yaml /tmp/lock.orig
# patch API serving patches for pkg:npm/debug@4.3.4 and pkg:npm/ms@2.1.2 (local mock)
socket-patch scan --mode vendored --yes          # vendors debug, then ms
socket-patch remove pkg:npm/debug@4.3.4 --yes    # exit 0, "Reverted vendoring for pkg:npm/debug@4.3.4"
rm -rf node_modules && pnpm install --frozen-lockfile --store-dir "$(mktemp -d)"
# ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY  Broken lockfile: no entry for 'ms@2.1.2' in pnpm-lock.yaml

The lock after remove (diff against the pre-vendor lock). The debug@4.3.4 snapshot is back to ms: 2.1.2, but ms is still keyed ms@file:…:

+overrides:
+  ms@2.1.2: file:.socket/vendor/npm/6666…/ms-2.1.2.tgz
-  ms@2.1.2:
-    resolution: {integrity: sha512-sGkPx+…}
+  ms@file:.socket/vendor/npm/6666…/ms-2.1.2.tgz:
+    resolution: {integrity: sha512-tTm4…, tarball: file:.socket/vendor/npm/6666…/ms-2.1.2.tgz}
+    version: 2.1.2
-  ms@2.1.2: {}
+  ms@file:.socket/vendor/npm/6666…/ms-2.1.2.tgz: {}

Takeover variant (fresh vendored project as above):

socket-patch scan --mode hosted --yes --json
# status: success, exit 0, redirected: 1
# redirect.skipped: [{purl: pkg:npm/ms@2.1.2, reason: vendored_revert_failed}]
# warnings: redirect_takeover_reverted_vendored, redirect_vendored_revert_failed
rm -rf node_modules && pnpm install --frozen-lockfile --store-dir "$(mktemp -d)"   # exit 0
head -1 node_modules/.pnpm/ms@2.1.2/node_modules/ms/index.js                       # upstream bytes, no patch

Rollback variant (fresh vendored project as above):

socket-patch rollback --yes   # exit 1
# Warning: snapshot ref `debug@file:.socket/vendor/npm/5555…/debug-4.3.4.tgz|ms` no longer exists; nothing to restore
# Error: Kept vendored state for pkg:npm/ms@2.1.2: lockfile wiring drifted; vendored state left untouched
socket-patch rollback --yes   # exit 1 again, same message (pnpm-lock.yaml is already byte-identical to the original)

Expected vs actual

  • CLI_CONTRACT.md (takeover): "A takeover revert that leaves vendored wiring in place is refused with redirect_vendored_revert_failed … the package stays vendored and skipped". It also says a taken-over package that "now installs the unpatched registry release … is reported as redirect_takeover_unpatched with status: "partial_failure" and exit 1, never as success." Actual: ms's wiring is gone (it isn't vendored any more), it isn't pinned, and the run is success / exit 0.
  • remove / vendor --revert should restore the recorded fragments for the target only, and leave the other vendored entries' live wiring consistent. Actual: the parent's revert overwrites the child's ref and leaves a lock pnpm rejects.
  • rollback should converge to the pre-vendor state and exit 0 once the lock is byte-exact. Actual: it's stuck at exit 1 with an orphaned artifact and ledger entry.

OS × version

All Linux (sandbox), main 045d7ec, each cell reproduced at least twice. The JSONStream → jsonparse pair reproduced the rollback cell on 9.15.9 and 12.8.1 too.

pnpm (lock) remove <parent> → frozen install vendored → hosted takeover rollback vendor --revert remove <child> (control)
7.33.7 (5.4) fails success, ms unpatched exit 1 forever, lock byte-exact — —
8.15.9 (6.0) fails success, ms unpatched exit 1 forever, lock byte-exact — —
9.15.9 (9.0) fails MISSING_DEPENDENCY success, ms unpatched exit 1 forever, lock byte-exact exit 0, residue pass
10.34.5 fails success, ms unpatched exit 1 forever, lock byte-exact — —
11.28.3 fails success, ms unpatched exit 1 forever, lock byte-exact — —
12.8.1 fails MISSING_DEPENDENCY success, ms unpatched exit 1 forever, lock byte-exact exit 0, residue pass
macOS / Windows untested (the lock surgery is OS-independent)

First bad release

Not a regression. Release 4.0.0 (@socketsecurity/socket-patch@4.0.0, scan --mode vendored --vendor-source build, then remove pkg:npm/debug@4.3.4) leaves the same broken lock, with ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY on 9.15.9 and 12.8.1.

Suspect code

  • crates/socket-patch-core/src/vendor/pnpm_lock.rs:3153-3167 (revert_block): the key_is_ours branch accepts a live block that differs from what this entry wrote, because a later vendor rewrote a ref inside it, and splices the full pre-vendor original over it. The child's live ref is lost. Either restore the block while keeping refs that point into another live vendored entry's dir, or revert dependents before the packages they depend on (reverse vendoring order).
  • crates/socket-patch-core/src/vendor/pnpm_lock.rs:3203-3256 (revert_snapshot_ref): the child's ref record is keyed by the parent's file: snapshot key. Once the parent is un-vendored, the record can never match again, so the entry is kept as drift forever, even when the ref under the restored key already equals the recorded original.
  • The takeover path then counts that drift as vendored_revert_failed, but the other wiring records were already reverted, so the package is neither vendored nor pinned.

No probe runs: macOS and Windows probe branches are still on hold (see ledger #303).

No activity

Activity on this issue will appear here.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions