[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
Before editing a bun.lockb, main recomputes Bun's package metadata hash and refuses the lock if no hash dialect matches the stored one (hash_style, added in de316b4 / #358). The recomputation sorts two versions of the same package wrongly when they share major.minor.patch and only one has a prerelease tag: X@1.0.0 vs X@1.0.0-beta.1. It falls back to a plain string compare of the prerelease field, so "" (the release) sorts before "beta.1". Semver and Bun put the prerelease first. The recomputed hash never matches, so a perfectly valid, Bun-written bun.lockb is rejected:
redirect_bun_lockb_invalid / vendor_bun_lockb_invalid:
bun.lockb: package metadata hash does not match the lockfile; run bun install to refresh it before patching
The whole lock is refused, so no package in the project can be patched, not just the prerelease pair. The pair can be anywhere in the tree, e.g. one dependency pins foo@1.0.0-rc.1 and another foo@1.0.0.
Impact
- Hosted mode fails open:
scan --mode hosted reports status: success, redirected: 0, exit 0, with only the warning. CI keeps installing unpatched dependencies and the job stays green.
- Vendored mode fails loudly (
partial_failure, exit 1, vendor_bun_lockb_invalid). It still can't patch.
- The remedy doesn't work:
bun install leaves bun.lockb byte-identical (Bun considers the hash valid, and bun install --frozen-lockfile succeeds), so the user is stuck.
- Regression: release v4.0.0 rewrites the same lock, and Bun 1.2.23 and 1.4.2 frozen installs of its output install the patched bytes.
Repro (real Bun, local mock registry + patch API)
# registry: plain-pkg@1.0.0 (patched by the mock API), pre-pkg@1.0.0-beta.1, pre-pkg@1.0.0,
# dep-x@1.0.0 -> { "pre-pkg": "1.0.0" }
mkdir p && cd p
echo '{"name":"app","version":"1.0.0","dependencies":{"plain-pkg":"1.0.0","pre-pkg":"1.0.0-beta.1","dep-x":"1.0.0"}}' > package.json
printf '[install]\nregistry = "http://127.0.0.1:18731/npm/"\nsaveTextLockfile = false\n' > bunfig.toml
bun install # writes bun.lockb (pre-pkg@1.0.0-beta.1 at root, dep-x/pre-pkg@1.0.0 nested)
bun install --frozen-lockfile # ok: Bun accepts the lock and its hash
socket-patch scan --mode hosted --json --yes # (SOCKET_API_URL / SOCKET_PATCH_SERVER_URL -> mock)
# exit 0, "status":"success", "redirected":0,
# warnings: [redirect_bun_lockb_invalid "package metadata hash does not match the lockfile; run bun install…"]
bun install && cmp bun.lockb <saved copy> # unchanged; re-running the scan gives the same result
socket-patch scan --mode vendored --json --yes # exit 1, partial_failure, vendor_bun_lockb_invalid
Controls, each with the same patched plain-pkg@1.0.0, all redirected: 1:
pre-pkg@1.0.0-beta.1 + pre-pkg@0.9.0 (different major.minor.patch).
pre-pkg@1.0.0-beta.1 + pre-pkg@1.0.0-beta.2 (both prerelease, so compare_prerelease is used).
- No second
pre-pkg.
Expected vs actual
- Expected: docs/testing/bun-compatibility.md ("Native
bun.lockb support") says binary locks are parsed and patched directly, and bun.lockb is in the supported-format table for hosted and vendored. A lock that Bun itself writes and accepts under --frozen-lockfile should be patched. The redirect_bun_lockb_invalid refusal is meant for a stale hash, which bun install fixes. Here it fires on a lock whose hash is valid. A refusal that fires when it shouldn't is a bug, and in hosted mode it also exits 0.
- Actual: the refusal fires for every lockb with a release + same-triple prerelease pair, nothing is patched, and hosted exits 0.
Matrix (Linux, main 045d7ec)
bun.lockb writer (Bun) |
release + prerelease pair |
older release + prerelease |
two prereleases |
no pair |
v4.0.0, pair |
| 1.1.45 |
refused (hosted exit 0) |
pass |
pass |
pass |
– |
| 1.2.23 |
refused |
pass |
pass |
pass |
– |
| 1.3.9 |
refused |
pass |
pass |
pass |
– |
| 1.4.2 |
refused (2/2, also after bun install); vendored exit 1 |
pass |
pass |
pass |
pass (frozen install by 1.2.23 and 1.4.2 is patched) |
Also reproduced with a scoped parent (@sc/Mixed.Name_x@0.1.0-rc.0 → nested pre-pkg@1.0.0, root pre-pkg@1.0.0-beta.1; 1.1.45 writer). Text bun.lock is unaffected: same shape, hosted and vendored pass on 1.4.2. macOS and Windows weren't probed, but the code path is OS-independent.
First bad commit: de316b4 (#358) per git log -S "fn hash_style", which added the pre-edit hash validation. v4.0.0 passes.
Suspect code
crates/socket-patch-core/src/vendor/bun_lockb.rs:1281-1287 (meta_hash sort): if !legacy && !a.3.is_empty() && !b.3.is_empty() { compare_prerelease(..) } else { a.3.cmp(&b.3)… }. When exactly one side has a prerelease, the string compare puts "" first. Semver precedence (and Bun's Version.order) needs the prerelease side to be Less. (In the legacy dialect, two numeric prerelease identifiers such as beta.2 vs beta.10 are also string-compared; I haven't verified that against a legacy writer.)
crates/socket-patch-core/src/vendor/bun_lockb.rs:1148 hash_style: turns the mismatch into the refusal.
- Hosted surfaces the refusal only as a warning with
status: success and exit 0.
No probe runs: the routine's earlier probe branches can't be deleted from the sandbox, so no new ones were pushed.
[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
Before editing a
bun.lockb, main recomputes Bun's package metadata hash and refuses the lock if no hash dialect matches the stored one (hash_style, added in de316b4 / #358). The recomputation sorts two versions of the same package wrongly when they sharemajor.minor.patchand only one has a prerelease tag:X@1.0.0vsX@1.0.0-beta.1. It falls back to a plain string compare of the prerelease field, so""(the release) sorts before"beta.1". Semver and Bun put the prerelease first. The recomputed hash never matches, so a perfectly valid, Bun-writtenbun.lockbis rejected:The whole lock is refused, so no package in the project can be patched, not just the prerelease pair. The pair can be anywhere in the tree, e.g. one dependency pins
foo@1.0.0-rc.1and anotherfoo@1.0.0.Impact
scan --mode hostedreportsstatus: success,redirected: 0, exit 0, with only the warning. CI keeps installing unpatched dependencies and the job stays green.partial_failure, exit 1,vendor_bun_lockb_invalid). It still can't patch.bun installleavesbun.lockbbyte-identical (Bun considers the hash valid, andbun install --frozen-lockfilesucceeds), so the user is stuck.Repro (real Bun, local mock registry + patch API)
Controls, each with the same patched
plain-pkg@1.0.0, allredirected: 1:pre-pkg@1.0.0-beta.1+pre-pkg@0.9.0(differentmajor.minor.patch).pre-pkg@1.0.0-beta.1+pre-pkg@1.0.0-beta.2(both prerelease, socompare_prereleaseis used).pre-pkg.Expected vs actual
bun.lockbsupport") says binary locks are parsed and patched directly, andbun.lockbis in the supported-format table for hosted and vendored. A lock that Bun itself writes and accepts under--frozen-lockfileshould be patched. Theredirect_bun_lockb_invalidrefusal is meant for a stale hash, whichbun installfixes. Here it fires on a lock whose hash is valid. A refusal that fires when it shouldn't is a bug, and in hosted mode it also exits 0.Matrix (Linux, main
045d7ec)bun.lockbwriter (Bun)bun install); vendored exit 1Also reproduced with a scoped parent (
@sc/Mixed.Name_x@0.1.0-rc.0→ nestedpre-pkg@1.0.0, rootpre-pkg@1.0.0-beta.1; 1.1.45 writer). Textbun.lockis unaffected: same shape, hosted and vendored pass on 1.4.2. macOS and Windows weren't probed, but the code path is OS-independent.First bad commit: de316b4 (#358) per
git log -S "fn hash_style", which added the pre-edit hash validation. v4.0.0 passes.Suspect code
crates/socket-patch-core/src/vendor/bun_lockb.rs:1281-1287(meta_hashsort):if !legacy && !a.3.is_empty() && !b.3.is_empty() { compare_prerelease(..) } else { a.3.cmp(&b.3)… }. When exactly one side has a prerelease, the string compare puts""first. Semver precedence (and Bun'sVersion.order) needs the prerelease side to beLess. (In thelegacydialect, two numeric prerelease identifiers such asbeta.2vsbeta.10are also string-compared; I haven't verified that against a legacy writer.)crates/socket-patch-core/src/vendor/bun_lockb.rs:1148hash_style: turns the mismatch into the refusal.status: successand exit 0.No probe runs: the routine's earlier probe branches can't be deleted from the sandbox, so no new ones were pushed.