Skip to content

Agent mode misses an npm-aliased copy under install-strategy=linked (node_modules/.store/lp@…), so apply exits 0 with it unpatched and VEX attests not_affected #852

Description

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

Summary

#738 (fixing #356) taught the agent-mode resolver to treat a real dir whose own package.json names the patched name@version as a copy of it, so lp@npm:left-pad@1.3.0 is now patched in a hoisted tree. Under npm's install-strategy=linked, though, npm 9–11 put the alias in its own store entry named after the alias: node_modules/.store/lp@1.3.0-<hash>/node_modules/lp. The top-level node_modules/lp is a symlink to it. Agent mode never reaches that copy:

  • Alias plus a plain copy ("left-pad": "1.3.0" and "lp": "npm:left-pad@1.3.0"): apply patches only the left-pad@1.3.0-<hash> store entry and exits 0. vex then writes not_affected for pkg:npm/left-pad@1.3.0, while require('lp') loads the unpatched file.
  • Alias only: apply exits 0 with 0 of 1 targeted patch applied … 1 not found on disk and the note "targets a package not installed on this host (resolved by the project lockfile; skipped)". The package is installed. vex refuses with package_not_found, which fails safe, but the diagnostic is wrong.

Impact

A false not_affected VEX statement for code the app actually loads, with a success exit and no warning (the mixed case). This is the #356 symptom that #738 fixed for hoisted trees, still present for linked trees on npm 9.4–11.

Repro (Linux, npm 10.9.4 / Node 22, main 4646693; no network beyond the registry)

mkdir p && cd p
echo '{"name":"p","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","lp":"npm:left-pad@1.3.0"}}' > package.json
echo 'install-strategy=linked' > .npmrc
npm install --no-audit --no-fund
readlink -f node_modules/left-pad node_modules/lp
#   …/node_modules/.store/left-pad@1.3.0-iv4j8hdajpqgDc7lVr5hdA/node_modules/left-pad
#   …/node_modules/.store/lp@1.3.0-NCKE2NXgCY5tgWWRE6qdYA/node_modules/lp
# hand-stage a patch for pkg:npm/left-pad@1.3.0 (marker prepended to index.js)
python3 - <<'PY'
import hashlib, json, os
def h(b): return hashlib.sha256(b"blob %d\0" % len(b) + b).hexdigest()
o = open("node_modules/left-pad/index.js","rb").read(); a = b"/* SOCKET-PATCHED */\n" + o
os.makedirs(".socket/blobs", exist_ok=True)
for b in (o, a): open(".socket/blobs/" + h(b), "wb").write(b)
json.dump({"patches": {"pkg:npm/left-pad@1.3.0": {"uuid": "1a2b3c4d-5e6f-4a1b-8c2d-0123456789ab",
  "exportedAt": "2026-01-01T00:00:00Z", "files": {"package/index.js": {"beforeHash": h(o), "afterHash": h(a)}},
  "vulnerabilities": {"GHSA-aaaa-bbbb-cccc": {"cves": ["CVE-2024-99999"], "summary": "s", "severity": "high", "description": "d"}},
  "description": "d", "license": "MIT", "tier": "free"}}}, open(".socket/manifest.json", "w"))
PY
socket-patch apply --offline          # exit 0: "1 of 1 targeted patch applied"
socket-patch vex --offline -O v.json  # exit 0: "status": "not_affected"
node -e "for (const m of ['left-pad','lp']) console.log(m, require('fs').readFileSync(require.resolve(m),'utf8').slice(0,20))"
#   left-pad /* SOCKET-PATCHED */
#   lp /* This program is f        <- unpatched

Drop "left-pad": "1.3.0" from package.json for the alias-only case: apply reports 1 not found on disk (exit 0) and vex exits 1 with package_not_found.

Expected vs actual

Matrix (Linux; each cell run at least twice, apply run twice per cell)

npm Node strategy alias + plain alias only
9.9.4 22 linked fail (lp unpatched, VEX not_affected) not run
10.9.4 22 linked fail fail (not found on disk, exit 0; VEX refuses)
11.6.2 22 linked fail fail
12.2.0 24 linked pass (npm 12 dedupes the alias into the left-pad@1.3.0-<hash> entry) pass (store entry named left-pad@…)
10.9.4 22 hoisted pass pass (#738)

macOS and Windows weren't probed (probe branches are paused). The resolver logic is OS-independent.

First bad version

Not a regression. Alias installs were never patched before #738 (#356), and #738 covered the hoisted layout only.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:2893 and :2899: the store-variant scan drops any entry whose advertised name (lp, taken from the .store dir name) differs from the package.json name, then probes entry_nm.join(&full_name) (…/node_modules/left-pad). An aliased entry holds …/node_modules/lp instead.
  • The Fix agent mode skipping npm-aliased copies (#356) #738 alias pass treats only real dirs as copies ("Links still never count"), so the top-level node_modules/lp symlink into .store isn't followed either.
  • list_npm_store_entries_sync (npm_crawler.rs:2491) derives the entry's name from the dir name. For npm's linked store, the entry's own package.json is the authority.

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

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions