CVE-2026-11075
Overview
Background
- Additive safe integer optimization
- a TurboFan speculative optimization that, given feedback that operands are “additive safe integers,” lowers
+/-to fast int64 arithmetic without full safe-integer checks. - `kMaxAdditiveSafeInteger` / `kMinAdditiveSafeInteger`
- bounds (formerly
±2^52) used both as the type range and as the constant subtracted in the range test(value - kMin) >> bitlen == 0. - `OperationTyper::SpeculativeAdditiveSafeIntegerAdd/Subtract`
- the typer methods that compute the result
Typeof a speculative additive-safe-integer add/subtract, feeding later optimizations (e.g. truncation/bounds decisions). - `kAdditiveSafeInteger` vs `kAdditiveSafeIntegerFeedback` type caches
- the internal
Typeused to decide whether a value fits the safe range and can be truncated without a checked conversion.
Root Cause Analysis
The optimization used feedback bounds of ±2^52 (kMaxAdditiveSafeInteger = 2^52-1, kMinAdditiveSafeInteger = -2^52), but two such minimum values added together (-2^52 + -2^52 = -2^53) reach exactly kMinSafeInteger, leaving no margin, and the old typer in operation-typer.cc further intersected the add/subtract result with cache_->kAdditiveSafeInteger, asserting a narrower result type than the arithmetic could actually produce. That over-narrow type let downstream code (in representation-change.cc) treat outputs as fitting kAdditiveSafeInteger and select unchecked TruncateFloat64ToWord32/ChangeFloat64ToInt64 paths, so a value that overflowed the assumed range was used without deopt — a type/range mistake the CL description calls “range mistakes.”
The fix removes the intersection so SpeculativeAdditiveSafeIntegerAdd/Subtract simply return SpeculativeNumberAdd/Subtract, and introduces tighter feedback bounds kMaxAdditiveSafeIntegerFeedback = 2^51-1 / kMinAdditiveSafeIntegerFeedback = -2^51 with kAdditiveSafeIntegerFeedbackBitLength = 52, so min + min = -2^52 stays safely inside range. The range-test call sites in builtins-number-tsa.cc and code-stub-assembler.cc and the truncation/deopt checks in representation-change.cc are all repointed to the *Feedback constants and cache_->kAdditiveSafeIntegerFeedback, and the constant-folding path now deopts (InsertUnconditionalDeopt) when a constant falls outside the tighter feedback range. The updated test/mjsunit/additive-safe-int-feedback.js confirms overflow now deopts instead of silently continuing.
±2^52, whose min+min saturates the safe-integer range) combined with a typer that intersected the result with kAdditiveSafeInteger and thereby claimed a narrower type than the operation could produce; the fix drops the intersection and tightens the feedback bounds to ±2^51, guaranteeing headroom so two minima can no longer overflow.Attack Path
- Train additive-safe-integer feedback
Script repeatedly runs an add/subtract on integers near the old
±2^52bounds so TurboFan recordsAdditiveSafeIntegerfeedback and optimizes the function. - Feed boundary operands
The optimized code is called with values whose sum reaches/exceeds the assumed range (e.g. two near-minimum operands summing to
-2^53), which the old bounds and the intersecting typer accepted without deopt. - Obtain a mistyped result
The typer’s intersection with
kAdditiveSafeIntegernarrows the result’sTypebelow its true range, andrepresentation-change.ccselects the unchecked truncation/ChangeFloat64ToInt64path for that value. - Downstream range misuse The incorrectly narrowed value is consumed by a later range-dependent optimization; with the true value outside the assumed bounds this produces the out-of-bounds read the advisory reports (specific sink/index not shown in the diff).
Impact Assessment
OOB / “Out of bounds read in V8”). The precondition is the ability to run and optimize script exercising the additive-safe-integer feedback path (gated by --additive-safe-int-feedback) with boundary operands. Escalation to a broader read/write primitive or code execution is not demonstrated by the patch and is (inferred).Files Changed
src/builtins/builtins-number-tsa.ccsrc/codegen/code-stub-assembler.ccsrc/common/globals.hsrc/compiler/operation-typer.ccsrc/compiler/representation-change.cc
Audit Directions
- Bound used as both type and test constantReview optimizations where one constant defines both a value’s
Typerange and the subtract-and-shift range check; verify the sum of two extreme operands still fits the safe range. - Typer intersections that narrow resultsAudit
OperationTypermethods thatType::Intersecta speculative result with an assumed cache type, since this can assert a narrower range than the operation actually produces. - Off-by-a-power-of-two marginsCheck other speculative integer optimizations (safe-integer, small-integer) for missing headroom when combining two boundary values, and confirm out-of-range inputs deopt rather than truncate unchecked.
Patch
From 330a25ad001ee1ac3b7a2176a1f175cee2592a5f Mon Sep 17 00:00:00 2001
From: Victor Gomes <victorgomes@chromium.org>
Date: Wed, 15 Apr 2026 10:29:16 +0200
Subject: [PATCH] [turbofan] Fix and simplify additive safe integer optimization
This CL simplifies the speculative optimization for additive safe
integers and introduces a safer margin for feedback ranges to prevent
overflow.
* Simplify optimization logic: Always perform the operation when the feedback is `AdditiveSafeInteger`, rather than relying on statically proving one side. This simplifies the operation typer and avoids range mistakes.
* Prevent bounds overflow: Introduce tighter feedback bounds (`kMaxAdditiveSafeIntegerFeedback` and `kMinAdditiveSafeIntegerFeedback` at ±2^51). This safer margin ensures that adding two minimum values together will no longer exceed the safe integer range.
* Update tests: Adjust `test/mjsunit/additive-safe-int-feedback.js` to reflect the newly introduced ranges.
Fixed: 499659070
Change-Id: Ic16dbb644b310bed24b169fed90c6637cac10daa
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7748026
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106492}
---
diff --git a/src/builtins/builtins-number-tsa.cc b/src/builtins/builtins-number-tsa.cc
index b9c5daf..8bf79e8 100644
--- a/src/builtins/builtins-number-tsa.cc
+++ b/src/builtins/builtins-number-tsa.cc
@@ -94,11 +94,11 @@
BIND(if_int);
{
- // Check if AdditiveSafeInteger: (value - kMinAdditiveSafeInteger) >> 53
- // == 0
- V<Word64> shifted_value =
- Word64ShiftRightLogical(Word64Sub(value_i64, kMinAdditiveSafeInteger),
- kAdditiveSafeIntegerBitLength);
+ // Check if AdditiveSafeIntegerFeedback: (value -
+ // kMinAdditiveSafeIntegerFeedback) >> 51 == 0
+ V<Word64> shifted_value = Word64ShiftRightLogical(
+ Word64Sub(value_i64, kMinAdditiveSafeIntegerFeedback),
+ kAdditiveSafeIntegerFeedbackBitLength);
GOTO_IF_NOT(Word64Equal(shifted_value, 0), if_fail);
return value_i64;
}
diff --git a/src/codegen/code-stub-assembler.cc b/src/codegen/code-stub-assembler.cc
index 30f47be..e95cd98 100644
--- a/src/codegen/code-stub-assembler.cc
+++ b/src/codegen/code-stub-assembler.cc
@@ -7186,10 +7186,11 @@
if_failed);
BIND(&if_int64);
- // Check if AdditiveSafeInteger: (value - kMinAdditiveSafeInteger) >> 53 == 0
- TNode<Int64T> shifted_value =
- Word64Shr(Int64Sub(value_int64, Int64Constant(kMinAdditiveSafeInteger)),
- Uint64Constant(kAdditiveSafeIntegerBitLength));
+ // Check if AdditiveSafeIntegerFeedback: (value -
+ // kMinAdditiveSafeIntegerFeedback) >> 51 == 0
+ TNode<Int64T> shifted_value = Word64Shr(
+ Int64Sub(value_int64, Int64Constant(kMinAdditiveSafeIntegerFeedback)),
+ Uint64Constant(kAdditiveSafeIntegerFeedbackBitLength));
GotoIfNot(Word64Equal(shifted_value, Int64Constant(0)), if_failed);
return UncheckedCast<AdditiveSafeIntegerT>(value_int64);
}
diff --git a/src/common/globals.h b/src/common/globals.h
index a795221..e3ded06 100644
--- a/src/common/globals.h
+++ b/src/common/globals.h
@@ -2133,29 +2133,30 @@
#endif // V8_ENABLE_UNDEFINED_DOUBLE
// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER
-constexpr uint64_t kMaxSafeIntegerUint64 = 9007199254740991; // 2^53-1
-static_assert(kMaxSafeIntegerUint64 == (uint64_t{1} << 53) - 1);
+constexpr uint64_t kMaxSafeIntegerUint64 = (uint64_t{1} << 53) - 1; // 2^53-1
constexpr double kMaxSafeInteger = static_cast<double>(kMaxSafeIntegerUint64);
// ES6 section 21.1.2.8 Number.MIN_SAFE_INTEGER
constexpr double kMinSafeInteger = -kMaxSafeInteger;
constexpr double kMaxUInt32Double = double{kMaxUInt32};
-constexpr int64_t kMaxAdditiveSafeInteger = 4503599627370495; // 2^52 - 1
-static_assert(kMaxAdditiveSafeInteger == (int64_t{1} << 52) - 1);
-constexpr int64_t kMinAdditiveSafeInteger = -4503599627370496; // - 2^52
-static_assert(kMinAdditiveSafeInteger == -(int64_t{1} << 52));
-constexpr int kAdditiveSafeIntegerBitLength = 53;
-// Number of bits to shift left before addition to detect potential overflow.
-constexpr int kAdditiveSafeIntegerShift = 64 - kAdditiveSafeIntegerBitLength;
-
+constexpr int64_t kMaxAdditiveSafeInteger = (int64_t{1} << 52) - 1; // 2^52 - 1
+constexpr int64_t kMinAdditiveSafeInteger =
+ -(int64_t{1} << 52) + 1; // - (2^52 - 1)
static_assert(kMaxAdditiveSafeInteger + kMaxAdditiveSafeInteger <=
kMaxSafeInteger);
-// kMinAdditiveSafeInteger + kMinAdditiveSafeInteger would overflow the integer
-// safe addition.
-static_assert(kMinAdditiveSafeInteger + (kMinAdditiveSafeInteger + 1) >=
+static_assert(kMinAdditiveSafeInteger + kMinAdditiveSafeInteger >=
kMinSafeInteger);
+constexpr int64_t kMaxAdditiveSafeIntegerFeedback =
+ (int64_t{1} << 51) - 1; // 2^51 - 1
+constexpr int64_t kMinAdditiveSafeIntegerFeedback =
+ -(int64_t{1} << 51); // - 2^51
+constexpr int kAdditiveSafeIntegerFeedbackBitLength = 52;
+// Number of bits to shift left before addition to detect potential overflow.
+constexpr int kAdditiveSafeIntegerFeedbackShift =
+ 64 - kAdditiveSafeIntegerFeedbackBitLength;
+
// The order of this enum has to be kept in sync with the predicates below.
enum class VariableMode : uint8_t {
// User declared variables:
diff --git a/src/compiler/operation-typer.cc b/src/compiler/operation-typer.cc
index d28d90c..d297650 100644
--- a/src/compiler/operation-typer.cc
+++ b/src/compiler/operation-typer.cc
@@ -719,22 +719,12 @@
}
Type OperationTyper::SpeculativeAdditiveSafeIntegerAdd(Type lhs, Type rhs) {
- Type result = SpeculativeNumberAdd(lhs, rhs);
- if (lhs.Is(cache_->kAdditiveSafeInteger) ||
- rhs.Is(cache_->kAdditiveSafeInteger)) {
- return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone());
- }
- return result;
+ return SpeculativeNumberAdd(lhs, rhs);
}
Type OperationTyper::SpeculativeAdditiveSafeIntegerSubtract(Type lhs,
Type rhs) {
- Type result = SpeculativeNumberSubtract(lhs, rhs);
- if (lhs.Is(cache_->kAdditiveSafeInteger) ||
- rhs.Is(cache_->kAdditiveSafeInteger)) {
- return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone());
- }
- return result;
+ return SpeculativeNumberSubtract(lhs, rhs);
}
Type OperationTyper::SpeculativeSmallIntegerAdd(Type lhs, Type rhs) {
diff --git a/src/compiler/representation-change.cc b/src/compiler/representation-change.cc
index 23ad745..cfc1cf3 100644
--- a/src/compiler/representation-change.cc
+++ b/src/compiler/representation-change.cc
@@ -1011,7 +1011,7 @@
op = machine()->ChangeFloat64ToUint32();
} else if (use_info.truncation().IsUsedAsWord32()) {
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
- if (output_type.Is(cache_->kAdditiveSafeInteger)) {
+ if (output_type.Is(cache_->kAdditiveSafeIntegerFeedback)) {
op = machine()->TruncateFloat64ToWord32();
} else {
op = simplified()->CheckedFloat64ToAdditiveSafeInteger(
@@ -1045,7 +1045,7 @@
op = machine()->ChangeFloat64ToUint32();
} else if (use_info.truncation().IsUsedAsWord32()) {
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
- if (output_type.Is(cache_->kAdditiveSafeInteger)) {
+ if (output_type.Is(cache_->kAdditiveSafeIntegerFeedback)) {
op = machine()->TruncateFloat64ToWord32();
} else {
op = simplified()->CheckedFloat64ToAdditiveSafeInteger(
@@ -1086,7 +1086,7 @@
CheckTaggedInputMode::kAdditiveSafeInteger, use_info.feedback());
} else if (use_info.truncation().IsUsedAsWord32()) {
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
- if (output_type.Is(cache_->kAdditiveSafeInteger)) {
+ if (output_type.Is(cache_->kAdditiveSafeIntegerFeedback)) {
op = simplified()->TruncateNumberOrOddballToWord32();
} else {
op = simplified()->CheckedTruncateTaggedToWord32(
@@ -1281,8 +1281,8 @@
int64_t const iv = static_cast<int64_t>(fv);
if (static_cast<double>(iv) == fv) {
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
- if (iv < kMinAdditiveSafeInteger ||
- kMaxAdditiveSafeInteger < iv ||
+ if (iv < kMinAdditiveSafeIntegerFeedback ||
+ kMaxAdditiveSafeIntegerFeedback < iv ||
(iv == 0 && std::signbit(fv))) {
Node* unreachable = InsertUnconditionalDeopt(
use_node, DeoptimizeReason::kNotAdditiveSafeInteger,
@@ -1372,7 +1372,7 @@
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
// float32 -> float64 -> int64
node = InsertChangeFloat32ToFloat64(node);
- if (output_type.Is(cache_->kAdditiveSafeInteger)) {
+ if (output_type.Is(cache_->kAdditiveSafeIntegerFeedback)) {
op = machine()->ChangeFloat64ToInt64();
} else {
op = simplified()->CheckedFloat64ToAdditiveSafeInteger(
@@ -1406,7 +1406,7 @@
}
} else if (output_rep == MachineRepresentation::kFloat64) {
if (use_info.type_check() == TypeCheckKind::kAdditiveSafeInteger) {
- if (output_type.Is(cache_->kAdditiveSafeInteger)) {
+ if (output_type.Is(cache_->kAdditiveSafeIntegerFeedback)) {
op = machine()->ChangeFloat64ToInt64();
} else {
op = simplified()->CheckedFloat64ToAdditiveSafeInteger(
@@ -1454,7 +1454,7 @@
Regression Test / PoC
diff --git a/test/mjsunit/additive-safe-int-feedback.js b/test/mjsunit/additive-safe-int-feedback.js
index 7530b9f..aabdb83 100644
--- a/test/mjsunit/additive-safe-int-feedback.js
+++ b/test/mjsunit/additive-safe-int-feedback.js
@@ -5,8 +5,8 @@
// Flags: --allow-natives-syntax --additive-safe-int-feedback
// Flags: --turbofan
-const maxAdditiveSafeInteger = 4503599627370495; // 2^52 - 1
-const minAdditiveSafeInteger = - 4503599627370496; // - 2^52
+const maxAdditiveSafeInteger = 2251799813685247; // 2^52 - 1
+const minAdditiveSafeInteger = - 2251799813685248; // - 2^51
// If one of the inputs is a constant in the additive safe range,
// we can use AdditiveSafeInteger.
@@ -89,8 +89,15 @@
assertEquals(1231234567891, foo(1231234567890, 1));
assertOptimized(foo);
- // We don't deopt in overflow.
+ // We deopt in overflow.
assertEquals(maxAdditiveSafeInteger + 1, foo(maxAdditiveSafeInteger, 1));
+ assertUnoptimized(foo);
+
+ // Re-optimize to continue test.
+ %PrepareFunctionForOptimization(foo);
+ assertEquals(1231234567891, foo(1231234567890, 1));
+ %OptimizeFunctionOnNextCall(foo);
+ assertEquals(1231234567891, foo(1231234567890, 1));
assertOptimized(foo);
// Don't deopt with doubles.
@@ -300,6 +307,13 @@
// And we cannot deopt by overflowing the first one.
assertEquals(maxAdditiveSafeInteger + 2, foo(maxAdditiveSafeInteger, 1));
+ assertUnoptimized(foo);
+
+ // Re-optimize to continue test.
+ %PrepareFunctionForOptimization(foo);
+ assertEquals(1231234567892, foo(1231234567890, 1));
+ %OptimizeFunctionOnNextCall(foo);
+ assertEquals(1231234567892, foo(1231234567890, 1));
assertOptimized(foo);
// Don't deopt with doubles.
Original Bug Report
V8: JIT Miscompilation via Incorrect Type Narrowing in TurboFan SpeculativeAdditiveSafeIntegerAdd
VULNERABILITY DETAILS
Summary
A type narrowing bug in TurboFan’s OperationTyper::SpeculativeAdditiveSafeIntegerAdd causes the compiler to compute an incorrectly narrow result type when the mathematical sum can exceed the kAdditiveSafeInteger range [-2^52, 2^52-1]. The function intersects the result type with kAdditiveSafeInteger when either input is in range (OR condition), but when both inputs are in range and their sum exceeds the range, the intersection clips the result to just 2 values instead of 2^32. After algebraic simplification of (x + C) - C -> x, the incorrect type Range(0, 1) persists on the result node, causing TypeNarrowingReducer to fold comparisons and ConstantFoldingReducer to eliminate reachable branches, producing wrong JavaScript results. The bug is reproducible on all 64-bit architectures with default V8 flags and no special command-line options.
Overview
The root cause is in OperationTyper::SpeculativeAdditiveSafeIntegerAdd (src/compiler/operation-typer.cc:721-728). The function intersects the addition result type with kAdditiveSafeInteger (range [-2^52, 2^52-1]) when either input type is a subset of kAdditiveSafeInteger (OR condition on line 723). This creates an incorrectly narrow result type when the mathematical sum can exceed the kAdditiveSafeInteger bounds, even though both inputs individually are within range.
When a subsequent subtraction of the same constant creates an algebraic identity (x + C) - C, the MachineOperatorReducer eliminates both operations and their runtime overflow checks. The incorrect type Range(0, 1) – computed from the clipped intermediate – persists on the result node, causing TypeNarrowingReducer to fold comparisons and ConstantFoldingReducer to eliminate branches, producing wrong JavaScript results.
Detail
The vulnerable code in OperationTyper::SpeculativeAdditiveSafeIntegerAdd:
// src/compiler/operation-typer.cc:721-728
Type OperationTyper::SpeculativeAdditiveSafeIntegerAdd(Type lhs, Type rhs) {
Type result = SpeculativeNumberAdd(lhs, rhs);
if (lhs.Is(cache_->kAdditiveSafeInteger) || // <-- OR condition: only ONE input needs to be in range
rhs.Is(cache_->kAdditiveSafeInteger)) {
return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone()); // <-- clips result to [-2^52, 2^52-1]
}
return result;
}
And the corresponding subtraction:
// src/compiler/operation-typer.cc:730-738
Type OperationTyper::SpeculativeAdditiveSafeIntegerSubtract(Type lhs,
Type rhs) {
Type result = SpeculativeNumberSubtract(lhs, rhs);
if (lhs.Is(cache_->kAdditiveSafeInteger) ||
rhs.Is(cache_->kAdditiveSafeInteger)) {
return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone());
}
return result;
}
How the miscompilation occurs (in pipeline execution order):
-
Incorrect Type Computation (Typer phase): For
ix(x >>> 0, Uint32 range[0, 2^32-1]) – which is a subset ofkAdditiveSafeInteger– adding constantBASE = 2^52 - 2producesSpeculativeNumberAddresultRange(4503599627370494, 4503603922337789). The OR condition triggers (both inputs are individually withinkAdditiveSafeInteger), and intersection withkAdditiveSafeIntegerclips this toRange(4503599627370494, 4503599627370495)– just 2 possible values instead of 2^32. -
Type Propagation (Typer phase): The subtraction
sum - BASEis typed asRange(0, 1)bySpeculativeAdditiveSafeIntegerSubtract, because the subtraction ranger computesRange(4503599627370494, 4503599627370495) - Range(4503599627370494, 4503599627370494) = Range(0, 1). The actual mathematical range should be[0, 2^32-1]. -
Algebraic Simplification (LateOptimization phase): The
MachineOperatorReducer(src/compiler/machine-operator-reducer.cc:1179-1196) converts(x + C) - Cintox + C + (-C), then constant-foldsC + (-C) = 0viaReduceInt64Add(src/compiler/machine-operator-reducer.cc:1147-1155), resulting inx + 0 = x. This eliminates both the addition and subtraction operations along with theirCheckedAdditiveSafeIntegerAdd/Subruntime overflow checks. The incorrect typeRange(0, 1)from step 2 persists on the result node even though the actual runtime value is the unmodifiedx >>> 0. -
Comparison Folding (TypedOptimizations phase):
TypeNarrowingReducer(src/compiler/type-narrowing-reducer.cc:33-34) foldsidx < 2tosingleton_truebecauseRange(0, 1).Max() = 1 < 2:// src/compiler/type-narrowing-reducer.cc:33-34 if (left_type.Max() < right_type.Min()) { new_type = op_typer_.singleton_true(); } -
Branch Elimination (TypedOptimizations phase):
ConstantFoldingReducer(src/compiler/constant-folding-reducer.cc:31-32) detects the singleton type and replaces the comparison with a constant, causing branch elimination to remove the reachable false branch:// src/compiler/constant-folding-reducer.cc:31-32 } else if (type.Is(Type::PlainNumber()) && type.Min() == type.Max()) { result = jsgraph->ConstantNoHole(type.Min());
Trigger Conditions
- Target must be a 64-bit architecture (
additive_safe_int_feedbackflag istrueby default only on 64-bit;src/flags/flag-definitions.h:860-877) - The addition operand must be within
kAdditiveSafeIntegerrange (e.g.,x >>> 0produces Uint32, a subset) - The constant added must be close to
kMaxAdditiveSafeInteger(2^52 - 1) so that the sum can exceed the range - The addition must be paired with a subtraction of the same constant, enabling algebraic simplification
- The result of the add-sub pair must be used in a comparison that can be folded by TypeNarrowingReducer
- The function must be compiled by TurboFan (requires sufficient warmup or
%OptimizeFunctionOnNextCall)
Version
Reproduced Version
mainbranch latest commit (2026/04/06):01333f9ce95- V8 14.9.0
Bisect
The commit 0a1fae9e77c6d8e85d8197b4f4396815ec9194b9 introduces this bug.
commit 0a1fae9e77c6d8e85d8197b4f4396815ec9194b9
Author: Victor Gomes <victorgomes@chromium.org>
Date: Tue Feb 11 14:13:21 2025 +0100
[turbofan] Use AdditiveSafeInt feedback for faster int add/sub
Add Int53 addition feedback and use it during simplified lowering
to optimize integer addition and subtraction. When one of the inputs
is known to be within the Int53 range (since the check is relatively
expensive), we can use a shifted integer addition instead of floating
addition.
AdditiveSafeInteger (Int53) is used instead of SafeInteger because
the safe integer range (-2^53 + 1 to 2^53 -1) excludes -2^53.
Using SafeInteger directly would require an additional check after
the operation to ensure the result remains within the valid bounds.
This change does *not* implement Maglev-specific optimizations.
Bug: 384959125
Change-Id: I7b54d075c55ab7828e407cec03933a34be6247d5
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6038008
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#98643}
The first V8 milestone branch that contains this commit is branch-heads/13.5 (Chrome M135). V8 branch-heads/13.4 does NOT contain the commit.
Reproduction Case
Release Build
out/x64.release/d8 --allow-natives-syntax poc.js
Result:
=== Wrong Result ===
[Interpreter] f(5) = 5
[TurboFan] f(5) = -1
Optimized: true
Debug Build
out/x64.debug/d8 --allow-natives-syntax poc.js
Result:
=== Wrong Result ===
[Interpreter] f(5) = 5
[TurboFan] f(5) = -1
Optimized: true
Also confirmed on ASAN release build (out/x64.release_asan/d8 --allow-natives-syntax poc.js) with identical results.
PoC Code
// Run: out/x64.release/d8 --allow-natives-syntax poc.js
const BASE = 4503599627370494; // 2^52 - 2
function f_interp(x) {
let ix = x >>> 0;
let idx = (ix + BASE) - BASE;
if (idx < 2) return -1;
return idx;
}
// Separate function to avoid feedback pollution from f_interp(5),
// which would record idx=5 and prevent AdditiveSafeInteger speculation.
function f_jit(x) {
let ix = x >>> 0;
let idx = (ix + BASE) - BASE;
if (idx < 2) return -1;
return idx;
}
// --- Interpreter ---
const interp = f_interp(5);
// --- TurboFan (train only with 0,1 so feedback stays AdditiveSafeInteger) ---
%PrepareFunctionForOptimization(f_jit);
for (let i = 0; i < 10000; i++) { f_jit(0); f_jit(1); }
%OptimizeFunctionOnNextCall(f_jit);
f_jit(0);
const opt = f_jit(5);
print("=== Wrong Result ===");
print("[Interpreter] f(5) = " + interp); // 5 (correct)
print("[TurboFan] f(5) = " + opt); // -1 (wrong)
print("Optimized: " + ((%GetOptimizationStatus(f_jit) & 32) !== 0));
Suggested Patch
src/compiler/operation-typer.cc
--- a/src/compiler/operation-typer.cc
+++ b/src/compiler/operation-typer.cc
@@ -721,7 +721,11 @@
Type OperationTyper::SpeculativeAdditiveSafeIntegerAdd(Type lhs, Type rhs) {
Type result = SpeculativeNumberAdd(lhs, rhs);
- if (lhs.Is(cache_->kAdditiveSafeInteger) ||
- rhs.Is(cache_->kAdditiveSafeInteger)) {
+ // Only narrow to kAdditiveSafeInteger when BOTH inputs are within range
+ // AND the computed result is already within range. With the OR condition,
+ // one input near the boundary plus any in-range value can produce a sum
+ // that exceeds the bounds, and the intersection clips the result type to
+ // a range that doesn't contain all possible values.
+ if (lhs.Is(cache_->kAdditiveSafeInteger) &&
+ rhs.Is(cache_->kAdditiveSafeInteger) &&
+ result.Is(cache_->kAdditiveSafeInteger)) {
return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone());
}
return result;
@@ -730,8 +734,9 @@
Type OperationTyper::SpeculativeAdditiveSafeIntegerSubtract(Type lhs,
Type rhs) {
Type result = SpeculativeNumberSubtract(lhs, rhs);
- if (lhs.Is(cache_->kAdditiveSafeInteger) ||
- rhs.Is(cache_->kAdditiveSafeInteger)) {
+ if (lhs.Is(cache_->kAdditiveSafeInteger) &&
+ rhs.Is(cache_->kAdditiveSafeInteger) &&
+ result.Is(cache_->kAdditiveSafeInteger)) {
return Type::Intersect(result, cache_->kAdditiveSafeInteger, zone());
}
return result;
Explanation
The fix changes the OR condition (||) to AND (&&) with an additional guard that the computed result is already within kAdditiveSafeInteger. The intersection is now only applied when it is provably a no-op (i.e., when both inputs AND the result are within bounds), so it can never incorrectly clip the result type.
The original OR condition was wrong because two values that are individually within kAdditiveSafeInteger (e.g., Uint32 [0, 2^32-1] and constant 2^52 - 2) can sum to a value that exceeds the range. The intersection then clips the result to a range that excludes actually-reachable values, creating incorrect type information that persists through algebraic simplification and causes downstream miscompilation.
Note: CanSpeculateAdditiveSafeInteger in simplified-lowering.cc:1839 does not need fixing. Its OR condition only controls whether CheckedAdditiveSafeIntegerAdd/Sub operations (with runtime overflow checks) are emitted – a lowering decision. The bug is solely in the typing of the result.
Credit Information
Reporter credit: Junyoung Park(@candymate) of KAIST Hacking Lab