Skip to content

Hosted and vendored modes refuse every valid bun.lockb that holds a release and a prerelease of the same version (X@1.0.0 + X@1.0.0-beta.1) as "metadata hash does not match"; hosted exits 0 with nothing patched (regression since 4.0.0) #739

Description

[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.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions