Skip to content

semver: widen comparator counter to u32 to avoid debug overflow panic#31010

Merged
Jarred-Sumner merged 7 commits into
mainfrom
ali/semver-comparator-count-overflow
May 19, 2026
Merged

semver: widen comparator counter to u32 to avoid debug overflow panic#31010
Jarred-Sumner merged 7 commits into
mainfrom
ali/semver-comparator-count-overflow

Conversation

@alii

@alii alii commented May 18, 2026

Copy link
Copy Markdown
Member

A version range with >=256 ||/whitespace-separated comparators panicked the debug build (attempt to add with overflow, exit 133) where released Bun fails gracefully. Debug-only: Cargo's dev profile enables overflow-checks; the release profile doesn't, so release wrapped (256 -> 0) exactly like Zig's ReleaseFast.

SemverQuery::parse used let mut count: u8 = 0; with count += 1; — the 256th increment overflows. count is only ever compared == 0, never stored or cast, so a wider type is strictly more correct (it also avoids Zig's silent u8 wrap, which was a latent bug that merely didn't crash).

Widen count to u32. Regression test (Bun.semver.satisfies with 300 || comparators, run in a subprocess since the failure was an uncatchable abort) passes on the debug build and matches released Bun.


Also carried:

  • wtf-bindings.cpp: assertASSERT — identical to wtf-bindings: use WTF ASSERT() so debug builds compile #30992; the WebKit upgrade in Upgrade WebKit to 2b257999 #30705 dropped the transitive <cassert> include, so debug builds currently fail to compile on main. Needed here so the regression test can run under bun bd.
  • 30s timeout on the pre-existing failures does not cause weird memory issues test (1e5 iterations) and the new subprocess test — both exceed the 5s default under debug+ASAN.

Verification

$ bun bd test test/cli/install/semver.test.ts
 23 pass  0 fail
$ ./build/release/bun test test/cli/install/semver.test.ts
 23 pass  0 fail

With the u32 change reverted, the new test fails on debug with panic: attempt to add with overflow (src/semver/SemverQuery.rs:981) on stderr and exit 132.

@robobun

robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator
Updated 9:10 PM PT - May 18th, 2026

@robobun, your commit e21ce998a90551dc62d009ef118f8071ce0b96b2 passed in Build #55944! 🎉


🧪   To try this PR locally:

bunx bun-pr 31010

That installs a local version of the PR into your bun-31010 executable, so you can run:

bun-31010 --bun

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94c34efb-8d12-4914-9be7-44f4ec910279

📥 Commits

Reviewing files that changed from the base of the PR and between 90793cd and e21ce99.

📒 Files selected for processing (2)
  • src/jsc/bindings/wtf-bindings.cpp
  • test/cli/install/semver.test.ts

Walkthrough

This PR widens the semver parser's local token counter, adds a regression test that spawns a Bun subprocess evaluating a 300-fragment || semver range, and replaces a raw assert with the WTF ASSERT macro in a terminal helper.

Changes

Semver Comparator Count Regression Test & Parser Fix

Layer / File(s) Summary
Parser token counter widen
src/semver/SemverQuery.rs
Widened local count in parse from u8 to u32 to avoid overflow when counting parsed tokens.
Semver comparator count regression test
test/cli/install/semver.test.ts
Added bunEnv and bunExe imports, increased test timeout to 30s, and added an async regression test that spawns a Bun subprocess with a 300-fragment `
WTF assert alignment in tty helper
src/jsc/bindings/wtf-bindings.cpp
Replaced assert(tio != NULL); with ASSERT(tio != NULL); in uv__tty_make_raw helper.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: widening the semver comparator counter from u8 to u32 to prevent debug overflow panics.
Description check ✅ Passed The description covers both required template sections: it clearly explains what the PR does (the u8→u32 widening, root cause, and additional carried changes) and provides detailed verification steps with test results (23 pass/0 fail on both debug and release builds).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@alii

alii commented May 18, 2026

Copy link
Copy Markdown
Member Author

@robobun fix review comments

@robobun

robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator

✅ Review feedback addressed — diff is the u8u32 widening + regression test, with stderr surfaced before stdout so a panic backtrace appears in the failure diff, and 30s timeouts for the debug+ASAN-slow tests. Also carries the one-line assertASSERT from #30992 so bun bd compiles (pre-existing main breakage from #30705).

Verified: bun bd test test/cli/install/semver.test.ts → 23 pass / 0 fail on both debug and release. Without the fix, the new test fails on debug with panic: attempt to add with overflow on stderr.

Buildkite: all test lanes that run are green. The only reds are macOS aarch64 lanes reporting Expired (agent-pool exhaustion — jobs never started) and a one-off JSC Wasm mprotect flake on windows-11-aarch64. None touch this diff — the wtf-bindings.cpp hunk is inside #if !OS(WINDOWS) and the semver change is platform-agnostic. Head at e21ce99; ready to merge once the macOS agent pool recovers.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/cli/install/semver.test.ts`:
- Around line 751-753: Capture the subprocess stderr alongside stdout and exited
by adding proc.stderr.text() to the Promise.all call (keep variables named
stdout, stderr, exitCode) and apply the "house" pattern: after awaiting, if
(exitCode !== 0) { expect(stderr).toBe(""); } immediately before
expect(exitCode).toBe(0) so failures surface stderr content for diagnostics;
update the Promise.all usage where stdout and exitCode are currently awaited in
semver.test.ts to include stderr and adjust subsequent assertions accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed1b9ebb-ab8d-474f-9868-c591decd166b

