From 5f4bc266f9bec81291b6a94899642b34db91a76c Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 2 Jun 2026 22:03:09 +0000 Subject: [PATCH] JSTests: stress tests for int32-boundary UB fixed in #244 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. --- ...seIntResult-should-not-wrap-above-int32.js | 38 +++++++++++++ .../switch-imm-scrutinee-above-int32.js | 56 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js create mode 100644 JSTests/stress/switch-imm-scrutinee-above-int32.js diff --git a/JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js b/JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js new file mode 100644 index 0000000000000..13e9f3dbfd67b --- /dev/null +++ b/JSTests/stress/dfg-parseIntResult-should-not-wrap-above-int32.js @@ -0,0 +1,38 @@ +// parseInt results outside int32 range must be boxed as doubles, not +// sign-wrapped int32s, once the call site tiers up to the DFG/FTL. +// parseIntResult() used to do static_cast(input) before the round-trip +// comparison deciding int32 vs double boxing; the cast is undefined behavior +// for out-of-range inputs, which let LTO fold the overflow guard away. + +function hex(s) { + return parseInt(s, 16); +} +noInline(hex); + +function dec(s) { + return parseInt(s); +} +noInline(dec); + +const hugeHex = "f".repeat(400); + +for (let i = 0; i < testLoopCount; ++i) { + let v = hex("80000000"); + if (v !== 2147483648) + throw "FAILED: parseInt('80000000', 16) === " + v + " at iteration " + i; + v = hex("ffffffff"); + if (v !== 4294967295) + throw "FAILED: parseInt('ffffffff', 16) === " + v + " at iteration " + i; + v = hex("7fffffff"); + if (v !== 2147483647) + throw "FAILED: parseInt('7fffffff', 16) === " + v + " at iteration " + i; + v = hex("-80000001"); + if (v !== -2147483649) + throw "FAILED: parseInt('-80000001', 16) === " + v + " at iteration " + i; + v = dec("2147483648"); + if (v !== 2147483648) + throw "FAILED: parseInt('2147483648') === " + v + " at iteration " + i; + v = hex(hugeHex); // overflows to Infinity + if (v !== Infinity) + throw "FAILED: parseInt('f'.repeat(400), 16) === " + v + " at iteration " + i; +} diff --git a/JSTests/stress/switch-imm-scrutinee-above-int32.js b/JSTests/stress/switch-imm-scrutinee-above-int32.js new file mode 100644 index 0000000000000..2ba88284ee4eb --- /dev/null +++ b/JSTests/stress/switch-imm-scrutinee-above-int32.js @@ -0,0 +1,56 @@ +// A double scrutinee outside int32 range must never match an immediate +// switch case. slow_path_switch_imm used to do static_cast(value) +// — undefined behavior for out-of-range doubles — before the range check; +// a UB-exploiting optimizer could then fold the check away and match the +// case whose label equals the wrapped/saturated conversion result. + +function bigSwitch(x) { + switch (x) { + case -2147483648: + return "int32-min"; + case 2147483647: + return "int32-max"; + case 0: + return "zero"; + default: + return "default"; + } +} +noInline(bigSwitch); + +function denseSwitch(x) { + // Dense enough for a jump table (SwitchType::Immediate). + switch (x) { + case -2147483648: return 0; + case -2147483647: return 1; + case -2147483646: return 2; + case -2147483645: return 3; + case -2147483644: return 4; + case -2147483643: return 5; + case -2147483642: return 6; + case -2147483641: return 7; + default: return -1; + } +} +noInline(denseSwitch); + +for (let i = 0; i < testLoopCount; ++i) { + let r = bigSwitch(2147483648); // 2^31, truncates to INT32_MIN on x86 + if (r !== "default") + throw "FAILED: bigSwitch(2^31) === " + r + " at iteration " + i; + r = bigSwitch(4294967296); // 2^32, truncates to 0 via modular wrap + if (r !== "default") + throw "FAILED: bigSwitch(2^32) === " + r + " at iteration " + i; + r = bigSwitch(Infinity); + if (r !== "default") + throw "FAILED: bigSwitch(Infinity) === " + r + " at iteration " + i; + r = bigSwitch(-2147483648); + if (r !== "int32-min") + throw "FAILED: bigSwitch(-2^31) === " + r + " at iteration " + i; + r = denseSwitch(2147483648); + if (r !== -1) + throw "FAILED: denseSwitch(2^31) === " + r + " at iteration " + i; + r = denseSwitch(-2147483648); + if (r !== 0) + throw "FAILED: denseSwitch(-2^31) === " + r + " at iteration " + i; +}