[agent] Found by the scheduled Deno bug-hunt routine (ledger #308).
Summary
#517 (closing #516) made agent-mode vex hash every installed copy of a PURL before attesting it. It gets those copies from find_manifest_package_copies_reusing → NpmCrawler::find_by_purls, whose doc comment says it deliberately does not list a store peer-variant copy when an importer-tree copy was already found. apply reaches those copies through a separate fan-out, find_store_peer_variant_copies, and since #496 that includes Deno's copy-index folders (node_modules/.deno/<name>@<ver>_N). So apply patches the _1 copy, but vex never checks it.
If the _1 copy goes back to the vulnerable bytes (a later deno install re-linking it from DENO_DIR, a manual restore, or a partial rollback), vex still emits not_affected with exit 0. The dependent that resolves to _1 (here schema-utils) loads the unpatched code at runtime.
I reported this on #516 and #517 before the merge (#516 (comment) and the follow-up comment). #516 is now closed, and this case still reproduces on main.
Impact
The VEX document claims not_affected (inline_mitigations_already_exist) while a vulnerable copy of the package is installed and loaded. apply itself is correct. Only the attestation is wrong.
Repro (Linux, real Deno, main b1f9818)
export DENO_DIR=$PWD/.dd
echo '{ "nodeModulesDir": "auto" }' > deno.json
echo '{ "name":"r","version":"1.0.0","dependencies": { "ajv": "6.12.0", "ajv-keywords": "3.5.2", "schema-utils": "2.7.1" } }' > package.json
echo 'import "schema-utils"; console.log(JSON.stringify(globalThis.__SP||[]));' > su.js
deno install
# node_modules/.deno/ajv-keywords@3.5.2 (peer ajv 6.12.0, linked from the root)
# node_modules/.deno/ajv-keywords@3.5.2_1 (peer ajv 6.15.0, linked from schema-utils@2.7.1)
P1=node_modules/.deno/ajv-keywords@3.5.2_1/node_modules/ajv-keywords
cp $P1/index.js pristine.js
# Stage .socket/manifest.json + .socket/blobs offline: one patch for pkg:npm/ajv-keywords@3.5.2
# changing package/index.js (after = before + `(globalThis.__SP||=[]).push(purl)`), one CVE.
socket-patch apply --offline --json # success, applied 1; both .deno copies patched
cp pristine.js $P1/index.js # only the _1 copy goes back to the vulnerable bytes
deno run -A su.js # [] -> schema-utils loads the UNPATCHED _1 copy
socket-patch vex --offline --product pkg:npm/r@1.0.0 --json -O v.json; echo $?
# 0, status success, statements: [not_affected]
Control, in the same project: revert only the root-linked .deno/ajv-keywords@3.5.2 copy instead, and vex drops the statement (partialFailure). So the verdict depends on which copy the crawler happens to return, which is exactly the crawl-order dependence #516 was meant to remove.
Expected vs actual
- Expected:
crates/socket-patch-cli/CLI_CONTRACT.md:390 says that for an agent record, "Every installed copy the crawler finds for the purl … must hash to the patched bytes, as apply patches every copy. One unpatched copy omits the purl." The _1 copy is one apply found and patched, so it should count.
- Actual:
vex checks only the importer-tree copy, emits not_affected and exits 0.
Matrix
| OS |
Deno |
Layout |
Result on main b1f9818 |
| Linux |
2.9.7 |
isolated, nodeModulesDir: auto, .deno/ajv-keywords@3.5.2_1 |
reproduces (2/2, clean projects) |
| Linux |
2.2.15 |
same |
reproduces (1/1) |
| Linux |
1.46.3 |
nodeModulesDir: true |
not applicable: Deno 1.x resolves the graph to a single ajv-keywords@3.5.2, with no _1 |
| Linux |
2.9.7 |
nodeModulesLinker: hoisted |
not applicable: no .deno store copies |
| macOS / Windows |
— |
— |
not probed (the logic isn't OS-specific) |
First bad version
This isn't a regression. Before #496, apply didn't patch the _1 copy at all and vex still attested. #517 fixed nested npm duplicates but not store peer-variant copies.
Suspect code
crates/socket-patch-cli/src/commands/vex.rs:556: find_manifest_package_copies_reusing(...) is the only copy source for agent records.
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1165-1170: find_by_purls doc. Peer-variant copies are "deliberately NOT enumerated" and left to the apply engine's find_store_peer_variant_copies (npm_crawler.rs:2537).
- The hosted path already fans out:
crates/socket-patch-cli/src/commands/vex_consumed.rs:330 calls find_store_peer_variant_copies. The agent-record path doesn't.
By the same reasoning, pnpm (peer) variants, vlt and bun isolated-store variants probably hit this too. I only tested Deno; this issue covers the Deno _N trigger.
[agent] Found by the scheduled Deno bug-hunt routine (ledger #308).
Summary
#517 (closing #516) made agent-mode
vexhash every installed copy of a PURL before attesting it. It gets those copies fromfind_manifest_package_copies_reusing→NpmCrawler::find_by_purls, whose doc comment says it deliberately does not list a store peer-variant copy when an importer-tree copy was already found.applyreaches those copies through a separate fan-out,find_store_peer_variant_copies, and since #496 that includes Deno's copy-index folders (node_modules/.deno/<name>@<ver>_N). Soapplypatches the_1copy, butvexnever checks it.If the
_1copy goes back to the vulnerable bytes (a laterdeno installre-linking it fromDENO_DIR, a manual restore, or a partial rollback),vexstill emitsnot_affectedwith exit 0. The dependent that resolves to_1(hereschema-utils) loads the unpatched code at runtime.I reported this on #516 and #517 before the merge (#516 (comment) and the follow-up comment). #516 is now closed, and this case still reproduces on main.
Impact
The VEX document claims
not_affected(inline_mitigations_already_exist) while a vulnerable copy of the package is installed and loaded.applyitself is correct. Only the attestation is wrong.Repro (Linux, real Deno, main
b1f9818)Control, in the same project: revert only the root-linked
.deno/ajv-keywords@3.5.2copy instead, andvexdrops the statement (partialFailure). So the verdict depends on which copy the crawler happens to return, which is exactly the crawl-order dependence #516 was meant to remove.Expected vs actual
crates/socket-patch-cli/CLI_CONTRACT.md:390says that for an agent record, "Every installed copy the crawler finds for the purl … must hash to the patched bytes, asapplypatches every copy. One unpatched copy omits the purl." The_1copy is oneapplyfound and patched, so it should count.vexchecks only the importer-tree copy, emitsnot_affectedand exits 0.Matrix
b1f9818nodeModulesDir: auto,.deno/ajv-keywords@3.5.2_1nodeModulesDir: trueajv-keywords@3.5.2, with no_1nodeModulesLinker: hoisted.denostore copiesFirst bad version
This isn't a regression. Before #496,
applydidn't patch the_1copy at all andvexstill attested. #517 fixed nested npm duplicates but not store peer-variant copies.Suspect code
crates/socket-patch-cli/src/commands/vex.rs:556:find_manifest_package_copies_reusing(...)is the only copy source for agent records.crates/socket-patch-core/src/crawlers/npm_crawler.rs:1165-1170:find_by_purlsdoc. Peer-variant copies are "deliberately NOT enumerated" and left to the apply engine'sfind_store_peer_variant_copies(npm_crawler.rs:2537).crates/socket-patch-cli/src/commands/vex_consumed.rs:330callsfind_store_peer_variant_copies. The agent-record path doesn't.By the same reasoning, pnpm
(peer)variants, vlt and bun isolated-store variants probably hit this too. I only tested Deno; this issue covers the Deno_Ntrigger.