📥 Commits

Reviewing files that changed from the base of the PR and between ef41651 and ef9b2b6.

📒 Files selected for processing (2)
  • src/semver/SemverQuery.rs
  • test/cli/install/semver.test.ts

Comment thread test/cli/install/semver.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/semver/SemverQuery.rs:725-729 — The sibling Zig implementation at src/semver/SemverQuery.zig:572 still declares var count: u8 = 0;, and that parser is the live code path for bun install (via src/install/dependency.zig:882 and src/install/lockfile/Package.zig:1045/1052) — so a dependency range with >=256 || comparators still overflows there (panic in Debug/ReleaseSafe, silent wrap to 0 in ReleaseFast). The PR description already calls the Zig wrap "a latent bug that merely didn't crash"; applying the same one-token u8u32 change at SemverQuery.zig:572 would complete the fix for parity. Non-blocking since the input is pathological and the Zig file is outside this diff.

    Extended reasoning...

    What the bug is

    This PR widens count from u8 to u32 in src/semver/SemverQuery.rs:729, but the Zig implementation it was ported from — src/semver/SemverQuery.zig:572 — still declares var count: u8 = 0; with an unchecked count += 1; at line 770. The Rust file's own trailing comment says // ported from: src/semver/SemverQuery.zig, and the new code comment added in this PR explicitly states "Zig's ReleaseFast wrapped (256 -> 0) silently" — so the author is aware the Zig sibling has the identical defect, but only the Rust port is patched.

    The Zig parser is the live bun install path

    The Zig SemverQuery.parse is not dead code. src/semver/semver.zig:9 exports pub const Query = @import("./SemverQuery.zig"), and bun install resolves dependency ranges through it:

    • src/install/dependency.zig:882Semver.Query.parse(allocator, dependency, sliced)
    • src/install/lockfile/Package.zig:1045 and :1052Semver.Query.parse(allocator, ...)

    These call sites pass an allocator argument, which matches the Zig signature, not the Rust one. Only the JS API Bun.semver.satisfies (the path the new regression test exercises) goes through the Rust port via bun_semver_jsc::SemverObject::satisfies. So a package.json dependency like "foo": "1.0.0 || 1.0.0 || ..." with 256+ comparators still hits the unfixed u8 counter.

    Why nothing prevents it

    There is no length cap on the range string and no saturating arithmetic on count. In Zig, count += 1 on a u8 at 255:

    • Debug / ReleaseSafe: panics on integer overflow (the same abort class this PR set out to fix, just via a different entry point).
    • ReleaseFast (production): wraps 256 → 0 silently.

    Step-by-step proof of the semantic divergence on wrap

    Consider a range of 256 1.0.0 comparators joined by ||, followed by || >2.0.0 as the 257th:

    1. After 256 non-skipped rounds, count wraps from 255 to 0.
    2. The || token sets is_or = true, token.tag = .none, skip_round = true.
    3. The 257th comparator >2.0.0 sets token.tag = .gt (not .version, not .none).
    4. In the dispatch chain, hyphenate is false, count == 0 and token.tag == .version is false (tag is .gt), so it falls into the next branch: else if (count == 0)list.andRange(...).
    5. With a non-wrapped counter, count would be 256 and the same token would instead hit else if (is_or)list.orRange(...).

    So the wrap re-enters the first-comparator special case and turns an OR into an AND, changing which versions satisfy the range. This is exactly what the PR description calls "a latent bug that merely didn't crash".

    Impact and fix

    Impact is low in practice — 256+ comparators in a real package.json is pathological — but it is the precise sibling of the bug being fixed here, on the code path (bun install) where untrusted package manifests are actually parsed. The fix is the same one-token widening:

    // src/semver/SemverQuery.zig:572
    -    var count: u8 = 0;
    +    var count: u32 = 0;

    The new regression test only spawns Bun.semver.satisfies (Rust path), so it does not cover this. Filing as a non-blocking nit for parity since the Zig file is outside this diff and the trigger is implausible, but it's a one-line change that completes the PR's stated goal.

