CVE-2026-0899
Overview
Background
- Class member initializer functions
- synthetic functions V8 creates to run a class’s instance-field initializers (
kClassMembersInitializerFunction) and static-field initializers (kClassStaticInitializerFunction). - `FunctionKind`
- a 5-bit enum tagging each function’s role; range predicates like
IsClassMembersInitializerFunctionusebase::IsInRangeover contiguous enum values to classify a kind. - `info_id` / reparsing
- the parser assigns sequential info ids to nested functions so a lazily-compiled function can be located and reparsed later;
ResetInfoId/SkipInfosmanage this counter. - `EnsureStaticElementsScope` / `EnsureInstanceMembersScope`
- parser-base helpers that lazily create the scope (and
FunctionKind) for a class’s static and instance initializer functions and record theirinfo_id.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Diff truncated: ResetInfoId(id) call site and reparse id-restore logic not shown, yet stated as fact. Path from swapped info_id to OOB access (wrong SFI/scope during lazy reparse) never shown; OOB mechanism inferred only from advisory.
A class can have both static and instance member initializers, and when they are textually intertwined the two synthetic initializer functions were both created with generic kinds (kClassStaticInitializerFunction and kClassMembersInitializerFunction) that did not encode their relative ordering. Because the reparsing/info-id logic distinguished the initializer functions only by these ambiguous kinds, the ids of the static and instance initializers could be “mixed up” (per the commit message), so reparsing associated an initializer with the wrong info_id and therefore the wrong source region / SharedFunctionInfo.
The fix adds two new ordering-aware kinds — kClassMembersInitializerFunctionPrecededByStatic and kClassStaticInitializerFunctionPrecededByMember — assigned in EnsureStaticElementsScope/EnsureInstanceMembersScope based on whether the other kind already exists, and generalizes the range predicate to IsClassInitializerFunction (with new sub-predicates IsClassInstanceInitializerFunction/IsClassStaticInitializerFunction) so all classification, arguments banning, lazy-compilability, and name selection cover the expanded range. ResetInfoId also gains an int id parameter so the counter can be restored to a specific value during reparse, keeping ids aligned. Correcting the kind/ordering lets reparse resolve each initializer to its correct id and source, eliminating the mismatch.
FunctionKinds, so their reparse info_ids could be swapped; the fix encodes the ordering into distinct new kinds (and lets ResetInfoId take an explicit id) so each initializer keeps its correct id.Attack Path
- Define a class with intertwined initializers Author a class whose static and public (instance) field initializers are interleaved so both an instance-members initializer and a static initializer are created.
- Trigger lazy compile then reparse
Cause the initializer functions to be lazily compiled and later reparsed, invoking the
info_id-based location logic. - Mix up the ids
With the ambiguous
FunctionKinds, the reparser matches an initializer to the wronginfo_id, resolving it to the wrong function’s source range /SharedFunctionInfo(inferred as to the exact mismatched structure). - Access out of bounds Operating on the wrong reparsed function metadata leads to the out-of-bounds memory access the advisory reports (inferred as to the exact buffer/offset, which the diff does not expose).
Impact Assessment
info_id bookkeeping, which the advisory classifies as an out-of-bounds memory access in V8, in the renderer process. The precondition is only script execution; the advisory does not state exploitation in the wild, and escalation from the OOB access to a controlled read/write primitive or code execution is (inferred) and not shown by the patch.Files Changed
src/ast/scopes.ccsrc/ast/scopes.hsrc/objects/call-site-info.ccsrc/objects/function-kind.hsrc/objects/shared-function-info.ccsrc/parsing/parser-base.h
Audit Directions
- Ambiguous `FunctionKind` rangesReview every
base::IsInRange/IsClass*Functionpredicate after adding enum values to confirm ranges andkLastFunctionKindcover all intended kinds and none leak. - Reparse id bookkeepingAudit
info_id/ResetInfoId/SkipInfosusage for synthetic functions (initializers, arrow bodies, default parameters) where ordering or intertwining could misassign ids to the wrong function/SharedFunctionInfo. - Order-dependent synthetic scopesLook for other lazily-created scopes (
EnsureStaticElementsScope/EnsureInstanceMembersScopeanalogues) whose identity depends on declaration order but is not encoded in the kind or id.
Patch
From 978f2b8a73fdc1c6d17fa5966dee81393e2f1533 Mon Sep 17 00:00:00 2001 From: Toon Verwaest <[email protected]> Date: Tue, 25 Nov 2025 17:37:52 +0100 Subject: [PATCH] Fix class member initializer reparsing logic Intertwined static / public member initializers can mix up ids, so unmix them. Bug: 458914193 Change-Id: If0708b56750a92e03eaa5530cfbff295c2acf630 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7203465 Reviewed-by: Leszek Swirski <[email protected]> Commit-Queue: Toon Verwaest <[email protected]> Auto-Submit: Toon Verwaest <[email protected]> Cr-Commit-Position: refs/heads/main@{#103958} --- diff --git a/src/ast/scopes.cc b/src/ast/scopes.cc index c0438fe..b476498 100644 --- a/src/ast/scopes.cc +++ b/src/ast/scopes.cc @@ -1448,7 +1448,7 @@ // Functions which force eager compilation and class member initializer // functions are not lazily compilable. return !force_eager_compilation_ && - !IsClassMembersInitializerFunction(function_kind()); + !IsClassInitializerFunction(function_kind()); } int Scope::ContextChainLength(Scope* scope) const { diff --git a/src/ast/scopes.h b/src/ast/scopes.h index 4b0a708..2698b64 100644 --- a/src/ast/scopes.h +++ b/src/ast/scopes.h @@ -996,7 +996,7 @@ #endif // V8_ENABLE_WEBASSEMBLY bool should_ban_arguments() const { - return IsClassMembersInitializerFunction(function_kind()); + return IsClassInitializerFunction(function_kind()); } void set_module_has_toplevel_await() { diff --git a/src/objects/call-site-info.cc b/src/objects/call-site-info.cc index eb9137c..f0a390c 100644 --- a/src/objects/call-site-info.cc +++ b/src/objects/call-site-info.cc @@ -467,7 +467,7 @@ DirectHandle<JSFunction> function(Cast<JSFunction>(info->function()), isolate); // Class members initializer function is not a method. - if (IsClassMembersInitializerFunction(function->shared()->kind())) { + if (IsClassInitializerFunction(function->shared()->kind())) { return isolate->factory()->null_value(); } diff --git a/src/objects/function-kind.h b/src/objects/function-kind.h index 9a1a64f..148109a 100644 --- a/src/objects/function-kind.h +++ b/src/objects/function-kind.h @@ -58,11 +58,13 @@ kConciseMethod, kStaticConciseMethod, kClassMembersInitializerFunction, + kClassMembersInitializerFunctionPrecededByStatic, kClassStaticInitializerFunction, + kClassStaticInitializerFunctionPrecededByMember, // END concise methods 2 kInvalid, - kLastFunctionKind = kClassStaticInitializerFunction, + kLastFunctionKind = kClassStaticInitializerFunctionPrecededByMember, }; constexpr int kFunctionKindBitSize = 5; @@ -105,8 +107,9 @@ inline bool IsConciseMethod(FunctionKind kind) { return base::IsInRange(kind, FunctionKind::kAsyncConciseMethod, FunctionKind::kStaticAsyncConciseGeneratorMethod) || - base::IsInRange(kind, FunctionKind::kConciseGeneratorMethod, - FunctionKind::kClassStaticInitializerFunction); + base::IsInRange( + kind, FunctionKind::kConciseGeneratorMethod, + FunctionKind::kClassStaticInitializerFunctionPrecededByMember); } inline bool IsStrictFunctionWithoutPrototype(FunctionKind kind) { @@ -114,8 +117,9 @@ FunctionKind::kAsyncArrowFunction) || base::IsInRange(kind, FunctionKind::kAsyncConciseMethod, FunctionKind::kStaticAsyncConciseGeneratorMethod) || - base::IsInRange(kind, FunctionKind::kConciseGeneratorMethod, - FunctionKind::kClassStaticInitializerFunction); + base::IsInRange( + kind, FunctionKind::kConciseGeneratorMethod, + FunctionKind::kClassStaticInitializerFunctionPrecededByMember); } inline bool IsGetterFunction(FunctionKind kind) { @@ -153,9 +157,22 @@ FunctionKind::kDerivedConstructor); } -inline bool IsClassMembersInitializerFunction(FunctionKind kind) { - return base::IsInRange(kind, FunctionKind::kClassMembersInitializerFunction, - FunctionKind::kClassStaticInitializerFunction); +inline bool IsClassInitializerFunction(FunctionKind kind) { + return base::IsInRange( + kind, FunctionKind::kClassMembersInitializerFunction, + FunctionKind::kClassStaticInitializerFunctionPrecededByMember); +} + +inline bool IsClassInstanceInitializerFunction(FunctionKind kind) { + return base::IsInRange( + kind, FunctionKind::kClassMembersInitializerFunction, + FunctionKind::kClassMembersInitializerFunctionPrecededByStatic); +} + +inline bool IsClassStaticInitializerFunction(FunctionKind kind) { + return base::IsInRange( + kind, FunctionKind::kClassStaticInitializerFunction, + FunctionKind::kClassStaticInitializerFunctionPrecededByMember); } inline bool IsConstructable(FunctionKind kind) { @@ -172,6 +189,7 @@ case FunctionKind::kStaticAsyncConciseMethod: case FunctionKind::kStaticAsyncConciseGeneratorMethod: case FunctionKind::kClassStaticInitializerFunction: + case FunctionKind::kClassStaticInitializerFunctionPrecededByMember: return true; default: return false; @@ -217,6 +235,10 @@ return "ClassMembersInitializerFunction"; case FunctionKind::kClassStaticInitializerFunction: return "ClassStaticInitializerFunction"; + case FunctionKind::kClassMembersInitializerFunctionPrecededByStatic: + return "ClassMembersInitializerFunctionPrecededByStatic"; + case FunctionKind::kClassStaticInitializerFunctionPrecededByMember: + return "ClassStaticInitializerFunctionPrecededByMember"; case FunctionKind::kDefaultBaseConstructor: return "DefaultBaseConstructor"; case FunctionKind::kDefaultDerivedConstructor: diff --git a/src/objects/shared-function-info.cc b/src/objects/shared-function-info.cc index e8877a4..748cde9 100644 --- a/src/objects/shared-function-info.cc +++ b/src/objects/shared-function-info.cc @@ -344,8 +344,8 @@ } #endif // V8_ENABLE_WEBASSEMBLY FunctionKind function_kind = shared->kind(); - if (IsClassMembersInitializerFunction(function_kind)) { - return function_kind == FunctionKind::kClassMembersInitializerFunction + if (IsClassInitializerFunction(function_kind)) { + return IsClassInstanceInitializerFunction(function_kind) ? isolate->factory()->instance_members_initializer_string() : isolate->factory()->static_initializer_string(); } diff --git a/src/parsing/parser-base.h b/src/parsing/parser-base.h index dddf315..bf412c9 100644 --- a/src/parsing/parser-base.h +++ b/src/parsing/parser-base.h @@ -310,7 +310,10 @@ void SkipInfos(int delta) { info_id_ += delta; } - void ResetInfoId() { info_id_ = 0; } + void ResetInfoId(int id = 0) { + DCHECK_LE(0, id); + info_id_ = id; + } // The Zone where the parsing outputs are stored. Zone* main_zone() const { return ast_value_factory()->single_parse_zone(); } @@ -628,8 +631,11 @@ DeclarationScope* EnsureStaticElementsScope(ParserBase* parser, int beg_pos, int info_id) { if (!has_static_elements()) { - static_elements_scope = parser->NewFunctionScope( - FunctionKind::kClassStaticInitializerFunction); + FunctionKind kind = + has_instance_members() + ? FunctionKind::kClassStaticInitializerFunctionPrecededByMember + : FunctionKind::kClassStaticInitializerFunction; + static_elements_scope = parser->NewFunctionScope(kind); static_elements_scope->SetLanguageMode(LanguageMode::kStrict); static_elements_scope->set_start_position(beg_pos); static_elements_function_id = info_id; @@ -643,8 +649,11 @@ DeclarationScope* EnsureInstanceMembersScope(ParserBase* parser, int beg_pos, int info_id) { if (!has_instance_members()) { - instance_members_scope = parser->NewFunctionScope( - FunctionKind::kClassMembersInitializerFunction); + FunctionKind kind = + has_static_elements() + ? FunctionKind::kClassMembersInitializerFunctionPrecededByStatic + : FunctionKind::kClassMembersInitializerFunction; + instance_members_scope = parser->NewFunctionScope(kind); instance_members_scope->SetLanguageMode(LanguageMode::kStrict);
Regression Test / PoC
diff --git a/test/unittests/objects/object-unittest.cc b/test/unittests/objects/object-unittest.cc
index e3c7d92..18032ea 100644
--- a/test/unittests/objects/object-unittest.cc
+++ b/test/unittests/objects/object-unittest.cc
@@ -575,6 +575,9 @@
case FunctionKind::kAsyncConciseGeneratorMethod:
case FunctionKind::kStaticAsyncConciseGeneratorMethod:
case FunctionKind::kClassMembersInitializerFunction:
+ case FunctionKind::kClassMembersInitializerFunctionPrecededByStatic:
+ case FunctionKind::kClassStaticInitializerFunction:
+ case FunctionKind::kClassStaticInitializerFunctionPrecededByMember:
return true;
default:
return false;
@@ -661,6 +664,9 @@
case FunctionKind::kConciseMethod:
case FunctionKind::kStaticConciseMethod:
case FunctionKind::kClassMembersInitializerFunction:
+ case FunctionKind::kClassMembersInitializerFunctionPrecededByStatic:
+ case FunctionKind::kClassStaticInitializerFunction:
+ case FunctionKind::kClassStaticInitializerFunctionPrecededByMember:
return false;
default:
return true;
Original Bug Report
Dcheck failure in fixed-array-inl.h
Description
Compilation error occurs with nested classes having a specific structure. The error manifests as an array bounds check violation in fixed-array-inl.h.
The error happens when attempting to access a WeakFixedArray element at index 6, which is out of bounds.
Reproduction Case
class C1 {}
const v1 = {
n() {
try {
this.n();
} catch (e) {}
class C2 {
constructor() {
class C3 extends C1 {
constructor() {}
a;
static {};
b;
static {
let x = 0;
}
}
}
};
new C2();
}
};
let res = v1.n();
Tested on debug build without ASAN on Linux x64
Build flags:
is_debug = true
target_cpu = "x64"
v8_enable_backtrace = true
v8_static_library = true
is_component_build = false
dcheck_always_on = true
v8_enable_disassembler = true
v8_enable_debugging_features=true
v8_dcheck_always_on = true
./out/debug/d8 poc.js
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 116
# Debug check failed: IsInBounds(index).
#
#
#
#FailureMessage Object: 0x7ffec77d5720
==== C stack trace ===============================
./out/debug/d8(v8::base::debug::StackTrace::StackTrace()+0x13) [0x55980cf3cbb3]
./out/debug/d8(+0x2909abd) [0x55980cf3babd]
./out/debug/d8(V8_Fatal(char const*, int, char const*, ...)+0x194) [0x55980cf33774]
./out/debug/d8(+0x2901025) [0x55980cf33025]
./out/debug/d8(void v8::internal::DeclarationScope::AllocateScopeInfos<v8::internal::Isolate>(v8::internal::ParseInfo*, v8::internal::DirectHandle<v8::internal::Script>, v8::internal::Isolate*)+0x715) [0x55980d153d05]
./out/debug/d8(+0x2ac01bf) [0x55980d0f21bf]
./out/debug/d8(v8::internal::Compiler::Compile(v8::internal::Isolate*, v8::internal::Handle<v8::internal::SharedFunctionInfo>, v8::internal::Compiler::ClearExceptionFlag, v8::internal::IsCompiledScope*, v8::internal::CreateSourcePositions)+0x88f) [0x55980d0f16cf]
./out/debug/d8(v8::internal::Compiler::Compile(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::Compiler::ClearExceptionFlag, v8::internal::IsCompiledScope*)+0x259) [0x55980d0f2909]
./out/debug/d8(+0x397500d) [0x55980dfa700d]
./out/debug/d8(v8::internal::Runtime_CompileLazy(int, unsigned long*, v8::internal::Isolate*)+0x84) [0x55980dfa6b14]
./out/debug/d8(+0x62fb67d) [0x55981092d67d]
Trace/breakpoint trap
Introduced
This vulnerability was introduced in: https://chromium.googlesource.com/v8/v8/+/a96a186d4d293bb9cc728eac23709b4e79dc358a (M141 Stable)
Proposed Fix
Fixing this proved to be quite challenging for me, but for a local fix I discovered that limiting the loop in DeclarationScope::AllocateScopeInfos to infos->length() prevents the crash:
diff --git a/src/ast/scopes.cc b/src/ast/scopes.cc
index e406e916459..b2ba7218be0 100644
--- a/src/ast/scopes.cc
+++ b/src/ast/scopes.cc
@@ -2803,7 +2803,7 @@ void DeclarationScope::AllocateScopeInfos(ParseInfo* parse_info,
// reuse. Also look at the compiled function itself, and reuse its function
// scope info if it exists.
for (int i = parse_info->literal()->function_literal_id();
- i <= parse_info->max_info_id(); ++i) {
+ i <= parse_info->max_info_id() && i < infos->length(); ++i) {
Tagged<MaybeObject> maybe_info = infos->get(i);
if (maybe_info.IsWeak()) {
Tagged<Object> info = maybe_info.GetHeapObjectAssumeWeak();
Additionally, reverting the changes made in commit a96a186d4d293bb9cc728eac23709b4e79dc358a also prevents the crash.
CREDIT INFORMATION
Reporter credit: @p1nky4745