CVE-2026-3543
Overview
Background
- V8
- Chrome’s JavaScript and WebAssembly engine that parses, compiles, and executes untrusted script from web pages.
- AST visitor
- A traversal object (such as
AstTraversalVisitororAstFunctionLiteralIdReindexer) that walks the parser’s abstract syntax tree node by node. - Stack overflow state
- A per-visitor sticky flag, set via
set_stack_overflow()and queried withHasStackOverflow(), indicating that recursive traversal hit the configuredstack_limit(). - `ClearStackOverflow`
- The method that resets the sticky overflow flag so it is not left dangling for later reuse or for the object’s destruction.
Root Cause Analysis
Recursive AST visitors detect deep nesting by comparing against stack_limit() and setting an internal stack-overflow flag, but several call sites consumed these visitors without checking or clearing that flag. In AstFunctionLiteralIdReindexer::Reindex, CheckVisited was invoked unconditionally even after the traversal had aborted on overflow, and in Parser::BuildInitializationBlock / class-member handling the reindexer.HasStackOverflow() result was checked in one path but the flag was never cleared, while another Reindex(computed_name) path skipped the check entirely. Likewise LiveEdit’s CollectFunctionLiterals was run as a temporary and its overflow result was never inspected, so it silently returned success on a partially traversed tree. The violated invariant is that a visitor which raised stack overflow must have that state explicitly acknowledged and cleared before its results are trusted or the object is destroyed.
The fix threads HasStackOverflow()/ClearStackOverflow() checks through each call site, gates post-traversal validation (CheckVisited) on a clean state, and adds an AstTraversalVisitor destructor CHECK so any forgotten handling becomes a hard, deterministic crash rather than silent misbehavior.
Attack Path
- Craft deep nesting
An attacker authors JavaScript with a deeply nested destructuring pattern, class members, or expression tree designed to drive recursive AST traversal to the
stack_limit(). - Trigger a reindexing visitor
Parsing the pattern (or invoking LiveEdit’s
CollectFunctionLiterals) runs an AST visitor that hits stack overflow and sets its overflow flag mid-traversal. - Bypass the abort
Because the caller does not check or clear the flag, the code continues past the aborted traversal (e.g. runs
CheckVisitedor returnstrue) operating on a partially processed AST. - Reach inconsistent state The parser proceeds with function-literal IDs or literal collections that were never fully reindexed, leaving V8’s internal AST bookkeeping in an inappropriate, inconsistent state.
Impact Assessment
stack_limit().Files Changed
src/ast/ast-function-literal-id-reindexer.ccsrc/ast/ast-traversal-visitor.hsrc/debug/liveedit.ccsrc/parsing/parser.cc
Audit Directions
- Unchecked sticky error flagsSearch for visitor or helper objects that expose
HasStackOverflow()/set_stack_overflow()and confirm every caller checks the flag before trusting results and clears it before reuse or destruction. - Temporary-object result lossFlag call sites that construct a visitor as a temporary (
Visitor(...).Run(...)) where an error or overflow status set during the call can never be inspected afterward. - Post-traversal validation after abortLook for verification or
DCHECK-style checks (likeCheckVisited) that run unconditionally after a traversal which may have aborted early on stack overflow, and ensure they are gated on a clean state.
Patch
From b07860d0159521ebeaa4361700fe1cdae4818149 Mon Sep 17 00:00:00 2001 From: Igor Sheludko <[email protected]> Date: Thu, 19 Feb 2026 17:40:20 +0100 Subject: [PATCH] [parser] Make sure AST visitors handle stack overflow ... by enforcing them to explicitly clear the stack overflow state. Fixed: 485267831 Change-Id: I8bee4e55ceb3fac6536d88edf28b6196a204d5cc Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7594973 Commit-Queue: Igor Sheludko <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Cr-Commit-Position: refs/heads/main@{#105349} --- diff --git a/src/ast/ast-function-literal-id-reindexer.cc b/src/ast/ast-function-literal-id-reindexer.cc index f6ddef9..e60afaa 100644 --- a/src/ast/ast-function-literal-id-reindexer.cc +++ b/src/ast/ast-function-literal-id-reindexer.cc @@ -20,7 +20,9 @@ visited_.clear(); #endif Visit(pattern); - CheckVisited(pattern); + if (!CheckStackOverflow()) { + CheckVisited(pattern); + } } void AstFunctionLiteralIdReindexer::VisitFunctionLiteral(FunctionLiteral* lit) { @@ -115,6 +117,7 @@ } // namespace void AstFunctionLiteralIdReindexer::CheckVisited(Expression* expr) { + DCHECK(!HasStackOverflow()); AstFunctionLiteralIdReindexChecker(stack_limit(), &visited_).Visit(expr); } #endif diff --git a/src/ast/ast-traversal-visitor.h b/src/ast/ast-traversal-visitor.h index 4611a80..cfde1bf 100644 --- a/src/ast/ast-traversal-visitor.h +++ b/src/ast/ast-traversal-visitor.h @@ -31,6 +31,11 @@ public: explicit AstTraversalVisitor(Isolate* isolate, AstNode* root = nullptr); explicit AstTraversalVisitor(uintptr_t stack_limit, AstNode* root = nullptr); + ~AstTraversalVisitor() { + // This is a guard against forgotten handling of stack overflows, + // the visitor must call ClearStackOverflow() if it was handled. + CHECK_WITH_MSG(!HasStackOverflow(), "Unhandled stack overflow"); + } AstTraversalVisitor(const AstTraversalVisitor&) = delete; AstTraversalVisitor& operator=(const AstTraversalVisitor&) = delete; diff --git a/src/debug/liveedit.cc b/src/debug/liveedit.cc index bec0c37..5cbe2ac 100644 --- a/src/debug/liveedit.cc +++ b/src/debug/liveedit.cc @@ -563,8 +563,12 @@ result->status = debug::LiveEditResult::COMPILE_ERROR; return false; } - CollectFunctionLiterals(isolate, parse_info->literal()) - .Run(literals, eval_calls); + CollectFunctionLiterals visitor(isolate, parse_info->literal()); + visitor.Run(literals, eval_calls); + if (visitor.HasStackOverflow()) { + visitor.ClearStackOverflow(); + return false; + } return true; } diff --git a/src/parsing/parser.cc b/src/parsing/parser.cc index ad28d3f..0260cca 100644 --- a/src/parsing/parser.cc +++ b/src/parsing/parser.cc @@ -2741,6 +2741,7 @@ reindexer.Reindex(p->initializer()); } if (reindexer.HasStackOverflow()) { + reindexer.ClearStackOverflow(); set_stack_overflow(); return; } @@ -2752,6 +2753,11 @@ // name. AstFunctionLiteralIdReindexer reindexer(stack_limit_, 1); reindexer.Reindex(computed_name); + if (reindexer.HasStackOverflow()) { + reindexer.ClearStackOverflow(); + set_stack_overflow(); + return; + } } void Parser::PrepareGeneratorVariables() {
Original Bug Report
Debug check failed: visited_->find(lit) != visited_->end()
Steps to reproduce the problem
run with: d8 poc.js
Problem Description
Parser::ReindexComputedMemberName (src/parsing/parser.cc:2750) calls AstFunctionLiteralIdReindexer::Reindex (src/ast/ast-function-literal-id-reindexer.cc:18) to shift IDs in computed class member names. Unlike the arrow-parameter path (src/parsing/parser.cc:2734), this path does not check reindexer.HasStackOverflow() and does not propagate parser stack overflow.
The traversal uses AstTraversalVisitor, where Visit() exits early on stack limit (src/ast/ast.h:2988). So with deep AST + lazy compile from deep runtime call stack, reindexing may stop mid-tree after mutating only part of nodes.
That creates inconsistent metadata:
- Some FunctionLiteral::function_literal_id values are shifted, others are not.
- Some Call::eval_scope_info_index values are shifted, others are not.
- IDs collide in Script::infos() and can mix object kinds (SharedFunctionInfo vs ScopeInfo) at the same index.
This invariant break is directly observable in release runs:
- Script::FindSharedFunctionInfo consistency check fails (src/objects/script.cc:37), because wrong SFI is fetched for a literal ID (StartPosition mismatch).
- BytecodeGenerator::AllocateDeferredConstants fails (src/interpreter/bytecode-generator.cc:1625), because an eval scope-info slot resolves to an SFI instead of ScopeInfo.
In debug, the same root cause appears earlier as: src/ast/ast-function-literal-id-reindexer.cc:108 (visited_->find(lit) != visited_->end()), showing traversal incompleteness.
Summary
Debug check failed: visited_->find(lit) != visited_->end()
Custom Questions
Type of crash:
tab
Crash state:
#
# Fatal error in ../../src/ast/ast-function-literal-id-reindexer.cc, line 108
# Debug check failed: visited_->find(lit) != visited_->end().
#
#
#
#FailureMessage Object: 0x7ffcdd1f6f68
==== C stack trace ===============================
/home/qy/new5/v8/out/x64.debug/libv8_libbase.so(v8::base::debug::StackTrace::StackTrace()+0x29) [0x75a80d0e60e9]
/home/qy/new5/v8/out/x64.debug/libv8_libplatform.so(+0x4e29d) [0x75a80d04729d]
/home/qy/new5/v8/out/x64.debug/libv8_libbase.so(V8_Fatal(char const*, int, char const*, ...)+0x205) [0x75a80d0ba2f5]
/home/qy/new5/v8/out/x64.debug/libv8_libbase.so(+0x53b8c) [0x75a80d0b9b8c]
/home/qy/new5/v8/out/x64.debug/libv8_libbase.so(V8_Dcheck(char const*, int, char const*)+0x4d) [0x75a80d0ba3ed]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c3652) [0x75a8072c3652]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c013d) [0x75a8072c013d]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c25aa) [0x75a8072c25aa]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98c001f) [0x75a8072c001f]
/home/qy/new5/v8/out/x64.debug/libv8.so(+0x98bfbe2) [0x75a8072bfbe2]
Trace/breakpoint trap (core dumped)
Reporter credit:
qymag1c
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A \