Comment thread test/cli/install/semver.test.ts Outdated
Comment thread src/semver/SemverQuery.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-token u8u32 widening on a local counter that's only ever compared to zero; strictly safer than before. (My Zig-side FYI is pre-existing and non-blocking.)

Extended reasoning...

Overview

Single-character production change: let mut count: u8 = 0;let mut count: u32 = 0; at src/semver/SemverQuery.rs:725. count is a function-local that's only incremented and compared == 0 — never stored, cast, or returned — so widening cannot change behavior except to remove the debug-build overflow panic at 256 comparators. The accompanying regression test in test/cli/install/semver.test.ts follows the house subprocess pattern (bunEnv/bunExe, await using, stderr captured and asserted on failure, exitCode asserted last) and all earlier review nits (mine and CodeRabbit's) were applied in 90793cd.

Security risks

None. No user-controlled data flows anywhere new; this only widens an internal loop counter. No auth, crypto, FS, or network surface touched.

Level of scrutiny

Low. This is a mechanical type-widen on a local variable whose sole observable use is a zero-check, plus a self-contained regression test. The one CI failure on the build dashboard is an infrastructure agent-provisioning error ("Failed to create agent for aarch64 - build-cpp"), not a build or test failure attributable to this diff.

Other factors

I previously left an FYI inline comment noting the parallel u8 counter still exists in src/semver/SemverQuery.zig (the file this was ported from, still used by bun install). That was explicitly flagged as pre-existing and non-blocking — it's a reasonable follow-up but doesn't gate this PR, which strictly improves the Rust path without regressing anything else.

- wtf-bindings.cpp: assert -> ASSERT (same as #30992; #30705 dropped
  the transitive <cassert> include so debug builds fail to compile)
- semver.test.ts: 30s timeout on the 1e5-iteration memory-churn test
  and the subprocess regression test; both exceed the 5s default under
  debug+ASAN on constrained runners

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-token type widening plus regression test; all prior review nits addressed.

Extended reasoning...

Overview

Three files touched: src/semver/SemverQuery.rs widens a local count from u8 to u32 (the variable is only ever compared == 0, never stored/cast, so this is strictly a correctness improvement with no behavioral side-effects beyond removing the overflow). src/jsc/bindings/wtf-bindings.cpp swaps libc assert() for the project's WTF ASSERT() macro on one line — a mechanical convention fix to unblock the debug build, consistent with the existing ASSERT at line ~244 of the same file. test/cli/install/semver.test.ts adds the harness imports, bumps one existing slow-loop test's timeout to 30s, and adds a subprocess regression test for the 300-comparator range.

Security risks

None. The semver parser change widens a local integer type with no effect on memory layout, allocation, or external input handling beyond not panicking. The assertASSERT swap is a no-op in release and a stricter check in debug. The test only spawns the local bunExe() with a fixed literal payload.

Level of scrutiny

Low. The Rust change is one token; count's only readers are two == 0 checks, so widening cannot change semantics for inputs <256 comparators and only fixes the pathological ≥256 case. The C++ change is a macro-name swap on a null-check that's already guarded by the sole caller. The test follows the repo's documented spawn-test pattern (stderr captured, asserted on non-zero exit, exitCode last).

Other factors

