Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds memory access in V8
DescriptionOut of bounds memory access in V8
ComponentV8
Sub-componentBlink>JavaScript>Runtime
Bug ClassOOB
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
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.
Independent review by AIPlausible
Reviewer notes: 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 second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker458914193
Fix commit978f2b8a73fd (v8/v8) +96/-29
CWECWE-125, CWE-787 (Out-of-bounds read, Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Credited@p1nky4745
Reported2025-11-08 · bounty $8000 (per vendor advisory)
Fix landed2025-11-25 (public git)
Disclosed2026-01-13
Patch gap49 days — fix public in source before the advisory

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 IsClassMembersInitializerFunction use base::IsInRange over 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/SkipInfos manage 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 their info_id.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
Intertwined static and instance class-member initializers shared indistinguishable 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

  1. 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.
  2. Trigger lazy compile then reparse Cause the initializer functions to be lazily compiled and later reparsed, invoking the info_id-based location logic.
  3. Mix up the ids With the ambiguous FunctionKinds, the reparser matches an initializer to the wrong info_id, resolving it to the wrong function’s source range / SharedFunctionInfo (inferred as to the exact mismatched structure).
  4. 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

An attacker running crafted JavaScript that defines classes with intertwined static/instance member initializers can desynchronize V8’s reparse 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.cc
  • src/ast/scopes.h
  • src/objects/call-site-info.cc
  • src/objects/function-kind.h
  • src/objects/shared-function-info.cc
  • src/parsing/parser-base.h

Audit Directions

  • Ambiguous `FunctionKind` ranges
    Review every base::IsInRange/IsClass*Function predicate after adding enum values to confirm ranges and kLastFunctionKind cover all intended kinds and none leak.
  • Reparse id bookkeeping
    Audit info_id/ResetInfoId/SkipInfos usage for synthetic functions (initializers, arrow bodies, default parameters) where ordering or intertwining could misassign ids to the wrong function/SharedFunctionInfo.
  • Order-dependent synthetic scopes
    Look for other lazily-created scopes (EnsureStaticElementsScope/EnsureInstanceMembersScope analogues) whose identity depends on declaration order but is not encoded in the kind or id.
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);
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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

View on issue tracker