Repository navigation
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": ["is-number@7.0.0", "", { "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:
- Hosted (
scan --mode hosted) rewrites the bundled entry to the Socket URL. It reportsredirected: 1,status: successand no warning. npm emitsredirect_npm_bundled_instance_skippedfor the same shape. - Vendored (
scan --mode vendored) rewrites it to.socket/vendor/npm/<uuid>/…tgz, also silently. - 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 nothingrequires. - Vendored
vex(default, with verification) attestsnot_affectedfor that purl on every Bun version and lock format. On Bun 1.1.45 and onbun.lockbthere is no patched copy anywhere on disk. Hostedvex --no-verifyattests too, although--no-verifymust skip only the hashing, never the wiring/contest gates. Hosted defaultvexdoes 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/bund@1.0.0 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/is-number@7.0.0 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": ["is-number@.socket/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_affectedHosted 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@versioncontests the reference … npm unpacks it from the parent package's tarball, so no rewire reaches it and it stays unpatched." Bun'sbundled: trueentry has exactly those semantics. So the rewriters should skip it with a warning (likeredirect_npm_bundled_instance_skipped/vendor_bundled_instance_skipped), or refuse like vlt'svendor_bundled_deps_unsupported. VEX should reportpatched_ref_unattributableand not attest, including under--no-verify(the contract's "Liveness gates":--no-verifyskips only the hashing). - Actual: the bundled entry is rewired silently, the run succeeds, and vendored
vex(verified) and hostedvex --no-verifyboth attestnot_affectedwhile 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 is-number@7.0.0 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_lockentry loop: matches on the spec only, not the{meta}.bundledflag)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 likevex/discover/npm.rsgot 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
bun: run 4 ledger (#469)
mikolalysenko commentedon Oct 1, 2026
[agent] Triage: priority:p1 (Bun, npm-family). Not a duplicate. No open or merged PR covers it.
Shares root cause with #471: the bundled-copy handling from #337 (#325) exists only on the npm-lock paths (
vex/discover/npm.rs,redirect_npm_bundled_instance_skippedinpatch/redirect/mod.rs,vendor_bundled_instance_skippedinvendor/npm_lock.rs). The Bun and vlt rewriters and their VEX discovery (vex/discover/bun.rs,vex/discover/vlt.rs,patch/redirect/vlt.rs,vendor/bun_lock.rs) have no bundled-copy skip or contest, so they rewire or attest a copy that only the parent's tarball supplies. I checked on currentmain:grep bundledfinds nothing in any Bun source file, and nothing in vlt discovery. Will be fixed together.Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Claiming this issue (with #471; shared root cause: bundled-copy skip/contest from #337 exists only for npm locks, not Bun or vlt). Branch: agent/fix-bundled-copy-bun-vlt. Claim-ID: 2026-10-01T14:20:51Z-cf03cb
Generated by Claude Code
mikolalysenko commentedon Oct 1, 2026
[agent] Draft PR for this cluster: #472
Generated by Claude Code
Skip bundled bun.lock entries, never attest them
Skip bundled bun.lockb records, never attest them
Keep bun bundled copies out of in-run VEX
Fix Bun/vlt bundled copies left unpatched (#469, #471) (#472)