Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in V8
DescriptionInappropriate implementation in V8
ComponentV8
Bug ClassLogic Error
Tracker485267831
Fix commitb07860d01595 (v8/v8) +21/-3
CISA KEVNot listed
Creditedqymag1c
Disclosed2026-03-03

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 AstTraversalVisitor or AstFunctionLiteralIdReindexer) 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 with HasStackOverflow(), indicating that recursive traversal hit the configured stack_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.

Key insight
The single core mistake was treating a stack-overflow abort as a silently ignorable condition, letting callers act on incompletely-traversed or unclear-state AST visitors; the fix makes overflow handling mandatory and explicit by forcing every caller to detect, clear, and propagate the condition, enforced by a destructor invariant check.

Attack Path

  1. 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().
  2. 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.
  3. Bypass the abort Because the caller does not check or clear the flag, the code continues past the aborted traversal (e.g. runs CheckVisited or returns true) operating on a partially processed AST.
  4. 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

An attacker delivering a specially nested script gains the ability to leave V8’s parser in an inconsistent internal state by having a stack-overflow abort go unacknowledged, an inappropriate-implementation logic error that executes in the renderer process where untrusted JavaScript runs. The only precondition is getting the victim to load attacker-controlled script (or, for the LiveEdit path, exercising the debugger’s live-edit flow) whose nesting depth exceeds the visitor’s stack_limit().

Files Changed

  • src/ast/ast-function-literal-id-reindexer.cc
  • src/ast/ast-traversal-visitor.h
  • src/debug/liveedit.cc
  • src/parsing/parser.cc

Audit Directions

  • Unchecked sticky error flags
    Search 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 loss
    Flag 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 abort
    Look for verification or DCHECK-style checks (like CheckVisited) that run unconditionally after a traversal which may have aborted early on stack overflow, and ensure they are gated on a clean state.
From b07860d0159521ebeaa4361700fe1cdae4818149 Mon Sep 17 00:00:00 2001
From: Igor Sheludko <ishell@chromium.org>
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 <ishell@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
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() {
Loading diff…

Original Bug Report

reported by qy...@gmail.com

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 \

View on issue tracker