Repository navigation
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:
remove pkg:npm/debug@4.3.4exits 0 with a broken lock.msstays vendored (ms@file:…packages/snapshots entries plus the override), butdebug@4.3.4's snapshot now points atms: 2.1.2, which has no entry. The nextpnpm install --frozen-lockfilefails withERR_PNPM_LOCKFILE_MISSING_DEPENDENCY Broken lockfile: no entry for 'ms@2.1.2' in pnpm-lock.yamlon every pnpm major from 7 to 12.- The vendored → hosted takeover (
scan --mode hosted) silently unpatches the child.debuggets a hosted pin.msis skipped withvendored_revert_failed, yet its vendored wiring has already been reverted to the registry entry. Status issuccess, exit 0, and a fresh frozen install gets unpatchedms. rollbackfails forever (exit 1). The child's revert seessnapshot ref \debug@file:…|ms` no longer exists(revert_snapshot_ref,pnpm_lock.rs:3255), counts it as drift, and keeps themsledger 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-editspnpm-lock.yamlor re-vendors).- The hosted takeover reports
successwhile a package that was patched under vendored mode now installs the vulnerable registry release. VEX does stay honest here: it omitsmswithvendor_unwired. rollbackcan never finish, and the.socket/vendor/npm/<uuid>/artifact plus thestate.jsonentry are stranded. No documented remedy applies: the advice to "undo the drift" has nothing to undo.debug→msis 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.yamlThe 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 patchRollback 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 asredirect_takeover_unpatchedwithstatus: "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 issuccess/ exit 0. remove/vendor --revertshould 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.rollbackshould 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): thekey_is_oursbranch 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'sfile: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).
Activity
pnpm: run 20 ledger (#830)
mikolalysenko commentedon Oct 5, 2026
[agent] Triaged as priority:p1 (pnpm). Not a duplicate. #636 is a different multi-package residue in pnpm reverts (the empty overrides and the scaffold). It doesn't share this cause: here
revert_blocksplices a parent's pre-vendor snapshot over a child's live ref.Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Claiming this issue as part of the pnpm open-issue sweep in draft PR #1007. Branch: agent/fix-pnpm-open-issues. Claim-ID: 2026-10-07T12:44:29Z-pnpm07
Fix unwinding a vendored pnpm parent clobbering its vendored child's …
mikolalysenko commentedon Oct 7, 2026
[agent] PR #1007 fixes this issue. It has a regression test that fails on
main, CI is fully green (552 checks) and it's ready for review. The issue will close when #1007 merges. The PR description's table gives the root cause, the fix and the test for each issue.