Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in V8
DescriptionInteger overflow in V8
ComponentV8
Sub-componentParsing
Bug ClassInteger Overflow
Action tierwatch — ub-class only — no demonstrated corruption path
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker506499280
Fix commit89ba284081b3 (v8/v8) +94/-15
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-25 (per vendor advisory)
Fix landed2026-05-12 (public git)
Disclosed2026-05-27
Patch gap15 days — fix public in source before the advisory

Background

Function literal / info IDs
monotonically increasing integer IDs the parser assigns to function literals and related parse artifacts via GetNextInfoId()/PeekNextInfoId().
`Call::EvalScopeInfoIndexField`
a 20-bit field packed into a Call node that stores eval_scope_info_index, so the index must fit within 2^20.
Reindexing
the parser sometimes shifts already-assigned function-literal IDs upward (AstFunctionLiteralIdReindexer::Reindex) to make room, e.g. when an arrow function’s formal parameters must move down by one, or when a class computed member name is re-parsed (ReindexArrowFunctionFormalParameters, ReindexComputedMemberName).
`AllowReindexScope` / `max_drift_`
a new RAII scope that increments/decrements the parser’s max_drift_ counter, tracking the maximum extra ID drift reindexing may introduce.

Root Cause Analysis

When the parser detected a possible eval call it set eval_scope_info_index = GetNextInfoId() and validated it with Call::EvalScopeInfoIndexField::is_valid(eval_scope_info_index), checking only the value at that moment. However, subsequent reindexing (arrow-function parameter reindexing and computed-member-name reindexing) can shift these IDs upward, so an index that was valid when checked could later drift beyond the 20-bit field’s 2^20 capacity, overflowing/truncating EvalScopeInfoIndexField and producing a wrong scope-info index.

The fix makes the parser accumulate the worst-case additional drift into max_drift_ (via AllowReindexScope created for arrow-parameter scopes and for computed member names) and validates is_valid(eval_scope_info_index + max_drift_) instead, rejecting with MessageTemplate::kTooManyEvals before overflow can occur. The reindexing entry points now take an AllowReindexScope argument to enforce that reindexing only happens where the drift was accounted for. The regression tests confirm this by using ~1048573 (~2^20) filler function literals plus an eval inside an arrow parameter / computed class member, expecting a SyntaxError “Too many eval calls in script”.

Key insight
The single mistake was validating eval_scope_info_index against the 20-bit field using only its current value, ignoring that later reindexing would add drift and push it past 2^20; the fix pre-accounts for the maximum drift (eval_scope_info_index + max_drift_) so the overflow is rejected up front.

Attack Path

  1. Fill the info-ID space near the limit supply a script with roughly 2^20 trivial function literals (the PoC repeats ()=>0; about 1048573 times) so GetNextInfoId() is close to the 20-bit boundary.
  2. Place an `eval` where reindexing applies put the eval inside an arrow-function parameter default or a class computed member name (e.g. (x = eval('')) => {} or class C { [(() => eval(''))()] = 1 }), so the eval’s index will later be reindexed.
  3. Pass the pre-drift validation eval_scope_info_index = GetNextInfoId() is validated against the field while still (barely) in range, so the old check accepts it.
  4. Overflow the 20-bit field via drift subsequent ReindexArrowFunctionFormalParameters/ReindexComputedMemberName shift the index past 2^20, truncating EvalScopeInfoIndexField to a wrong (aliased) scope-info index (downstream misuse of the corrupted index is (inferred); the diff shows only the overflow and the added guard).

Impact Assessment

An attacker supplying a crafted script (e.g. via new Function or any parsed source) can drive eval_scope_info_index past the 20-bit EvalScopeInfoIndexField so the packed index silently truncates, an integer overflow in V8’s parser that yields an incorrect scope-info index. The advisory classifies this as an integer overflow in V8 and does not state exploited-in-the-wild status; any escalation to a type confusion or memory-corruption primitive from the aliased index is (inferred) and not demonstrated by the patch. The precondition is the ability to parse a very large script that reaches near the 2^20 info-ID limit with an eval in a reindexed position.

Changed Functions

