Skip to content

After npm install <pkg>@<other version> moves a vendored npm package off its patched version, scan --prune, vendor --revert, remove and rollback all drift-keep it, so vendor --check stays red and every remedy it names loops #1155

Description

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

Summary

Vendor a package, then upgrade or downgrade it with npm (npm install ms@2.1.3 over a vendored ms@2.1.2). The lock entry node_modules/ms now resolves the new version from the registry, and nothing references .socket/vendor/npm/<uuid>/ any more. After that:

  • vendor --check exits 1: dependency removed: no lockfile resolves pkg:npm/ms@2.1.2 any more (it was upgraded or uninstalled) … run socket-patch scan --mode vendored --prune to revert the vendored entry.
  • scan --mode vendored --prune exits 0 and reverts nothing. --json shows gc.keptVendoredEntries: ["pkg:npm/ms@2.1.2"]. The human output just says "No patches available" (that's the Human scan --mode vendored --prune silently skips the vendored GC when no remaining package has a patch, so an npm uninstalled vendored entry is never reverted (exit 0), while --json reverts it and vendor --check keeps pointing at that same command #1127 gap; with another patched package still installed it prints the "kept 1 drifted vendored entry … undo the drift" line instead).
  • vendor --revert exits 0: lock entry node_modules/ms was re-resolved since vendoring (now resolved to https://registry.npmjs.org/ms/-/ms-2.1.3.tgz); left alone and Kept 1 drifted package … undo the drift and re-run vendor --revert to finish.
  • remove pkg:npm/ms@2.1.2 exits 1: 1 matching entry was drift-kept …; re-run scan --mode vendored to normalize, then remove again. Re-running the scan changes nothing.
  • rollback pkg:npm/ms@2.1.2 exits 1: Kept vendored state … lockfile wiring drifted.
  • vendor --check is still 1.

"Undo the drift" would mean downgrading back to the vulnerable version. The only way out is deleting .socket/vendor/npm/<uuid>/ and editing .socket/vendor/state.json by hand.

The Bun routine found this first and handed it to the npm ledger. Bun's text and binary locks behave the same. #1132 (Bun bun.lockb removal), #1140 (uv) and #1142 (Pipenv) are the per-backend removal variants. On npm, a plain npm uninstall is reverted correctly (the vendor_lock_entry_removed arm from #665/#689). Only the re-resolved case loops.

Impact

Any project that upgrades a vendored dependency (the normal way to drop a patch once upstream ships a fix, and something Dependabot or Renovate does by itself) is left with a committed orphan tarball and a ledger entry. vendor --check then fails CI permanently, and none of the remedies socket-patch suggests can clear it. Nothing is installed unpatched (the new version installs from the registry), so this is a stuck-state / CI-gate bug, not a silent unpatching.

Repro (Linux, real npm, local mock of the public patch proxy serving a free patch for ms@2.1.2)

export SOCKET_PROXY_URL=http://127.0.0.1:18732 SOCKET_API_URL=http://127.0.0.1:18732 NO_PROXY=127.0.0.1
git init -q proj && cd proj
echo '{"name":"proj","version":"1.0.0","dependencies":{"ms":"2.1.2"}}' > package.json
npm install && git add -A && git commit -qm init
socket-patch scan --mode vendored && npm install && git add -A && git commit -qm vendored
socket-patch vendor --check; echo $?          # 0: committed artifact and wiring verified
npm install ms@2.1.3                          # lock: node_modules/ms -> registry ms-2.1.3.tgz
socket-patch vendor --check; echo $?          # 1: "dependency removed … upgraded or uninstalled … run scan --mode vendored --prune"
socket-patch scan --mode vendored --prune --json | jq '.gc | {revertedVendoredEntries, keptVendoredEntries}'
                                              # {"revertedVendoredEntries": [], "keptVendoredEntries": ["pkg:npm/ms@2.1.2"]}
socket-patch vendor --revert; echo $?         # 0, "Kept 1 drifted package … undo the drift"
socket-patch remove pkg:npm/ms@2.1.2; echo $? # 1, "re-run scan --mode vendored to normalize, then remove again"
socket-patch rollback pkg:npm/ms@2.1.2; echo $? # 1, "lockfile wiring drifted"
socket-patch vendor --check; echo $?          # still 1
ls .socket/vendor/npm/                        # 22222222-… still there

A downgrade (left-pad 1.3.0 → npm install left-pad@1.2.0) behaves the same.

Expected vs actual

  • Expected: CLI_CONTRACT.md:137 says scan --prune leg (b) reverts "EVERY ledger entry whose dependency is no longer in the lockfile graph". pkg:npm/ms@2.1.2 is no longer in the graph. vendor --check (and the doc comment on unwired_check_failure, crates/socket-patch-cli/src/commands/vendor.rs:396) names "upgraded or uninstalled" and scan --prune as the fix. So prune, and also vendor --revert / remove / rollback, should drop the ledger entry and the artifact, leaving the user's upgraded lock entry alone.
  • Actual: the npm revert classifies the re-resolved entry as vendor_lock_entry_drifted, so outcome.drift_skipped() keeps the artifact and ledger entry. Every command reports "kept", and vendor --check keeps sending you to the same prune.

Matrix (main c4235a2, Linux, Node 22.22 / Node 24.21 for npm 12)

OS npm lock upgrade (ms 2.1.2→2.1.3) downgrade (left-pad 1.3.0→1.2.0) npm uninstall (control)
Linux 8.19.4 v2 loops – reverted (verified 2026-10-07, ledger #302)
Linux 10.9.4 v3 loops (×2) loops reverted (--json prune, this run)
Linux 12.2.0 v3 loops – reverted (verified 2026-10-07, ledger #302)

(On npm 8 the pre-upgrade vendor --check is already red because of #879. That's unrelated.) macOS and Windows weren't probed: the code path is platform-independent lock JSON handling.

Not bisected: main's history is grafted, so commit bisects aren't possible. The "upgraded or uninstalled … --prune" check text came with #970/#1050.

Suspect code

  • crates/socket-patch-core/src/vendor/npm_lock.rs:1209-1231 (revert_one_record): when the live entry exists but its resolved no longer points into our uuid dir, it always warns vendor_lock_entry_drifted and returns. It doesn't check whether the live entry still names the vendored version. An entry whose version differs from rec.original/rec.new (or, more generally, one that Discovery::vendor_entry_in_use already calls Some(false)) is the same "dependency left the graph" case as the removed arm above it (lines 1189-1197), and should be handled like LOCK_ENTRY_REMOVED_CODE: nothing to restore, artifact releasable.
  • crates/socket-patch-cli/src/commands/vendor.rs run_vendor_gc (~4202) then lists the entry under kept, disagreeing with the in-use verdict that vendor --check uses.

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