Skip to content

Vendored npm vex and vendor --check fail after any npm 7–10 npm install on a lockfileVersion 2 lock, because npm drops resolved from the legacy mirror and #813 treats that as an unpatched npm 6 install (regression) #879

Description

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

Summary

After a vendored scan of an npm lockfileVersion 2 project, an ordinary npm install with npm 7, 8 or 10 re-saves the lock. npm's serializer never writes resolved for a file: resolution in the legacy dependencies mirror, so the mirror node keeps only version and the patched integrity. The packages half is still wired to .socket/vendor/…, and the installed tree is patched.

Since #813 (the #432 fix, f023506), drop_mirror_unwired reads a mirror node with no resolved as "resolves from a non-Socket source". So:

  • vex refuses the patch: vendor_unwired / patched_ref_unattributable ("… still resolves it to "no resolved url", so npm 6 installs the unpatched bytes"), exit 1.
  • vendor --check exits 1: vendor_check_failed "wiring missing: no lockfile or config references .socket/vendor/npm/ any more, so a fresh install gets the unpatched package".

Both claims are false. The packages entry still references the artifact, a fresh npm 7+ npm ci installs the patched bytes, and npm 6 doesn't install unpatched bytes from that mirror node either: the node keeps the patched integrity, so a cold-cache npm 6 npm ci fails closed with EINTEGRITY (ledger #302 already records this as npm behaviour). That's the same fail-closed outcome #813 accepts for a hosted alias mirror node (redirect_npm_legacy_alias_client), which vex still attests.

Impact

Every npm 7/8 team, and every npm 9–11 team with an existing v2 lock, that vendors a patch and then runs npm install for any reason gets a red vendor --check in CI and loses its VEX statement. The suggested remedy loops: re-running scan --mode vendored re-adds resolved (check passes), and the next npm install strips it again. Hosted mode isn't affected, because npm keeps an http(s) resolved in the mirror.

Repro (Linux, npm 8.19.4, Node 22; a local mock patch API serving one free left-pad patch)

mkdir p && cd p
echo '{"name":"app","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
npm install                                  # npm 8 → lockfileVersion 2
socket-patch scan --mode vendored --yes $API # exit 0
socket-patch vendor --check                  # exit 0
socket-patch vex --product pkg:npm/app@1.0.0 -O v.json   # exit 0, 1 statement
npm install                                  # no changes; re-saves the lock
#   "dependencies": { "left-pad": { "version": "1.3.0", "integrity": "sha512-<patched>" } }   ← resolved dropped
head -c 22 node_modules/left-pad/index.js    # still patched
socket-patch vendor --check                  # exit 1: vendor_check_failed "wiring missing"
socket-patch vex --product pkg:npm/app@1.0.0 -O v.json   # exit 1: vendor_unwired / patched_ref_unattributable
socket-patch scan --mode vendored --yes $API # re-adds resolved; check passes
npm install                                  # strips it again; check fails again

The npm 6 claim, checked on the re-saved lock: npm6 ci --cache <empty> gives exit 1 with EINTEGRITY wanted sha512-<patched> but got sha512-<registry>.

Expected vs actual

  • Expected: a packages entry wired to the vendored artifact, plus a mirror node with the matching patched integrity and no resolved, counts as wired (or at worst gets an npm 6 caveat warning). That's what vendor --check and vex did before Fix npm 6 installing unpatched aliases (#432) #813. CLI_CONTRACT / vendor --check promise a failure only when "a fresh install gets the unpatched package", and none does here.
  • Actual: exit 1 from both, with a diagnosis saying no lockfile references the artifact.

Matrix (Linux)

npm lock npm install re-save vendor --check vex
7.24.2 v2 strips mirror resolved fail fail
8.19.4 v2 (plain dep and an lp@npm:left-pad alias) strips fail (3×) fail
10.9.4 existing v2 (kept as v2) strips fail fail
any v3 (no mirror) n/a pass pass
hosted, 8.19.4 v2 keeps http resolved n/a pass

macOS and Windows weren't probed (probe branches are paused), but the logic is OS-independent.

Bisect

Suspect code

crates/socket-patch-core/src/vex/discover/npm.rs:289-301 (drop_mirror_unwired): node.resolved.map_or_else(Located::default, …) turns a missing resolved into "neither vendored nor hosted", then into resolved_elsewhere plus unwired. A mirror node with no resolved whose integrity equals the wired packages entry's (patched) integrity should contest nothing. The vendor --check wiring verdict seems to come from the same discovery, given the identical exit and timing.

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