CVE-2026-9968
Overview
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
Callnode that storeseval_scope_info_index, so the index must fit within2^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”.
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
- Fill the info-ID space near the limit
supply a script with roughly
2^20trivial function literals (the PoC repeats()=>0;about1048573times) soGetNextInfoId()is close to the 20-bit boundary. - Place an `eval` where reindexing applies
put the
evalinside an arrow-function parameter default or a class computed member name (e.g.(x = eval('')) => {}orclass C { [(() => eval(''))()] = 1 }), so the eval’s index will later be reindexed. - 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. - Overflow the 20-bit field via drift
subsequent
ReindexArrowFunctionFormalParameters/ReindexComputedMemberNameshift the index past2^20, truncatingEvalScopeInfoIndexFieldto 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
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
| Function | Change | Notes |
|---|---|---|
AllowReindexScopesrc/ast/ast-function-literal-id-reindexer.h |
modified | |
AstFunctionLiteralIdReindexersrc/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 | |
ifsrc/parsing/parser-base.h |
modified |
Files Changed
src/ast/ast-function-literal-id-reindexer.ccsrc/ast/ast-function-literal-id-reindexer.hsrc/parsing/expression-scope.hsrc/parsing/parser-base.hsrc/parsing/parser.cc
Audit Directions
- Validate-then-mutate index checksaudit 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 deltasreview 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 siteswith reindexing now gated by
AllowReindexScope, look for any otherReindex*invocation or ID-shifting path that runs without accounting for its effect on already-validated indices.
Patch
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(¶meters);
+ AllowReindexScope dummy_scope(nullptr);
+ impl()->ReindexArrowFunctionFormalParameters(¶meters, 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 @@
Regression Test / PoC
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");
Original Bug Report
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.