Skip to content

Agent-mode vex still attests not_affected when a Deno .deno/<name>@<ver>_1 copy is unpatched: the #517 every-copy check never sees store peer-variant copies #603

Description

[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.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions