Skip to content

Bun isolated linker: transitive packages under node_modules/.bun are "not installed" in agent mode, and scan --mode agent exits 0 with them unpatched #366

Description

[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

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