FunctionChangeNotes
AllowReindexScope
src/ast/ast-function-literal-id-reindexer.h
modified
AstFunctionLiteralIdReindexer
src/ast/ast-function-literal-id-reindexer.h
modified
function_literal_id_
src/parsing/expression-scope.h
modified
allow_reindex_scope_
src/parsing/expression-scope.h
modified
if
src/parsing/parser-base.h
modified

Files Changed

  • src/ast/ast-function-literal-id-reindexer.cc
  • src/ast/ast-function-literal-id-reindexer.h
  • src/parsing/expression-scope.h
  • src/parsing/parser-base.h
  • src/parsing/parser.cc

Audit Directions

  • Validate-then-mutate index checks
    audit places that bounds-check an ID or offset against a packed bit-field and then perform reindexing/shifting afterward; the check must include the maximum post-mutation value.
  • Bit-field capacity vs. accumulated deltas
    review other narrow packed fields (*Field::is_valid) fed by counters that can be adjusted by later parser passes, ensuring worst-case drift is bounded before packing.
  • Reindexing call sites
    with reindexing now gated by AllowReindexScope, look for any other Reindex* invocation or ID-shifting path that runs without accounting for its effect on already-validated indices.
From 89ba284081b3c2f96b612d2f78b79e370b0479f4 Mon Sep 17 00:00:00 2001
From: Toon Verwaest <verwaest@chromium.org>
Date: Mon, 11 May 2026 18:21:14 +0200
Subject: [PATCH] [parsing] Fix eval index overflow in bit-field

The parser now tracks the maximum potential drift that can accumulate
during arrow function parameter and computed member name reindexing.
This drift is accounted for when validating the `eval_scope_info_index`
to prevent overflows in the 20-bit field.

Enforced `AllowReindexScope` to guarantee that reindexing only happens
when properly accounted for.

TAG=agy
CONV=4cab861a-b43c-4c44-8f9b-82d0f4388a01

Bug: 506499280
Change-Id: I3f4a39b1c4600b964ecc0927a5808608e5cedd5b
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7831209
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107263}
---

diff --git a/src/ast/ast-function-literal-id-reindexer.cc b/src/ast/ast-function-literal-id-reindexer.cc
index e60afaa..99ff7ac 100644
--- a/src/ast/ast-function-literal-id-reindexer.cc
+++ b/src/ast/ast-function-literal-id-reindexer.cc
@@ -15,7 +15,8 @@
 
 AstFunctionLiteralIdReindexer::~AstFunctionLiteralIdReindexer() = default;
 
