[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.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.
- 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.
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).
[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-vendorsnapshots: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'sfile: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 livems: file:…ref is discarded and replaced withms: 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.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).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)The lock after
remove(diff against the pre-vendor lock). Thedebug@4.3.4snapshot is back toms: 2.1.2, butmsis still keyedms@file:…:Takeover variant (fresh vendored project as above):
Rollback variant (fresh vendored project as above):
Expected vs actual
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. TheJSONStream→jsonparsepair reproduced the rollback cell on 9.15.9 and 12.8.1 too.remove <parent>→ frozen installrollbackvendor --revertremove <child>(control)msunpatchedmsunpatchedMISSING_DEPENDENCYmsunpatchedmsunpatchedmsunpatchedMISSING_DEPENDENCYmsunpatchedFirst bad release
Not a regression. Release 4.0.0 (
@socketsecurity/socket-patch@4.0.0,scan --mode vendored --vendor-source build, thenremove pkg:npm/debug@4.3.4) leaves the same broken lock, withERR_PNPM_LOCKFILE_MISSING_DEPENDENCYon 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.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).