I previously left two inline comments on this PR: a stderr-capture nit (applied in 90793cd/b5e51db — the diff now reads proc.stderr.text() and guards if (exitCode !== 0) expect(stderr).toBe("")) and an FYI about the sibling SemverQuery.zig (author clarified it's a non-compiled porting reference per src/CLAUDE.md, which is a satisfactory answer). CodeRabbit's identical stderr nit is also resolved. All inline threads are marked resolved, no bugs were found by the bug-hunting system, and Buildkite is building the latest commit. Nothing remains outstanding.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-token type widening with a regression test; the remaining inline note is a non-blocking diagnostic nit.

Extended reasoning...

Overview

Three changes: (1) src/semver/SemverQuery.rs widens a local count from u8 to u32 — the variable is only ever compared == 0 and never stored/cast, so this is strictly more correct and fixes a debug-build overflow panic at ≥256 comparators; (2) src/jsc/bindings/wtf-bindings.cpp swaps assertASSERT, a one-token carry from #30992 to unblock the debug build after the WebKit upgrade; (3) test/cli/install/semver.test.ts adds a subprocess regression test and bumps two timeouts to 30s for debug+ASAN headroom.

Security risks

None. No user-facing input handling changes, no auth/crypto/permissions. The semver parser already accepted arbitrary-length input; widening a local counter does not change the attack surface.

Level of scrutiny

Low. The Rust change is a single-token type widening on a local with no downstream type dependencies. The C++ change is a macro rename identical to the one in #30992. The test follows the documented bunExe()/bunEnv spawn pattern from test/CLAUDE.md.

Other factors

All prior inline review comments (mine and CodeRabbit's) are resolved — stderr is now captured, comments removed per src/CLAUDE.md, and the .zig sibling was correctly left untouched per the repo's reference-only convention. The one remaining inline nit (assertion order for better failure diffs) is explicitly diagnostic-quality only; the test is functionally correct either way, and the current order matches an established house pattern (PR #29322 learning). The Buildkite failures on 0a802c5 are unrelated HiveArrayFallback deprecation errors in files this PR does not touch, plus a known jsc-stress Windows-aarch64 flake the author re-rolled.

Comment thread test/cli/install/semver.test.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — one-token type widening with a clean regression test; all prior nits addressed.

Extended reasoning...

Overview

Three files touched: (1) src/semver/SemverQuery.rs widens a local count variable from u8 to u32 — the variable is only ever compared == 0 and incremented, never stored or cast, so widening is strictly safer and fixes a debug-build overflow panic at the 256th comparator; (2) src/jsc/bindings/wtf-bindings.cpp swaps lowercase assert for WTF's ASSERT macro on one line inside a non-Windows TTY helper, carried from #30992 to unblock the debug build after #30705 dropped the transitive <cassert> include; (3) test/cli/install/semver.test.ts adds a subprocess regression test for the 300-comparator case plus a 30s timeout on the pre-existing 1e5-iteration test for debug+ASAN headroom.

Security risks

None. The semver parser change widens a local integer used purely as a "first token seen" sentinel — no narrowing casts, no externally observable size, no allocation sizing derived from it. The assertASSERT swap is in debug-only TTY termios setup and is semantically equivalent (both compile out in release). The test spawns bunExe() with a fixed inline -e script under bunEnv; no untrusted input flows into the command.

Level of scrutiny

Low. The Rust hunk is a one-token type change to a local with trivially provable correctness (every use site — count == 0 twice and count += 1 once — accepts u32 identically). The C++ hunk is a mechanical macro rename already landing separately in #30992. The test follows the repo's documented spawn-and-assert pattern from test/CLAUDE.md after three rounds of nit feedback (stderr capture, ordering, ASAN happy-path tolerance), all of which are now resolved in the final diff.

Other factors

All three of my prior inline comments and CodeRabbit's nit are marked resolved and verified against the current diff: stderr is read via Promise.all, the if (exitCode !== 0) expect(stderr).toBe("") guard now precedes the stdout assertion so panic backtraces surface, and the Zig sibling (SemverQuery.zig) was correctly identified as a non-compiled porting reference per src/CLAUDE.md. CI is green except for an unrelated Windows aarch64 JSC stress flake (mprotect failed) that the #if !OS(WINDOWS) hunk cannot affect. No outstanding human reviewer comments.

@Jarred-Sumner Jarred-Sumner merged commit 2ff6df8 into main May 19, 2026
75 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the ali/semver-comparator-count-overflow branch May 19, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants