CVE-2026-19150
Overview
Background
- `VariableDeclarationParsingScope`
- A parser helper (an
ExpressionScopesubclass) active while alet/const/vardeclaration 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
Parsermethod that inserts a named binding into a specifiedScope, asserting viaDCHECKthat 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.
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
- 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}]. - Push inner ClassScope
While parsing the inner class sub-expression, the parser pushes a
ClassScopeontoparser()->scope(), changing what the dynamic scope lookup returns. - Misdirect outer declaration
VariableDeclarationParsingScope::Declarereads the now-currentClassScopeand declares the outer pattern’s name there instead of the enclosing scope. - Trigger name collision
The
ClassScopelater declares its own internal class binding, colliding with the mis-placed name and failing a consistencyDCHECK.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
names_src/parsing/expression-scope.h |
modified |
Files Changed
src/parsing/expression-scope.htest/mjsunit/regress/regress-526380803-1.jstest/mjsunit/regress/regress-526380803-2.js
Audit Directions
- Dynamic scope re-readsSearch 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 scopesReview 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 introducersAudit 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.
Patch
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();
Regression Test / PoC
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();
Original Bug Report
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.