Skip to content

JSTests: stress tests for the int32-boundary double->int UB fixed in #244#245

Open
robobun wants to merge 1 commit into
mainfrom
fix/dfg-parseint-result-int32-overflow-ub
Open

JSTests: stress tests for the int32-boundary double->int UB fixed in #244#245
robobun wants to merge 1 commit into
mainfrom
fix/dfg-parseint-result-int32-overflow-ub

Conversation

@robobun

@robobun robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Re-scoped to tests-only. #244 landed equivalent (and more thorough) fixes for the double→int cast UB this PR originally addressed — parseIntResult, slow_path_switch_imm, plus getUInt32, LiteralParser, String.fromCodePoint, and NumberPrototype — and oven-sh/bun picked them up via the pin bump in oven-sh/bun#31724. The source changes here are dropped; what remains is the regression coverage #244 didn't include:

  • JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js — parseInt around the int32 boundary (hex/decimal/negative, no-radix path, Infinity overflow via parseInt("f".repeat(400), 16)), looped past DFG tier-up.
  • JSTests/stress/switch-imm-scrutinee-above-int32.js — dense jump-table and sparse switches with out-of-int32 scrutinees (2^31, 2^32, Infinity), which land exactly on a case label if the scrutinee's range check is folded.

Context: newer LLVM LTO backends (LLVM 22 via rust-lld in bun's Linux release link) legally fold (double)(int)x == x round-trips into bare integrality tests when the cast is UB, which shipped as parseInt("80000000", 16) === -2147483648 after JIT warmup on bun's linux-x64 canaries (and the Map-key variant, oven-sh/bun#31080). Both tests passed on a jsc shell built with the equivalent fixes under -flto=full -fwhole-program-vtables; bun-side regression tests covering the same behavior through the full pipeline are in oven-sh/bun#31679.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 10 minutes and 53 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8f67de21-f397-4191-af97-17917c47b2b6

📥 Commits

Reviewing files that changed from the base of the PR and between 6d586e2 and 5f4bc26.

📒 Files selected for processing (2)
  • JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js
  • JSTests/stress/switch-imm-scrutinee-above-int32.js

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

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Consumer side: oven-sh/bun#31679 links the non-LTO JSC prebuilt as an immediate mitigation and adds a regression test that runs on bun's Linux release (LTO) CI lanes. Its workaround registry trips on the next WEBKIT_VERSION bump — once this PR merges and a pin includes it, the -lto variant selection gets restored.

robobun added a commit to oven-sh/bun that referenced this pull request Jun 2, 2026
… the pin

Official Linux release builds (--lto=on) link the -lto JSC prebuilt —
bitcode compiled by clang 21 whose final optimization runs in rust-lld
(rustc 1.97 nightly, LLVM 22.1.4). DFG parseIntResult() in that bitcode
does static_cast<int>(input) on out-of-range doubles, which is undefined
behavior, and LLVM 22's InstCombine folds the int32 overflow guard into
a bare integrality test. Once a parseInt call site tiers up to the DFG,
results at or above 2^31 come back as sign-wrapped int32s:

    parseInt("80000000", 16) === -2147483648   // after ~10k hot calls

and stay wrong for the life of the process. Lower tiers box through the
hardened jsNumber(double) path, so fresh processes look fine — the flip
only appears after JIT warmup, and only on LTO builds (Linux release
CI). Debug, ASAN, macOS (no LTO / native builds) are unaffected.

The source fix is oven-sh/WebKit#245. Until a WEBKIT_VERSION pin
includes it, select the non-LTO (native object) JSC tarball: clang 21
codegen keeps the guard. Bun's own C++/Rust modules stay LTO. The
workaround registry trips on the next pin bump as the removal reminder.

Also pass the artifact builders' LTO flags (-flto=full,
-fwhole-program-vtables, -fforce-emit-vtables) to local Linux WebKit
builds — with plain -flto=thin the final link fails with 'inconsistent
LTO Unit splitting' because bun-profile links -fwhole-program-vtables.

Test: test/js/bun/jsc/parseint-jit-int32-overflow.test.ts spawns bun
with a small jitPolicyScale and hammers parseInt past tier-up. Fails on
the current canary artifact in ~130 ms; passes on an --lto=on build
with this change and on non-LTO/debug builds (which never had the bug —
the miscompile needs the LTO pipeline, so only release CI lanes
exercise the failure mode).
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
5f4bc266 autobuild-preview-pr-245-5f4bc266 2026-06-02 22:45:14 UTC
29422029 autobuild-preview-pr-245-29422029 2026-06-02 03:25:54 UTC
88395aed autobuild-preview-pr-245-88395aed 2026-06-02 02:04:46 UTC

robobun added a commit to oven-sh/bun that referenced this pull request Jun 2, 2026
…ping JSC artifacts

Linking the non-LTO (native object) JSC into the -flto=full
-fwhole-program-vtables release link broke whole-program
devirtualization: vtables living in native archives are invisible to
the LTO summary, and CI caught segfaults in jsc-stress and a
GC-finalizer regression in bundler_defer on linux-aarch64 plus several
x64 shards. aarch64 also never had the parseInt fold in the first
place (fcvtzs saturates, so the round-trip guard survives there) — the
miscompile was x86-64 only.

Revert the prebuiltSuffix change and the workaround-registry entry, and
instead pin WEBKIT_VERSION to autobuild-preview-pr-245-88395aed — the
preview artifacts of the actual source fix (DFG parseIntResult() using
tryConvertToStrictInt32 instead of a UB double->int cast). The build
layout is exactly what CI has been shipping; only the one JSC function
changes. Re-pin to the merged sha once oven-sh/WebKit#245 lands.

Validated on linux-x64 --lto=on against the preview artifacts:
parseInt("80000000", 16) stays 2147483648 through 10M iterations with
forced FTL tier-up, the new regression test passes (and still fails in
~130 ms on the current canary), and bundler_defer + jsc-stress — the
suites that failed with the artifact swap — pass.
@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed a second commit after a systematic scan for the same UB class (raw static_cast<int…>(double) followed by an equality compare against the source double, across JavaScriptCore + WTF):

  • slow_path_switch_imm (LLIntSlowPaths.cpp) had the identical pattern on the switch scrutinee — switch (2**31) could wrongly match a case -2147483648: label if the optimizer folds the range check. Now uses truncateDoubleToInt32(). New stress test covers sparse-list and dense-jump-table switches with out-of-int32 scrutinees.
  • These two were the only remaining instances: normalizeMapKey (Number map keys are truncated to int32 in canary bun#31080 — broken in older canaries, already fixed by the existing hardening at 963f875), tryGetAsInt32, the DFG abstract interpreter, and the bytecode generator's switch classification all already use the hardened helpers.
  • Also extended the parseInt stress test with the Infinity overflow case (parseInt("f".repeat(400), 16)), which the folded check would also have boxed as INT32_MIN.

Both stress tests pass on a jsc shell built from this branch (-flto=full -fwhole-program-vtables).

robobun added a commit to oven-sh/bun that referenced this pull request Jun 2, 2026
…ession tests

oven-sh/WebKit#245 grew a second commit after a systematic scan for the
same UB class: slow_path_switch_imm did static_cast<int32_t>(value) on
the double scrutinee before its range check — switch (2**31) could
wrongly match a case -2147483648 label if the optimizer folds the
check. parseIntResult and switch_imm were the only two raw round-trips
left; Map key normalization (#31080) was the same disease but is
already hardened at the current pin — verified the Map repro passes on
today's canary.

Extend the regression test with Map key coverage (keys >= 2^31 and
±Infinity must survive a Map round-trip, issue #31080) and switch
scrutinee coverage, sized to run under debug/ASAN timeouts.

Validated on linux-x64 --lto=on against autobuild-preview-pr-245-29422029:
parseInt repro clean through 10M iterations with forced FTL, Map and
switch repros clean, both regression tests pass (parseInt test still
fails in ~130 ms on the current canary), bundler_defer and jsc-stress
pass.
parseInt results, and switch-immediate scrutinees, at or above 2^31
must not come back as wrapped int32s once out-of-range double->int
casts are involved. #244 fixed the undefined behavior (newer LLVM LTO
backends folded the round-trip overflow guards into bare integrality
tests, so bun's linux-x64 release builds boxed parseInt("80000000",
16) as -2147483648 after DFG tier-up); these tests pin the behavior:

- dfg-parseIntResult-should-not-wrap-above-int32.js: hex/decimal/
  negative parses around the int32 boundary plus the Infinity-overflow
  case, with and without radix.
- switch-imm-scrutinee-above-int32.js: dense jump-table and sparse
  switches with out-of-int32 scrutinees (2^31, 2^32, Infinity), which
  land exactly on a case label if the scrutinee range check is folded.

Both passed on a jsc shell built with the equivalent fixes under
-flto=full -fwhole-program-vtables.
@robobun robobun force-pushed the fix/dfg-parseint-result-int32-overflow-ub branch from 2942202 to 5f4bc26 Compare June 2, 2026 22:03
@robobun robobun changed the title DFG: fix parseIntResult wrapping results outside int32 range under LTO JSTests: stress tests for the int32-boundary double->int UB fixed in #244 Jun 2, 2026
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.

1 participant