Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in V8
DescriptionOut of bounds read in V8
ComponentV8
Bug ClassOOB
Tracker499659070
Fix commit330a25ad001e (v8/v8) +86/-99
CISA KEVNot listed
CreditedJunYoung Park(@candymate) of KAIST Hacking Lab
Disclosed2026-06-02

Files Changed

  • src/builtins/builtins-number-tsa.cc
  • src/codegen/code-stub-assembler.cc
  • src/common/globals.h
  • src/compiler/operation-typer.cc
  • src/compiler/representation-change.cc
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 @@
Loading diff…

Regression Test / PoC

shipped with the fix
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.
Loading diff…

Original Bug Report

reported by ca...@gmail.com

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):

  1. Incorrect Type Computation (Typer phase): For ix (x >>> 0, Uint32 range [0, 2^32-1]) – which is a subset of kAdditiveSafeInteger – adding constant BASE = 2^52 - 2 produces SpeculativeNumberAdd result Range(4503599627370494, 4503603922337789). The OR condition triggers (both inputs are individually within kAdditiveSafeInteger), and intersection with kAdditiveSafeInteger clips this to Range(4503599627370494, 4503599627370495) – just 2 possible values instead of 2^32.

  2. Type Propagation (Typer phase): The subtraction sum - BASE is typed as Range(0, 1) by SpeculativeAdditiveSafeIntegerSubtract, because the subtraction ranger computes Range(4503599627370494, 4503599627370495) - Range(4503599627370494, 4503599627370494) = Range(0, 1). The actual mathematical range should be [0, 2^32-1].

  3. Algebraic Simplification (LateOptimization phase): The MachineOperatorReducer (src/compiler/machine-operator-reducer.cc:1179-1196) converts (x + C) - C into x + C + (-C), then constant-folds C + (-C) = 0 via ReduceInt64Add (src/compiler/machine-operator-reducer.cc:1147-1155), resulting in x + 0 = x. This eliminates both the addition and subtraction operations along with their CheckedAdditiveSafeIntegerAdd/Sub runtime overflow checks. The incorrect type Range(0, 1) from step 2 persists on the result node even though the actual runtime value is the unmodified x >>> 0.

  4. Comparison Folding (TypedOptimizations phase): TypeNarrowingReducer (src/compiler/type-narrowing-reducer.cc:33-34) folds idx < 2 to singleton_true because Range(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();
    }
    
  5. 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

  1. Target must be a 64-bit architecture (additive_safe_int_feedback flag is true by default only on 64-bit; src/flags/flag-definitions.h:860-877)
  2. The addition operand must be within kAdditiveSafeInteger range (e.g., x >>> 0 produces Uint32, a subset)
  3. The constant added must be close to kMaxAdditiveSafeInteger (2^52 - 1) so that the sum can exceed the range
  4. The addition must be paired with a subtraction of the same constant, enabling algebraic simplification
  5. The result of the add-sub pair must be used in a comparison that can be folded by TypeNarrowingReducer
  6. The function must be compiled by TurboFan (requires sufficient warmup or %OptimizeFunctionOnNextCall)

Version

Reproduced Version

  • main branch 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

View on issue tracker