Skip to content

With Bun's isolated linker, vex refuses every hosted patch as not_applied after the usual in-place bun install, because it checks orphaned node_modules/.bun registry entries that Bun never removes (regression from #496) #599

Description

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

Summary

Since #496 (commit 35de754, the fix for #366 / #405), the npm crawler walks node_modules/.bun/<name>@<version>… store entries. Bun's isolated linker never prunes that store. After a hosted scan, the developer runs bun install --frozen-lockfile in the same checkout. Bun then adds new entries named is-number@http+++…patch+npm+is-number+6.0.0+… and re-links every importer to them. The old is-number@6.0.0 registry entry stays on disk, but nothing links to it. Nothing can load it any more.

vex counts that orphan as an installed copy, finds the original bytes, and drops every patch from the document as not_applied. It exits 1, even though every copy the runtime can load is patched. A fresh clone with an empty node_modules attests correctly, so only the in-place workflow is affected, and that's the one a developer uses locally.

In vendored mode the same orphans produce false vendored_tree_out_of_sync warnings ("re-run your package manager's install to resync it"). Re-running the install can't clear them, because Bun keeps the orphans.

Impact

  • Hosted + isolated linker (the default for a fresh Bun ≥ 1.3 workspace): scan, then bun install, then vex refuses all patches with exit 1. A CI job that runs vex on a warm or persisted node_modules fails, and so does a local pre-commit check. Following the advice (re-run the install) doesn't help. Only rm -rf node_modules does.
  • Vendored: the attestation is right, but you get a spurious out-of-sync warning for each patch.
  • The same orphan state comes from any earlier version churn: a removed dependency, or an upgrade that leaves an old name@version store dir behind.

Repro (Linux, real Bun, local patch-API mock)

# usage: repro-orphan.sh <bun> <dir> <hosted|vendored>
B=$1; D=$2; M=$3
rm -rf "$D"; mkdir -p "$D/packages/a"; cd "$D"
printf '{"name":"root","version":"1.0.0","private":true,"workspaces":["packages/*"],"dependencies":{"is-odd":"3.0.1"}}' > package.json
printf '{"name":"a","version":"1.0.0","dependencies":{"is-number":"6.0.0","left-pad":"1.3.0"}}' > packages/a/package.json
printf '[install]\nlinker = "isolated"\n' > bunfig.toml
"$B" install                                  # node_modules/.bun/is-number@6.0.0 ...
socket-patch scan --mode "$M" --json          # exit 0, bun.lock rewired
"$B" install --frozen-lockfile                # same checkout
ls node_modules/.bun                          # is-number@6.0.0 AND is-number@http+++… (orphan kept)
node -p "require.resolve('is-number',{paths:[require('fs').realpathSync('packages/a')]})"   # -> the patched http+++ entry
socket-patch vex --product pkg:npm/root@1.0.0 --output vex.json --json   # exit 1

Output on main 203e092, hosted, Bun 1.4.2:

store entries: is-number@6.0.0  is-number@http+++127.0.0.1+18999+patch+npm+is-number+6.0.0+…  is-odd@3.0.1  is-odd@http+++…  left-pad@1.3.0  left-pad@http+++…  node_modules
runtime is-number -> is-number@http+++…/node_modules/is-number/index.js   (patched)
vex exit=1  error 0 verified; [('pkg:npm/is-number@6.0.0','not_applied'), ('pkg:npm/is-odd@3.0.1','not_applied'), ('pkg:npm/left-pad@1.3.0','not_applied')]

rm -rf node_modules && bun install --frozen-lockfile, then vex → exit 0, 3 verified. Vendored, Bun 1.4.2: vex exit 0 with 3 verified, plus 2 vendored_tree_out_of_sync warnings although the tree is in sync.

The org-scoped API was mocked locally (the sandbox can't reach patches-api). The mock serves the batch, view, patches/package and tarball routes, plus a registry passthrough via SOCKET_NPM_REGISTRY. The patches append a marker line to index.js.

Expected vs actual

  • Expected: vex attests a patch when every copy the install can actually load is patched. Today's code already does this for a fresh clone, and for the hoisted linker, where Bun replaces the dir in place. A store entry that no importer, entry node_modules link or .bun/node_modules hoist link points to isn't an installed copy. The npm/pnpm store walks have the same property, because pnpm prunes its .pnpm entries on install.
  • Actual: unreachable .bun entries are treated as installed copies. Hosted → not_applied (exit 1). Vendored → vendored_tree_out_of_sync.

OS × version

OS Bun linker hosted vex after in-place frozen install vendored
Linux 1.4.2 isolated (workspace default) fail (2/2 runs) false out-of-sync warnings
Linux 1.3.14 isolated (workspace default) fail untested
Linux 1.2.23 isolated (linker = "isolated") fail untested
Linux 1.4.2 hoisted pass (no store) pass
Linux any isolated, fresh clone pass pass
macOS / Windows — — untested (probe branches currently blocked) —

First bad commit

35de754 (#496). Before it, the crawler never looked into .bun, which was #405 (the opposite failure: attesting without checking the bytes). Release 4.0.0 predates it.

Suspect code

  • crates/socket-patch-core/src/crawlers/npm_crawler.rs:1443 and :2132 (list_pnpm_shaped_store_entries_sync) enumerate every .bun entry dir as a package location, with no reachability check.
  • crates/socket-patch-core/src/vex/verify.rs:211-224 takes every crawled copy as an installed copy (package_copies) for the not_applied / out-of-sync verdicts.

Possible directions: for StoreLayout::Bun, count only entries reachable from an importer or the hoist links. Or, when a lock-wired hosted/vendored entry exists, ignore an orphaned registry-named entry. Bun's own node_modules/.bun/node_modules hoist dir and the importer links name exactly the live entries.

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