Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker526380803
Fix commit2a21c7560310 (v8/v8) +20/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-06

Background

`VariableDeclarationParsingScope`
A parser helper (an ExpressionScope subclass) active while a let/const/var declaration is being parsed, responsible for declaring each bound name into a target scope.
Destructuring pattern
A binding form like let [a, b] whose sub-expressions are parsed individually and can themselves introduce nested lexical scopes.
`ClassScope`
The dedicated scope V8 pushes onto parser()->scope() when it parses a class expression, holding the class’s internal name and private-member bindings.
`DeclareVariable`
The Parser method that inserts a named binding into a specified Scope, asserting via DCHECK that the resulting scope state is internally consistent.

Root Cause Analysis

VariableDeclarationParsingScope::Declare resolved the destination scope by re-reading this->parser()->scope() dynamically at declaration time instead of using the scope that was current when the parsing scope was constructed. When a destructuring binding pattern such as let [class x{...x}] embedded a class expression, parsing that inner expression pushed a ClassScope onto parser()->scope(), so the outer pattern’s names were declared into the transient ClassScope rather than the enclosing declaration scope. This violated the invariant that all names bound by a single VariableDeclarationParsingScope land in the scope where that scope was created, producing scope pollution and a name collision that tripped a DCHECK when the ClassScope subsequently declared its own internal class binding.

The fix captures scope_ = parser->scope() in the constructor and passes that stored scope_ to DeclareVariable (and uses it for the num_var() limit check), so declarations are always anchored to the creation-time scope regardless of scopes pushed by nested expressions.

Key insight
The single mistake was reading the target scope late and dynamically (parser()->scope() at declaration time) instead of binding it at object construction, letting nested class scopes hijack outer declarations; the fix pins the scope once at construction (scope_) and uses it throughout.

Attack Path

  1. Craft nested-scope binding An attacker supplies JavaScript with a variable declaration whose destructuring pattern embeds a class expression, e.g. let [class x{...x}].
  2. Push inner ClassScope While parsing the inner class sub-expression, the parser pushes a ClassScope onto parser()->scope(), changing what the dynamic scope lookup returns.
  3. Misdirect outer declaration VariableDeclarationParsingScope::Declare reads the now-current ClassScope and declares the outer pattern’s name there instead of the enclosing scope.
  4. Trigger name collision The ClassScope later declares its own internal class binding, colliding with the mis-placed name and failing a consistency DCHECK.

Impact Assessment

An attacker who can run script in the renderer causes the V8 parser to declare bindings in the wrong scope, corrupting scope resolution and hitting a DCHECK-guarded invariant violation reachable purely from parsing attacker-controlled source. The effect occurs in the renderer process during compilation, with the only precondition being the ability to feed a crafted script (e.g. via a web page or eval); the metadata classifies this as a high-severity logic/inappropriate-implementation error rather than a demonstrated memory-corruption primitive.

Changed Functions

FunctionChangeNotes
names_
src/parsing/expression-scope.h
modified

Files Changed

  • src/parsing/expression-scope.h
  • test/mjsunit/regress/regress-526380803-1.js
  • test/mjsunit/regress/regress-526380803-2.js

Audit Directions

  • Dynamic scope re-reads
    Search parser and expression-scope code for parser()->scope() (or similar current-context accessors) used after construction where a captured, creation-time reference is the correct semantics.
  • State captured late in helper scopes
    Review RAII-style parsing/scoping helpers that read mutable global-ish parser state at use time rather than snapshotting it at construction, since intervening sub-parses can mutate it.
  • Destructuring plus nested scope introducers
    Audit paths where destructuring, default-value expressions, or pattern sub-expressions can embed class or function scopes, checking that outer bindings are not redirected into those inner scopes.
From 2a21c756031083b4abcd0e3077790d485e270e14 Mon Sep 17 00:00:00 2001
From: Leszek Swirski <[email protected]>
Date: Wed, 29 Jul 2026 17:43:31 +0200
Subject: [PATCH] [parser] Use creation-time scope in VariableDeclarationParsingScope

When parsing variable declarations that have destructuring, inner expressions within destructuring patterns can push child scopes (such as ClassScope) onto parser()->scope(), e.g. `let [class x{...x}]`

If VariableDeclarationParsingScope::Declare uses parser()->scope()
dynamically at declaration time, variables from the outer binding
pattern are declared in the inner ClassScope instead of the outer scope
where the VariableDeclarationParsingScope was created. This causes scope
pollution and name collision DCHECK failures when ClassScope later
declares its internal class binding.

This CL fixes the issue by capturing scope_ = parser->scope() at
construction time in VariableDeclarationParsingScope and passing scope_
to DeclareVariable.