-void AstFunctionLiteralIdReindexer::Reindex(Expression* pattern) {
+void AstFunctionLiteralIdReindexer::Reindex(Expression* pattern,
+                                            const AllowReindexScope& scope) {
 #ifdef DEBUG
   visited_.clear();
 #endif
diff --git a/src/ast/ast-function-literal-id-reindexer.h b/src/ast/ast-function-literal-id-reindexer.h
index f7a41e0..8555f78 100644
--- a/src/ast/ast-function-literal-id-reindexer.h
+++ b/src/ast/ast-function-literal-id-reindexer.h
@@ -14,6 +14,19 @@
 namespace v8 {
 namespace internal {
 
+class AllowReindexScope {
+ public:
+  explicit AllowReindexScope(int* counter) : counter_(counter) {
+    if (counter_) (*counter_)++;
+  }
+  ~AllowReindexScope() {
+    if (counter_) (*counter_)--;
+  }
+
+ private:
+  int* counter_;
+};
+
 // Changes the ID of all FunctionLiterals in the given Expression by adding the
 // given delta.
 class AstFunctionLiteralIdReindexer final
@@ -25,7 +38,7 @@
       const AstFunctionLiteralIdReindexer&) = delete;
   ~AstFunctionLiteralIdReindexer();
 
-  void Reindex(Expression* pattern);
+  void Reindex(Expression* pattern, const AllowReindexScope& scope);
 
   // AstTraversalVisitor implementation.
   void VisitFunctionLiteral(FunctionLiteral* lit);
diff --git a/src/parsing/expression-scope.h b/src/parsing/expression-scope.h
index 2a8a7f9..eee29e7 100644
--- a/src/parsing/expression-scope.h
+++ b/src/parsing/expression-scope.h
@@ -7,6 +7,7 @@
 
 #include <utility>
 
+#include "src/ast/ast-function-literal-id-reindexer.h"
 #include "src/ast/scopes.h"
 #include "src/common/message-template.h"
 #include "src/objects/function-kind.h"
@@ -334,6 +335,7 @@
     return base::IsInRange(type_, kMaybeArrowParameterDeclaration,
                            kMaybeAsyncArrowParameterDeclaration);
   }
+
   bool IsCertainlyPattern() const { return IsCertainlyDeclaration(); }
   bool CanBeParameterDeclaration() const {
     return base::IsInRange(type_, kMaybeArrowParameterDeclaration,
@@ -761,7 +763,8 @@
             kind == FunctionKind::kArrowFunction
                 ? ExpressionScope<Types>::kMaybeArrowParameterDeclaration
                 : ExpressionScope<Types>::kMaybeAsyncArrowParameterDeclaration),
-        function_literal_id_(function_literal_id) {
+        function_literal_id_(function_literal_id),
+        allow_reindex_scope_(&parser->max_drift_) {
     DCHECK(kind == FunctionKind::kAsyncArrowFunction ||
            kind == FunctionKind::kArrowFunction);
     DCHECK(this->CanBeDeclaration());
@@ -852,6 +855,7 @@
   int function_literal_id_;
   bool has_simple_parameter_list_ = true;
   bool uses_this_ = false;
+  AllowReindexScope allow_reindex_scope_;
 };
 
 }  // namespace internal
diff --git a/src/parsing/parser-base.h b/src/parsing/parser-base.h
index 0cd2770..834cb59 100644
--- a/src/parsing/parser-base.h
+++ b/src/parsing/parser-base.h
@@ -746,6 +746,7 @@
     bool is_private;
     bool is_static;
     bool is_rest;
+    std::optional<AllowReindexScope> allow_reindex_scope;
   };
 
   void DeclareLabel(ZonePtrList<const AstRawString>** labels,
@@ -1703,6 +1704,10 @@
     return expression_scope_;
   }
 
+ public:
+  V8_INLINE void set_max_drift(int drift) { max_drift_ = drift; }
+  V8_INLINE int max_drift() const { return max_drift_; }
+
   bool MaybeParsingArrowhead() const {
     return expression_scope_ != nullptr &&
            expression_scope_->has_possible_arrow_parameter_in_scope_chain();
@@ -1766,6 +1771,7 @@
   FuncNameInferrer fni_;
   AstValueFactory* ast_value_factory_;  // Not owned.
   typename Types::Factory ast_node_factory_;
+  int max_drift_ = 0;
   RuntimeCallStats* runtime_call_stats_;
   internal::V8FileLogger* v8_file_logger_;
   bool parsing_on_main_thread_;
@@ -1806,6 +1812,7 @@
     DeclarationScope* scope = nullptr;
     int function_literal_id = -1;
     bool could_be_immediately_invoked = false;
+    const AllowReindexScope* allow_reindex_scope = nullptr;
 
     bool HasInitialState() const { return scope == nullptr; }
 
@@ -1814,6 +1821,7 @@
       function_literal_id = -1;
       ClearStrictParameterError();
       could_be_immediately_invoked = false;
+      allow_reindex_scope = nullptr;
       DCHECK(HasInitialState());
     }
 
@@ -2597,6 +2605,7 @@
       prop_info->is_computed_name = true;
       Consume(Token::kLeftBracket);
       AcceptINScope scope(this, true);
+      prop_info->allow_reindex_scope.emplace(&max_drift_);
       ExpressionT expression = ParseAssignmentExpression();
       Expect(Token::kRightBracket);
       if (prop_info->kind == ParsePropertyKind::kNotSet) {
@@ -2766,7 +2775,9 @@
         if (!has_error() && next_info_id != PeekNextInfoId() &&
             !(prop_info->is_static ? class_info->has_static_elements()
                                    : class_info->has_instance_members())) {
-          impl()->ReindexComputedMemberName(name_expression);
+          DCHECK(prop_info->allow_reindex_scope.has_value());
+          impl()->ReindexComputedMemberName(name_expression,
+                                            *prop_info->allow_reindex_scope);
         }
       } else {
         CheckClassFieldName(prop_info->name, prop_info->is_static);
@@ -3348,7 +3359,8 @@
     // not, we'll reindex the arrow function formal parameters to shift them all
     // 1 down to make space for the arrow function.
     if (function_literal_id != GetNextInfoId()) {
-      impl()->ReindexArrowFunctionFormalParameters(&parameters);
+      AllowReindexScope dummy_scope(nullptr);
+      impl()->ReindexArrowFunctionFormalParameters(&parameters, dummy_scope);
     }
 
     expression = ParseArrowFunctionLiteral(parameters, function_literal_id,
@@ -4048,7 +4060,8 @@
         int eval_scope_info_index = 0;
         if (CheckPossibleEvalCall(result, is_optional, scope())) {
           eval_scope_info_index = GetNextInfoId();
-          if (!Call::EvalScopeInfoIndexField::is_valid(eval_scope_info_index)) {
+          if (!Call::EvalScopeInfoIndexField::is_valid(eval_scope_info_index +
+                                                       max_drift_)) {
             ReportMessage(MessageTemplate::kTooManyEvals);
             return impl()->FailureExpression();
           }
diff --git a/src/parsing/parser.cc b/src/parsing/parser.cc
index ad49c58..5e30c28 100644
--- a/src/parsing/parser.cc
+++ b/src/parsing/parser.cc
@@ -2781,13 +2781,13 @@
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-506499280-extended.js b/test/mjsunit/regress/regress-506499280-extended.js
new file mode 100644
index 0000000..b3d3cb1
--- /dev/null
+++ b/test/mjsunit/regress/regress-506499280-extended.js
@@ -0,0 +1,31 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --fuzzing --disable-abortjs --disable-in-process-stack-traces
+
+const count = 1048573; // Pushes close to the 2^20 limit
+const filler = "()=>0;";
+
+// Helper to create a large string of functions
+function getFiller(n) {
+  return filler.repeat(n);
+}
+
+// Test case 1: Arrow function in parameter
+assertThrows(() => {
+  let s = getFiller(count) + "(x = eval('')) => {}";
+  new Function(s);
+}, SyntaxError, "Too many eval calls in script");
+
+// Test case 2: Nested arrow functions in parameters
+assertThrows(() => {
+  let s = getFiller(count - 1) + "(x = (y = eval('')) => {}) => {}";
+  new Function(s);
+}, SyntaxError, "Too many eval calls in script");
+
+// Test case 3: Computed member name in class
+assertThrows(() => {
+  let s = getFiller(count) + "class C { [(() => eval(''))()] = 1 }";
+  new Function(s);
+}, SyntaxError, "Too many eval calls in script");
diff --git a/test/mjsunit/regress/regress-506499280.js b/test/mjsunit/regress/regress-506499280.js
new file mode 100644
index 0000000..607f629
--- /dev/null
+++ b/test/mjsunit/regress/regress-506499280.js
@@ -0,0 +1,13 @@
+// Copyright 2026 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --fuzzing --disable-abortjs --disable-in-process-stack-traces
+
+const count = 1048573;
+const filler = "()=>0;";
+const s = filler.repeat(count) + "(x = eval('')) => {}";
+
+assertThrows(() => {
+  new Function(s);
+}, SyntaxError, "Too many eval calls in script");
Loading diff…

Original Bug Report

reported by 24...@project.gserviceaccount.com

CHECK failure: is_valid(value) in bit-field.h

Detailed Report: https://clusterfuzz.com/testcase?key=5005399816175616

Fuzzer: big_sleep Job Type: linux_asan_d8_dbg Platform Id: linux

Crash Type: CHECK failure Crash Address: Crash State: is_valid(value) in bit-field.h v8::internal::AstTraversalVisitorv8::internal::AstFunctionLiteralIdReindexer:: v8::internal::AstFunctionLiteralIdReindexer::Reindex

Sanitizer: address (ASAN)

Regressed: https://clusterfuzz.com/revisions?job=linux_asan_d8_dbg&range=95042:95043

Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5005399816175616

Issue filed automatically.

To reproduce this, please build the target in this report and run it against the reproducer testcase. Please use the GN arguments provided at bottom of this report when building the binary.

If you have trouble reproducing, please also export the environment variables listed under “[Environment]” in the crash stacktrace.

If you have any feedback on reproducing test cases, let us know at https://forms.gle/Yh3qCYFveHj6E5jz5 so we can improve.

View on issue tracker