[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
With Bun's isolated linker, every package lives at node_modules/.bun/<name>@<version>/node_modules/<name>. The importer's node_modules only holds symlinks to its direct deps. Agent mode never looks inside node_modules/.bun, so a transitive dependency is treated as lockfile-only. scan --mode agent reports it skipped / package_not_installed, doesn't record it in the manifest, and exits 0 with status: "success". The installed copy stays unpatched.
The isolated linker is Bun's default for new workspaces from Bun 1.3.x on (configVersion: 1 locks). On a fresh Bun 1.3.14 or 1.4.2 workspace with no bunfig.toml, agent mode doesn't patch any transitive dependency. It's also opt-in for any project through [install] linker = "isolated".
This is the Bun twin of #359 (npm install-strategy=linked / .store) and #362 (pnpm custom virtual store). The code path is different: the .bun store directory.
Impact
- The vulnerable transitive copy stays on disk while
scan --mode agent --json --yes exits 0 with success. The patch isn't even written to .socket/manifest.json, so a later apply or the postinstall hook never retries it.
get <uuid> and apply on such a purl fail loudly (partial_failure, exit 1, package_not_installed), which is at least honest. The silent path is scan.
- VEX correctly omits the package (
package_not_found), so there's no false attestation.
Repro (Linux, bun 1.4.2, main f6b7fb9)
mkdir iso && cd iso
printf '{"name":"app","version":"1.0.0","dependencies":{"mkdirp":"0.5.6","left-pad":"1.3.0"}}\n' > package.json
printf '[install]\nlinker = "isolated"\n' > bunfig.toml
bun install
ls node_modules/.bun # [email protected] [email protected] [email protected] node_modules
# patch API mock serving free patches for [email protected] and [email protected] (the probe below embeds one)
socket-patch scan --mode agent --json --yes --api-url http://127.0.0.1:18901 --org test-org --api-token fake
echo $? # 0
head -c 22 node_modules/.bun/[email protected]/node_modules/minimist/index.js # unpatched
Envelope excerpt: "status": "success", "lockfileOnlyPackages": 1, apply.patches: left-pad added (the direct symlink works), minimist skipped package_not_installed. node -e 'require.resolve("minimist",{paths:[dirname(require.resolve("mkdirp"))]})' resolves to the .bun/[email protected]/... copy, so the package is installed and in use.
The default-linker workspace shape reproduces the same way with no bunfig: a root {"workspaces":["packages/*"]} plus packages/a depending on [email protected], on bun 1.3.14 / 1.4.2.
Control: the same project with linker = "hoisted" patches both packages.
Expected vs actual
- Expected: CLI_CONTRACT.md "Deeply nested transitive dependencies are fully supported", which says
apply "patches a package by PURL … regardless of how deep in the dependency tree it was installed". The crawler already walks pnpm's .pnpm, legacy .<registry> stores and vlt's .vlt store for exactly this transitive-only-home layout. pkg_managers.rs even documents that bun's isolated linker "populates node_modules/.bun/".
- Actual:
.bun falls through to the generic hidden-entry skip, so its entries are never probed or crawled.
OS × version
| OS |
Bun 1.2.23 |
Bun 1.3.14 |
Bun 1.4.2 |
| Linux (sandbox + ubuntu-latest) |
fail (bunfig isolated); default workspace is hoisted, pass |
fail (bunfig isolated and default workspace) |
fail (both) |
| macOS (macos-latest) |
fail (bunfig isolated); default workspace hoisted, pass |
fail (both) |
fail (both) |
| Windows (windows-latest) |
fail (bunfig isolated) |
fail (bunfig isolated); default-workspace cell inconclusive (bun install exited 1 on the runner) |
same as 1.3.14 |
Releases: it's the same on 4.0.0 and 3.3.0 (npm @socketsecurity/socket-patch), so it isn't a regression.
Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1069: the nested-walk hidden-entry skip used by find_by_purls (apply / rollback / get). .pnpm, .vlt and legacy pnpm stores are special-cased just above it; .bun is not.
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1392: the same skip in the crawl_all walk (scan), which is why the package shows up only through the lockfile supplement (lockfileOnlyPackages).
crates/socket-patch-core/src/crawlers/pkg_managers.rs:93: the detector knows about .bun/ but only uses it for classification.
Probe run (3 OS × bun 1.2.23 / 1.3.14 / 1.4.2, main built on each runner): https://github.com/SocketDev/socket-patch/actions/runs/36765789215
[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
With Bun's isolated linker, every package lives at
node_modules/.bun/<name>@<version>/node_modules/<name>. The importer'snode_modulesonly holds symlinks to its direct deps. Agent mode never looks insidenode_modules/.bun, so a transitive dependency is treated as lockfile-only.scan --mode agentreports itskipped/package_not_installed, doesn't record it in the manifest, and exits 0 withstatus: "success". The installed copy stays unpatched.The isolated linker is Bun's default for new workspaces from Bun 1.3.x on (
configVersion: 1locks). On a fresh Bun 1.3.14 or 1.4.2 workspace with nobunfig.toml, agent mode doesn't patch any transitive dependency. It's also opt-in for any project through[install] linker = "isolated".This is the Bun twin of #359 (npm
install-strategy=linked/.store) and #362 (pnpm custom virtual store). The code path is different: the.bunstore directory.Impact
scan --mode agent --json --yesexits 0 withsuccess. The patch isn't even written to.socket/manifest.json, so a laterapplyor the postinstall hook never retries it.get <uuid>andapplyon such a purl fail loudly (partial_failure, exit 1,package_not_installed), which is at least honest. The silent path isscan.package_not_found), so there's no false attestation.Repro (Linux, bun 1.4.2, main
f6b7fb9)Envelope excerpt:
"status": "success","lockfileOnlyPackages": 1,apply.patches:left-padadded(the direct symlink works),minimistskippedpackage_not_installed.node -e 'require.resolve("minimist",{paths:[dirname(require.resolve("mkdirp"))]})'resolves to the.bun/[email protected]/...copy, so the package is installed and in use.The default-linker workspace shape reproduces the same way with no bunfig: a root
{"workspaces":["packages/*"]}pluspackages/adepending on[email protected], on bun 1.3.14 / 1.4.2.Control: the same project with
linker = "hoisted"patches both packages.Expected vs actual
apply"patches a package by PURL … regardless of how deep in the dependency tree it was installed". The crawler already walks pnpm's.pnpm, legacy.<registry>stores and vlt's.vltstore for exactly this transitive-only-home layout.pkg_managers.rseven documents that bun's isolated linker "populatesnode_modules/.bun/"..bunfalls through to the generic hidden-entry skip, so its entries are never probed or crawled.OS × version
bun installexited 1 on the runner)Releases: it's the same on 4.0.0 and 3.3.0 (npm
@socketsecurity/socket-patch), so it isn't a regression.Suspect code
crates/socket-patch-core/src/crawlers/npm_crawler.rs:1069: the nested-walk hidden-entry skip used byfind_by_purls(apply / rollback / get)..pnpm,.vltand legacy pnpm stores are special-cased just above it;.bunis not.crates/socket-patch-core/src/crawlers/npm_crawler.rs:1392: the same skip in thecrawl_allwalk (scan), which is why the package shows up only through the lockfile supplement (lockfileOnlyPackages).crates/socket-patch-core/src/crawlers/pkg_managers.rs:93: the detector knows about.bun/but only uses it for classification.Probe run (3 OS × bun 1.2.23 / 1.3.14 / 1.4.2, main built on each runner): https://github.com/SocketDev/socket-patch/actions/runs/36765789215