Skip to content

Bun hosted and vendored rewiring rewrites bundled: true lock entries that Bun never fetches: the bundled copy stays unpatched, scan reports success, and vendored vex attests not_affected #469

Description

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

Summary

Bun records a package's bundleDependencies in bun.lock as their own entries, keyed parent/child and carrying { "bundled": true }:

"@bh/bund/is-number": ["[email protected]", "", { "bundled": true }, "sha512-…"],

Bun unpacks these from the parent's tarball and never fetches them. That's the same reason #325 / #337 made npm skip and contest inBundle copies. The Bun backends have no such handling:

  1. Hosted (scan --mode hosted) rewrites the bundled entry to the Socket URL. It reports redirected: 1, status: success and no warning. npm emits redirect_npm_bundled_instance_skipped for the same shape.
  2. Vendored (scan --mode vendored) rewrites it to .socket/vendor/npm/<uuid>/…tgz, also silently.
  3. After a fresh bun install --frozen-lockfile, the copy the parent actually loads (node_modules/<parent>/node_modules/<pkg>) is unpatched. On text locks with Bun ≥ 1.2, Bun also hoists an extra, patched top-level copy from the rewritten entry, which nothing requires.
  4. Vendored vex (default, with verification) attests not_affected for that purl on every Bun version and lock format. On Bun 1.1.45 and on bun.lockb there is no patched copy anywhere on disk. Hosted vex --no-verify attests too, although --no-verify must skip only the hashing, never the wiring/contest gates. Hosted default vex does refuse (not_applied), because its installed-tree check sees the bundled copy.

vex/discover/bun.rs and both Bun rewriters never look at bundled (grep -n bundled returns nothing in any *bun* source file).

Impact

A project whose dependency bundles a vulnerable package gets a successful hosted/vendored run and, in vendored mode, a verified OpenVEX not_affected statement, while the code that actually runs is the unpatched bundled copy. It's the Bun twin of #325, which was fixed for npm only.

Repro (Linux, any Bun ≥ 1.1.39; mock patch API, no secrets)

Fixture: a scoped package @bh/[email protected] with "bundleDependencies": ["is-number"], shipping node_modules/is-number (7.0.0) inside its tarball, served from a local registry scope. A mock patch API serves a hosted/vendored patch for pkg:npm/[email protected] that prepends /* SOCKET-PATCHED */ to index.js. The routes are the same ones e2e_redirect_bun_build.rs mocks: patches/batch, by-package, patches/package, view/<uuid>, plus the hosted tarball.

mkdir p && cd p
echo '{"name":"p","version":"1.0.0","dependencies":{"@bh/bund":"1.0.0"}}' > package.json
printf '[install.scopes]\n"@bh" = "http://127.0.0.1:18765/npm/"\n' > bunfig.toml
bun install                                  # bun.lock gets "@bh/bund/is-number": [..., {"bundled": true}, ...]
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18765
G="--api-url http://127.0.0.1:18765 --org org --api-token fake"
socket-patch scan --mode vendored --json --yes --cwd . $G   # status success, no warnings
grep bundled bun.lock
#  "@bh/bund/is-number": ["[email protected]/vendor/npm/<uuid>/is-number-7.0.0.tgz", { "bundled": true }, "sha512-<patched>"],
# fresh checkout, cold cache
rm -rf node_modules && BUN_INSTALL_CACHE_DIR=$(mktemp -d) bun install --frozen-lockfile    # exit 0
grep -c SOCKET-PATCHED node_modules/@bh/bund/node_modules/is-number/index.js            # 0  <- the copy @bh/bund loads
node -e 'const p=require.resolve("is-number",{paths:[require("path").dirname(require.resolve("@bh/bund"))]});console.log(p)'
#   …/node_modules/@bh/bund/node_modules/is-number/index.js  (unpatched)
socket-patch vex --cwd . --json $G -O out.vex.json; echo $?   # 0, event "verified", statement not_affected

Hosted is the same with --mode hosted: redirected: 1, no warning, the bundled copy unpatched after the frozen install, and vex --no-verify exits 0 with not_affected.

Expected vs actual

  • Expected: CLI_CONTRACT.md, "Contested locks": "A bundled npm copy … of the same name@version contests the reference … npm unpacks it from the parent package's tarball, so no rewire reaches it and it stays unpatched." Bun's bundled: true entry has exactly those semantics. So the rewriters should skip it with a warning (like redirect_npm_bundled_instance_skipped / vendor_bundled_instance_skipped), or refuse like vlt's vendor_bundled_deps_unsupported. VEX should report patched_ref_unattributable and not attest, including under --no-verify (the contract's "Liveness gates": --no-verify skips only the hashing).
  • Actual: the bundled entry is rewired silently, the run succeeds, and vendored vex (verified) and hosted vex --no-verify both attest not_affected while the loaded copy is unpatched.

Matrix (Linux; real Bun from npm @oven/bun-linux-x64; main d984832; shape: bundled copy only)

Bun lock hosted scan vendored scan bundled copy after frozen install hoisted phantom copy vendored vex hosted vex hosted vex --no-verify
1.1.45 text v0 success, no warning success, no warning unpatched none not_affected not_applied (ok) not_affected
1.1.45 bun.lockb success success unpatched none not_affected not_applied not_affected
1.2.23 text v1 success success unpatched patched, unused not_affected not_applied not_affected
1.3.14 text v1 success success unpatched patched, unused not_affected not_applied not_affected
1.4.2 text v2 success success unpatched patched, unused not_affected not_applied not_affected
1.4.2 bun.lockb success success unpatched none not_affected not_applied not_affected

Each 1.4.2 text cell reproduced on 2+ fresh runs. The "both" shape (the project also depends on [email protected] directly) gives the same result: both entries are rewired, and the bundled copy stays unpatched. macOS and Windows weren't probed. The behaviour is in lockfile parsing and rewriting, which isn't OS-specific.

First bad version: not a regression. Release 4.0.0 behaves the same, and also attests in hosted default vex.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:3633 (rewrite_bun_lock entry loop: matches on the spec only, not the {meta}.bundled flag)
  • crates/socket-patch-core/src/vendor/bun_lock.rs:285 / :1013 (vendored text rewrite)
  • crates/socket-patch-core/src/patch/redirect/bun_binary.rs:18, crates/socket-patch-core/src/vendor/bun_binary.rs:25 (binary lock: the bundled record is rewritten too)
  • crates/socket-patch-core/src/vex/discover/bun.rs:119 (discovery takes the bundled entry as a live Socket ref, and has no bundled contest like vex/discover/npm.rs got in Fix npm VEX attesting packages with an unpatched bundled copy (#325) #337)

Related: #325 (npm, fixed by #337), #405 (another Bun VEX false attestation).

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