TAG=agy
CONV=a1d36d0b-e7d9-488d-9a5b-3e7b1a1e5870

Fixed: 526380803
Change-Id: Iedeb8d7f2fdb770b86afe84a058bb91ced246f16
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8167021
Auto-Submit: Leszek Swirski <[email protected]>
Commit-Queue: Leszek Swirski <[email protected]>
Reviewed-by: Olivier Flückiger <[email protected]>
Cr-Commit-Position: refs/heads/main@{#108953}
---

diff --git a/src/parsing/expression-scope.h b/src/parsing/expression-scope.h
index eee29e7..a448a4e 100644
--- a/src/parsing/expression-scope.h
+++ b/src/parsing/expression-scope.h
@@ -370,7 +370,8 @@
                                      ? ExpressionScopeT::kLexicalDeclaration
                                      : ExpressionScopeT::kVarDeclaration),
         mode_(mode),
-        names_(names) {}
+        names_(names),
+        scope_(parser->scope()) {}
 
   VariableDeclarationParsingScope(const VariableDeclarationParsingScope&) =
       delete;
@@ -381,10 +382,9 @@
     VariableKind kind = NORMAL_VARIABLE;
     bool was_added;
     Variable* var = this->parser()->DeclareVariable(
-        name, kind, mode_, Variable::DefaultInitializationFlag(mode_),
-        this->parser()->scope(), &was_added, pos);
-    if (was_added &&
-        this->parser()->scope()->num_var() > kMaxNumFunctionLocals) {
+        name, kind, mode_, Variable::DefaultInitializationFlag(mode_), scope_,
+        &was_added, pos);
+    if (was_added && scope_->num_var() > kMaxNumFunctionLocals) {
       this->parser()->ReportMessage(MessageTemplate::kTooManyVariables);
     }
     if (names_) names_->Add(name, this->parser()->zone());
@@ -428,6 +428,7 @@
 
   VariableMode mode_;
   ZonePtrList<const AstRawString>* names_;
+  Scope* scope_;
 };
 
 template <typename Types>
diff --git a/test/mjsunit/regress/regress-526380803-1.js b/test/mjsunit/regress/regress-526380803-1.js
new file mode 100644
index 0000000..c4662fc
--- /dev/null
+++ b/test/mjsunit/regress/regress-526380803-1.js
@@ -0,0 +1,5 @@
+// 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.
+
+assertThrows(() => eval("let[class x{...x}]"), SyntaxError);
diff --git a/test/mjsunit/regress/regress-526380803-2.js b/test/mjsunit/regress/regress-526380803-2.js
new file mode 100644
index 0000000..bcede54
--- /dev/null
+++ b/test/mjsunit/regress/regress-526380803-2.js
@@ -0,0 +1,9 @@
+// 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.
+
+function testScopeResolutionInPatternDefault() {
+  let [c = class A { field = x; }, x] = [undefined, 42];
+  assertEquals(42, new c().field);
+}
+testScopeResolutionInPatternDefault();
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/regress-526380803-1.js b/test/mjsunit/regress/regress-526380803-1.js
new file mode 100644
index 0000000..c4662fc
--- /dev/null
+++ b/test/mjsunit/regress/regress-526380803-1.js
@@ -0,0 +1,5 @@
+// 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.
+
+assertThrows(() => eval("let[class x{...x}]"), SyntaxError);
diff --git a/test/mjsunit/regress/regress-526380803-2.js b/test/mjsunit/regress/regress-526380803-2.js
new file mode 100644
index 0000000..bcede54
--- /dev/null
+++ b/test/mjsunit/regress/regress-526380803-2.js
@@ -0,0 +1,9 @@
+// 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.
+
+function testScopeResolutionInPatternDefault() {
+  let [c = class A { field = x; }, x] = [undefined, 42];
+  assertEquals(42, new c().field);
+}
+testScopeResolutionInPatternDefault();
Loading diff…

Original Bug Report

reported by [email protected]

javascript_parser_proto_fuzzer: DCHECK failure in was_added in scopes.cc

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

Fuzzing Engine: libFuzzer Fuzz Target: javascript_parser_proto_fuzzer Job Type: libfuzzer_chrome_asan_debug Platform Id: linux

Crash Type: DCHECK failure Crash Address: Crash State: was_added in scopes.cc v8::internal::ClassScope::DeclareClassVariable v8::internal::Parser::DeclareClassVariable

Sanitizer: address (ASAN)

Regressed: https://clusterfuzz.com/revisions?job=libfuzzer_chrome_asan_debug&range=1323726:1323731

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

Issue filed automatically.

See https://chromium.googlesource.com/chromium/src/+/master/testing/libfuzzer/reproducing.md for instructions on reproducing this bug locally.

View on issue tracker