[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
pnpm 11+ isolates global installs. Each pnpm add -g … command gets its own install directory, $PNPM_HOME/global/v11/<hash>/, with its own node_modules and, on pnpm 12 (or pnpm 11 with enableGlobalVirtualStore: false), its own node_modules/.pnpm virtual store. pnpm root -g returns the parent global/v11, and socket-patch uses that one directory as the global root.
When two global install groups both contain [email protected] (a direct pnpm add -g [email protected], plus a global tool that depends on left-pad), get -g / apply -g / scan -g --mode agent patch only one of the two physical copies. The tool keeps loading the unpatched copy. The run exits 0 with status: success, applied: 1, a re-run of apply -g says already_patched, and vex -g attests not_affected.
Which copy is missed depends on directory-listing order. When the group holding the direct left-pad link is listed first, the other group's .pnpm/[email protected] store entry is never probed. On ext4 that's about half of fresh setups; on tmpfs it's deterministic (see the repro).
Impact
A globally installed tool (any CLI installed with pnpm add -g) keeps running a vulnerable dependency while socket-patch reports it patched and emits a VEX statement saying the vulnerability isn't exploitable. Nothing in the JSON or on stderr shows that a copy was skipped.
Repro (Linux, pnpm 12.8.2, Node 22)
Uses /dev/shm so the listing order is deterministic. A local mock of the patch API serves a patch for pkg:npm/[email protected] that prepends /*SOCKET_PATCHED*/ to index.js (batch / by-package / view routes with inline blob content, as in crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs).
# a global "tool" that depends on left-pad 1.3.0
mkdir wrap && cd wrap
echo '{"name":"lp-wrapper","version":"1.0.0","bin":{"lpw":"cli.js"},"dependencies":{"left-pad":"1.3.0"}}' > package.json
printf '#!/usr/bin/env node\nconsole.log(require("fs").readFileSync(require.resolve("left-pad"),"utf8").slice(0,18))\n' > cli.js
npm pack && cd ..
base=/dev/shm/g; mkdir -p $base/home/bin
export PNPM_HOME=$base/home HOME=$base PATH=$base/home/bin:$PATH
pnpm add -g ./wrap/lp-wrapper-1.0.0.tgz # group 1: left-pad is transitive (only in its .pnpm)
pnpm add -g [email protected] # group 2: left-pad is a direct global package
pnpm root -g # $base/home/global/v11
find $base/home -path '*left-pad/index.js' # two copies, one per global/v11/<hash>/node_modules/.pnpm
mkdir w && cd w
socket-patch get pkg:npm/[email protected] -g --yes --json --api-url $MOCK --org test-org --api-token fake
# exit 0, "status": "success", "applied": 1
lpw # "/* This program is" <- the tool still loads the original bytes
socket-patch apply -g --json --offline # success, left-pad: skipped / already_patched
socket-patch vex -g --offline --product pkg:npm/[email protected] --output v.json
# exit 0, one statement: not_affected, subcomponent pkg:npm/[email protected]
If you install the two groups in the opposite order, both copies are patched.
Expected vs actual
- Expected: agent mode patches every physical copy of a
name@version. find_by_purls documents this ("Returns every physical copy … patching only one leaves a live, vulnerable copy while reporting success (a silent partial)"), as do docs/ecosystems.md (npm row: "any install layout … every store copy") and the agent-mode notes ("apply and rollback patch every store copy of a name@version"). VEX should not attest a patch that a loaded copy doesn't carry.
- Actual: one copy is patched, the other stays original, and every command reports success.
OS × version (Linux, Node 22, main 2463257)
| pnpm |
global layout |
copies of [email protected] |
result |
| 10.34.5 |
single global/5, shared .pnpm |
1 |
pass |
| 11.0.0 / 11.28.3 (default) |
per-install dirs, global virtual store (store/v11/links) |
1 shared copy |
pass here (the shared-store write is #361's problem) |
11.28.3, enableGlobalVirtualStore: false |
per-install .pnpm |
2 |
fail (2 of 3 ext4 runs) |
| 12.4.2 |
per-install .pnpm |
2 |
fail |
| 12.8.1 |
per-install .pnpm |
2 |
fail (1 of 3 ext4 runs; order-dependent) |
| 12.8.2 |
per-install .pnpm |
2 |
fail: tmpfs 4/4 in the order above; on a fixed ext4 layout, 3/3 re-runs fail |
| 12.8.2, groups installed in the opposite order |
per-install .pnpm |
2 |
pass |
macOS and Windows weren't probed (no probe branch this run; see the ledger). The walk order there is filesystem-dependent too, so the miss should be possible there as well.
First bad commit
This is a regression. On the same ext4 layout, release 4.0.0 patched both copies in 3/3 clean runs, and main missed one in 3/3. Bisected on the deterministic tmpfs layout:
Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:922: in resolve_pending_targets, unmatched_names is computed walk-wide. Once any importer tree has matched left-pad (group 2's direct link), every later pnpm store's entries for that name are filtered out, including a different install's own .pnpm that holds a separate physical copy.
npm_crawler.rs:972: since b32711f, inside a store entry only a real directory matches (is_real_package_dir_sync). Before that, group 1's .pnpm/lp-wrapper…/node_modules/left-pad link probably matched, which is how 4.0.0 reached the second copy.
find_store_peer_variant_copies (npm_crawler.rs:1913) only fans out within the found copy's own virtual store, so it can't recover the other install's copy.
get_global_node_modules_paths (npm_crawler.rs:1135, :1148) passes pnpm 11+'s global/v11 as one root. Treating each global/v11/<hash>/node_modules as its own root (as for separate projects) would also avoid the cross-install filter. The analogous project-mode layout (a sharedWorkspaceLockfile: false workspace, where each package has its own .pnpm) patches both copies correctly on pnpm 10.34.5 and 12.8.2, because those are separate roots.
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
pnpm 11+ isolates global installs. Each
pnpm add -g …command gets its own install directory,$PNPM_HOME/global/v11/<hash>/, with its ownnode_modulesand, on pnpm 12 (or pnpm 11 withenableGlobalVirtualStore: false), its ownnode_modules/.pnpmvirtual store.pnpm root -greturns the parentglobal/v11, and socket-patch uses that one directory as the global root.When two global install groups both contain
[email protected](a directpnpm add -g [email protected], plus a global tool that depends on left-pad),get -g/apply -g/scan -g --mode agentpatch only one of the two physical copies. The tool keeps loading the unpatched copy. The run exits 0 withstatus: success, applied: 1, a re-run ofapply -gsaysalready_patched, andvex -gattestsnot_affected.Which copy is missed depends on directory-listing order. When the group holding the direct
left-padlink is listed first, the other group's.pnpm/[email protected]store entry is never probed. On ext4 that's about half of fresh setups; on tmpfs it's deterministic (see the repro).Impact
A globally installed tool (any CLI installed with
pnpm add -g) keeps running a vulnerable dependency while socket-patch reports it patched and emits a VEX statement saying the vulnerability isn't exploitable. Nothing in the JSON or on stderr shows that a copy was skipped.Repro (Linux, pnpm 12.8.2, Node 22)
Uses
/dev/shmso the listing order is deterministic. A local mock of the patch API serves a patch forpkg:npm/[email protected]that prepends/*SOCKET_PATCHED*/toindex.js(batch / by-package / view routes with inline blob content, as incrates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs).If you install the two groups in the opposite order, both copies are patched.
Expected vs actual
name@version.find_by_purlsdocuments this ("Returns every physical copy … patching only one leaves a live, vulnerable copy while reporting success (a silent partial)"), as do docs/ecosystems.md (npm row: "any install layout … every store copy") and the agent-mode notes ("applyandrollbackpatch every store copy of aname@version"). VEX should not attest a patch that a loaded copy doesn't carry.OS × version (Linux, Node 22, main
2463257)global/5, shared.pnpmstore/v11/links)enableGlobalVirtualStore: false.pnpm.pnpm.pnpm.pnpm.pnpmmacOS and Windows weren't probed (no probe branch this run; see the ledger). The walk order there is filesystem-dependent too, so the miss should be possible there as well.
First bad commit
This is a regression. On the same ext4 layout, release 4.0.0 patched both copies in 3/3 clean runs, and main missed one in 3/3. Bisected on the deterministic tmpfs layout:
0b4e645(b32711f^): passb32711f"Support vlt in hosted, vendored and agent modes (Support vlt in hosted, vendored and agent modes #269)": first bad (reproduced twice)f6b7fb9,2463257(main): fail. Draft PR Fix npm crawler missing relocated dependency stores (#359, #362) #365's head80f4a71also fails.Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:922: inresolve_pending_targets,unmatched_namesis computed walk-wide. Once any importer tree has matchedleft-pad(group 2's direct link), every later pnpm store's entries for that name are filtered out, including a different install's own.pnpmthat holds a separate physical copy.npm_crawler.rs:972: since b32711f, inside a store entry only a real directory matches (is_real_package_dir_sync). Before that, group 1's.pnpm/lp-wrapper…/node_modules/left-padlink probably matched, which is how 4.0.0 reached the second copy.find_store_peer_variant_copies(npm_crawler.rs:1913) only fans out within the found copy's own virtual store, so it can't recover the other install's copy.get_global_node_modules_paths(npm_crawler.rs:1135,:1148) passes pnpm 11+'sglobal/v11as one root. Treating eachglobal/v11/<hash>/node_modulesas its own root (as for separate projects) would also avoid the cross-install filter. The analogous project-mode layout (asharedWorkspaceLockfile: falseworkspace, where each package has its own.pnpm) patches both copies correctly on pnpm 10.34.5 and 12.8.2, because those are separate roots.