CVE-2026-5279
Overview
Background
- `Processor`
- The V8 AST rewriter class (
AstVisitor<Processor>) insrc/parsing/rewriter.ccthat walks parsed statements and rewrites them so the last evaluated expression’s completion value is captured. - `CheckStackOverflow()`
- A recursion guard that reports whether the interpreter’s C++ stack limit has been exceeded during the deep
Visit()/Process()recursion over the AST. - `replacement_` / `is_set_`
- Mutable
Processormember state where each recursive visit records its rewritten node and whether a completion-value assignment was set, which the caller consumes immediately after the recursive call returns. - `VISIT_AND_RETURN_IF_STACK_OVERFLOW`
- A macro that runs a recursive
Visit()and then bails out of the current frame ifCheckStackOverflow()is true, preventing consumption of half-computed state.
Root Cause Analysis
The recursive rewriting path in Processor (VisitBlock, VisitIfStatement, VisitDoWhileStatement, VisitSwitchStatement, VisitWithStatement, VisitSloppyBlockFunctionStatement, and the loop in Process) called Visit() or Process() on child nodes and then unconditionally consumed the resulting replacement_ and is_set_ members (e.g. statements->Set(i, replacement_) or node->set_then_statement(replacement_)). When a deeply nested AST triggered a stack overflow mid-recursion, the recursive call returned early without producing valid output, yet the caller still read and stored the stale or partially-computed replacement_, violating the invariant that state is only consumed after a fully-completed recursive visit.
The fix introduces PROCESS_AND_RETURN_IF_STACK_OVERFLOW and replaces the bare Visit()/Process() calls with the *_AND_RETURN_IF_STACK_OVERFLOW macros so each frame immediately returns after a detected overflow instead of consuming results. It also reorders the check in the rewriter entry point so processor.HasStackOverflow() is tested before processor.result_assigned() is consulted, returning std::nullopt before any assigned result is used. Together these ensure that once overflow is reported, no frame builds a rewritten AST node out of invalid intermediate state.
replacement_/is_set_ state without first checking whether that visit had bailed out on stack overflow; the fix makes every recursive Visit()/Process() call return immediately on overflow so corrupted intermediate state is never woven into the rewritten AST.Attack Path
- Craft deeply nested script
An attacker supplies JavaScript with sufficiently deep nesting of blocks,
if/switch/with/do-while statements to drive theProcessorrecursion toward the C++ stack limit. - Trigger overflow mid-rewrite
During completion-value rewriting a recursive
Visit()/Process()call hitsCheckStackOverflow(), returning without having produced a validreplacement_. - Consume stale state
The parent frame unconditionally stores the stale/partial
replacement_(e.g. viastatements->Setorset_then_statement), splicing an inconsistent node into the AST. - Produce corrupted AST The rewriter returns a result built from half-unwound recursion state, yielding a malformed AST that downstream compilation consumes.
- Achieve object corruption Later execution of the corrupted program state produces object corruption in V8, giving the attacker a memory-safety primitive from a single malicious script.
Impact Assessment
Processor, and the corruption occurs in the sandboxed renderer process. Such object corruption is a strong primitive that can be leveraged toward memory disclosure or control-flow hijacking within the renderer.Changed Functions
| Function | Change | Notes |
|---|---|---|
Processorsrc/parsing/rewriter.cc |
modified |
Files Changed
src/parsing/rewriter.cc
Audit Directions
- Recursive AST visitorsAudit other
AstVisitorsubclasses that recurse and then read shared mutable state (likereplacement_) after the recursive call, ensuring they bail out whenCheckStackOverflow()orHasStackOverflow()is set. - Check-after-consume orderingLook for code that consumes a recursive result before testing its overflow/error flag; the error or overflow check must precede any use of the produced state.
- Bare Visit/Process callsGrep for direct
Visit()/Process()calls in parser and compiler passes that are not guarded by the*_AND_RETURN_IF_STACK_OVERFLOWmacros and could splice partial state into an output structure.
Patch
From c29f676270548a5b042a7b203b7c61e2808f0f09 Mon Sep 17 00:00:00 2001
From: Toon Verwaest <verwaest@chromium.org>
Date: Fri, 13 Mar 2026 10:48:08 +0100
Subject: [PATCH] [parser] Fix stack overflow in the rewriter
Bug: 490642836
Change-Id: Iaab7be8083e14c44867a68eb5d52c9dcb5b78e50
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7666017
Reviewed-by: Igor Sheludko <ishell@chromium.org>
Auto-Submit: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Toon Verwaest <verwaest@chromium.org>
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105783}
---
diff --git a/src/parsing/rewriter.cc b/src/parsing/rewriter.cc
index 3ed2e76..905eac4 100644
--- a/src/parsing/rewriter.cc
+++ b/src/parsing/rewriter.cc
@@ -20,6 +20,12 @@
Visit(param); \
if (CheckStackOverflow()) return;
+// Use this macro when a recursive Process() call may mutate state that would be
+// invalid to consume once stack overflow has been reported.
+#define PROCESS_AND_RETURN_IF_STACK_OVERFLOW(param) \
+ Process(param); \
+ if (CheckStackOverflow()) return;
+
namespace v8::internal {
class Processor final : public AstVisitor<Processor> {
@@ -124,7 +130,7 @@
// early.
for (int i = statements->length() - 1; i >= 0 && (breakable_ || !is_set_);
--i) {
- Visit(statements->at(i));
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(statements->at(i));
statements->Set(i, replacement_);
}
}
@@ -141,7 +147,7 @@
// to prevent rewriting in that case.
if (!node->ignore_completion_value()) {
BreakableScope scope(this, node->is_breakable());
- Process(node->statements());
+ PROCESS_AND_RETURN_IF_STACK_OVERFLOW(node->statements());
}
replacement_ = node;
}
@@ -161,12 +167,12 @@
// Rewrite both branches.
bool set_after = is_set_;
- Visit(node->then_statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->then_statement());
node->set_then_statement(replacement_);
bool set_in_then = is_set_;
is_set_ = set_after;
- Visit(node->else_statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->else_statement());
node->set_else_statement(replacement_);
replacement_ = set_in_then && is_set_ ? node : AssignUndefinedBefore(node);
@@ -182,7 +188,7 @@
DCHECK(breakable_ || !is_set_);
BreakableScope scope(this);
- Visit(node->body());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->body());
node->set_body(replacement_);
replacement_ = AssignUndefinedBefore(node);
@@ -297,7 +303,7 @@
ZonePtrList<CaseClause>* clauses = node->cases();
for (int i = clauses->length() - 1; i >= 0; --i) {
CaseClause* clause = clauses->at(i);
- Process(clause->statements());
+ PROCESS_AND_RETURN_IF_STACK_OVERFLOW(clause->statements());
}
replacement_ = AssignUndefinedBefore(node);
@@ -318,7 +324,7 @@
void Processor::VisitWithStatement(WithStatement* node) {
- Visit(node->statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->statement());
node->set_statement(replacement_);
replacement_ = is_set_ ? node : AssignUndefinedBefore(node);
@@ -328,7 +334,7 @@
void Processor::VisitSloppyBlockFunctionStatement(
SloppyBlockFunctionStatement* node) {
- Visit(node->statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->statement());
node->set_statement(replacement_);
replacement_ = node;
}
@@ -417,6 +423,11 @@
result, info->ast_value_factory(), info->zone());
processor.Process(body);
+ if (processor.HasStackOverflow()) {
+ *out_has_stack_overflow = true;
+ return std::nullopt;
+ }
+
if (processor.result_assigned()) {
int pos = kNoSourcePosition;
VariableProxy* result_value =
@@ -429,15 +440,11 @@
}
return result_value;
}
-
- if (processor.HasStackOverflow()) {
- *out_has_stack_overflow = true;
- return std::nullopt;
- }
}
return nullptr;
}
+#undef PROCESS_AND_RETURN_IF_STACK_OVERFLOW
#undef VISIT_AND_RETURN_IF_STACK_OVERFLOW
} // namespace v8::internal
Original Bug Report
Rewriter stack overflow can corrupt AST and can cause wrong-scope bytecode generation
VULNERABILITY DETAILS
Summary
Processor::Process trusts replacement_ from visits even when stack overflow occurred, then it can lead to corrupted AST and bytecode generator consuming it.
Root cause analysis
// src/ast/ast.h
#define DEFINE_AST_VISITOR_SUBCLASS_MEMBERS() \
public: \
void VisitNoStackOverflowCheck(AstNode* node) { \
GENERATE_AST_VISITOR_SWITCH() \
} \
\
void Visit(AstNode* node) { \
if (CheckStackOverflow()) return; \
VisitNoStackOverflowCheck(node); \
} \
\
void SetStackOverflow() { stack_overflow_ = true; } \
void ClearStackOverflow() { stack_overflow_ = false; } \
bool HasStackOverflow() const { return stack_overflow_; } \
\
bool CheckStackOverflow() { \
if (stack_overflow_) return true; \
if (GetCurrentStackPosition() < stack_limit_) { \
stack_overflow_ = true; \
return true; \
} \
return false; \
} \
...
The generic visitor sets a sticky flag and silently returns when stack overflow occurs.
// src/parsing/rewriter.cc
void Processor::VisitBlock(Block* node) {
if (!node->ignore_completion_value()) {
BreakableScope scope(this, node->is_breakable());
Process(node->statements());
}
replacement_ = node;
}
Processor::VisitBlock unconditionally publishes the current block as replacement_ even if an inner Process(node->statements()); can overflow.
// src/parsing/rewriter.cc
void Processor::Process(ZonePtrList<Statement>* statements) {
for (int i = statements->length() - 1; i >= 0 && (breakable_ || !is_set_);
--i) {
Visit(statements->at(i));
statements->Set(i, replacement_);
}
}
Processor::Process trusts replacement_ from Visit.
// src/parsing/rewriter.cc
// Assumes code has been parsed. Mutates the AST, so the AST should not
// continue to be used in the case of failure.
bool Rewriter::Rewrite(ParseInfo* info, bool* out_has_stack_overflow) {
RCS_SCOPE(info->runtime_call_stats(),
RuntimeCallCounterId::kCompileRewriteReturnResult,
RuntimeCallStats::kThreadSpecific);
FunctionLiteral* function = info->literal();
DCHECK_NOT_NULL(function);
Scope* scope = function->scope();
DCHECK_NOT_NULL(scope);
DCHECK_EQ(scope, scope->GetClosureScope());
if (scope->is_repl_mode_scope() ||
!(scope->is_script_scope() || scope->is_eval_scope())) {
return true;
}
ZonePtrList<Statement>* body = function->body();
return RewriteBody(info, scope, body, out_has_stack_overflow).has_value();
}
std::optional<VariableProxy*> Rewriter::RewriteBody(
ParseInfo* info, Scope* scope, ZonePtrList<Statement>* body,
bool* out_has_stack_overflow) {
DisallowGarbageCollection no_gc;
DisallowHandleAllocation no_handles;
DisallowHandleDereference no_deref;
if (!body->is_empty()) {
Variable* result = scope->AsDeclarationScope()->NewTemporary(
info->ast_value_factory()->dot_result_string());
Processor processor(info->stack_limit(), scope->AsDeclarationScope(),
result, info->ast_value_factory(), info->zone());
processor.Process(body);
if (processor.result_assigned()) {
int pos = kNoSourcePosition;
VariableProxy* result_value =
processor.factory()->NewVariableProxy(result, pos);
if (!info->flags().is_repl_mode()) {
Statement* result_statement;
result_statement =
processor.factory()->NewReturnStatement(result_value, pos);
body->Add(result_statement, info->zone());
}
return result_value;
}
if (processor.HasStackOverflow()) {
*out_has_stack_overflow = true;
return std::nullopt;
}
}
return nullptr;
}
Rewriter::RewriteBody trusts that stack overflow did not happen if processor.result_assigned() returns true.
// src/parsing/rewriter.cc
void Processor::VisitIfStatement(IfStatement* node) {
// Rewrite both branches.
bool set_after = is_set_;
Visit(node->then_statement()); // successful
node->set_then_statement(replacement_);
bool set_in_then = is_set_;
is_set_ = set_after;
Visit(node->else_statement()); // later overflows
node->set_else_statement(replacement_);
replacement_ = set_in_then && is_set_ ? node : AssignUndefinedBefore(node);
is_set_ = true;
}
But result_assigned_ can be true when Processor::VisitIfStatement visits then branch successfully and stack overflow occurs in else branch.
And AstVisitor::Visit does not roll back result_assigned_.
So by the time it gets back to Rewriter::RewriteBody, both can be true at once:
processor.result_assigned()== trueprocessor.HasStackOverflow()== true
Eventually Rewriter::Rewrite can return safely with corrupted AST.
// src/interpreter/bytecode-generator.cc
int BytecodeGenerator::GetNewClosureSlot(FunctionLiteral* literal) {
DCHECK_EQ(feedback_slot_cache()->Get(
FeedbackSlotCache::SlotKind::kClosureFeedbackCell, literal),
-1); // dcheck failure
...
#ifdef DEBUG
feedback_slot_cache()->Put(
FeedbackSlotCache::SlotKind::kClosureFeedbackCell, literal, index);
#endif
return index;
}
Then previous sate causes the same FunctionLiteral* to become reachable twice during bytecode generation.
Here’s the exact execution of repro_carrier.js (used in reproduction):
Parser::PostProcessParseResult()callsRewriter::Rewrite.Rewriter::RewriteBody()callsprocessor.Process(body).- Rewriter handles the outer block’s statements from last to first.
VisitBlock()enters the block that containsfunction g() {}and theif (1) 0; else <deep switch chain>.- Inside that block,
VisitIfStatement()descends into theelseside and reaches nestedVisitSwitchStatement()/Process(clause->statements())recursion. AstVisitor::Visit()hits the native stack limit and flips stickystack_overflow_.- Execution returns to
VisitBlock(), which still doesreplacement_ = node. Process()then moves to the0;statement, butVisit(0;)immediately returns because the overflow flag is already set.- So
statements->Set(i, replacement_)replaces that earlier0;statement with the old block still stored inreplacement_. RewriteBody()can still return success, and later bytecode generation revisits the sameFunctionLiteral*.
Impact
The malformed AST may cause miscompilation, where bytecode is emitted for a node under the wrong lexical scope, leading to wrong context-slot reads/writes and potentially type confusion.
VERSION
tested v8 git commit : f9e2258a2c6c7aa50372c496f75db8ddcecff25f
v8 git commit that introduces this bug:
commit af33cccfc4b8b806cbc54a1c72a6b5682312df8d
Author: neis <neis@chromium.org>
Date: Thu Oct 1 02:06:06 2015 -0700
Enable visitor in rewriter to replace statements.
This is in preparation of implementing ES6 completion semantics and
depends on #1362333002.
R=rossberg
BUG=
Review URL: https://codereview.chromium.org/1362363002
Cr-Commit-Position: refs/heads/master@{#31041}
Release tags containing the root-cause commit (from git -C /home/slave/v8-bug-bounty/v8_latest/v8 tag --contains af33cccfc4b8b806cbc54a1c72a6b5682312df8d):
- Earliest local numeric release tag containing the root-cause commit: 4.7.84
- Latest local numeric release tag containing the root-cause commit: 14.7.168
REPRODUCTION CASE
gn args out/x64.release_asan_ubsan:
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true
v8_enable_backtrace = true
dcheck_always_on = true
symbol_level = 2
is_clang = true
is_asan = true
is_ubsan = true
is_ubsan_no_recover = false
Execute:
./out/x64.release_asan_ubsan/d8 --stack-size=128 repro_carrier.js -- outer_dup_donor_decl_switch 158 158 1
Result:
TRY outer_dup_donor_decl_switch n=158 len=4823
#
# Fatal error in ../../src/interpreter/bytecode-generator.cc, line 9047
# Debug check failed: feedback_slot_cache()->Get( FeedbackSlotCache::SlotKind::kClosureFeedbackCell, literal) == -1 (0 vs. -1).
#
#
#
#FailureMessage Object: 0x7bfff5fdc060
==== C stack trace ===============================
out/x64.release_asan_ubsan/d8(__interceptor_backtrace+0x46) [0x55555cf729f6]
out/x64.release_asan_ubsan/d8(v8::base::debug::StackTrace::StackTrace()+0x34) [0x555566dc00b4]
out/x64.release_asan_ubsan/d8(+0x11866ef3) [0x555566dbaef3]
out/x64.release_asan_ubsan/d8(V8_Fatal(char const*, int, char const*, ...)+0x2ce) [0x555566d6d636]
out/x64.release_asan_ubsan/d8(+0x118180ec) [0x555566d6c0ec]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::GetNewClosureSlot(v8::internal::FunctionLiteral*)+0x313) [0x55555ed5bda3]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitFunctionLiteral(v8::internal::FunctionLiteral*)+0x2b0) [0x55555ed55c40]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitFunctionDeclaration(v8::internal::FunctionDeclaration*)+0x202) [0x55555ed55102]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitDeclarations(v8::base::ThreadedListBase<v8::internal::Declaration, v8::base::EmptyBase, v8::base::ThreadedListTraits<v8::internal::Declaration>, false>*)+0x16b) [0x55555ed4d66b]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockDeclarationsAndStatements(v8::internal::Block*)+0x22c) [0x55555ed50d2c]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockMaybeDispose(v8::internal::Block*)+0x2bc) [0x55555ed52c3c]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlock(v8::internal::Block*)+0x1fd) [0x55555ed521ad]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitStatements(v8::internal::ZoneList<v8::internal::Statement*> const*, int)+0x568) [0x55555ed51808]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockDeclarationsAndStatements(v8::internal::Block*)+0x3cf) [0x55555ed50ecf]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockMaybeDispose(v8::internal::Block*)+0x2bc) [0x55555ed52c3c]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlock(v8::internal::Block*)+0x1fd) [0x55555ed521ad]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::VisitStatements(v8::internal::ZoneList<v8::internal::Statement*> const*, int)+0x568) [0x55555ed51808]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBodyStatementsWithoutImplicitFinalReturn(int)+0x312) [0x55555ed4ee02]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBodyStatements(int)+0x16) [0x55555ed4b3d6]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBytecode(unsigned long)+0x5c8) [0x55555ed43c88]
out/x64.release_asan_ubsan/d8(v8::internal::interpreter::InterpreterCompilationJob::ExecuteJobImpl()+0xa1c) [0x55555ee1f24c]
out/x64.release_asan_ubsan/d8(v8::internal::UnoptimizedCompilationJob::ExecuteJob()+0x288) [0x55555d9970c8]
out/x64.release_asan_ubsan/d8(+0x84b3eae) [0x55555da07eae]
out/x64.release_asan_ubsan/d8(+0x8469d65) [0x55555d9bdd65]
out/x64.release_asan_ubsan/d8(+0x846eefb) [0x55555d9c2efb]
out/x64.release_asan_ubsan/d8(v8::internal::Compiler::GetFunctionFromEval(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::String>, v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::DirectHandle<v8::internal::Context>, v8::internal::LanguageMode, v8::internal::ParseRestriction, int, int, v8::internal::ParsingWhileDebugging)+0x15c8) [0x55555d9c9510]
out/x64.release_asan_ubsan/d8(+0xa9aad50) [0x55555fefed50]
out/x64.release_asan_ubsan/d8(v8::internal::Runtime_ResolvePossiblyDirectEval(int, unsigned long*, v8::internal::Isolate*)+0x1b6) [0x55555fefdc4e]
out/x64.release_asan_ubsan/d8(+0x11334a78) [0x555566888a78]
[1] 1452834 trace trap out/x64.release_asan_ubsan/d8 --stack-size=128 -- outer_dup_donor_decl_switc
another example of reproducible binary:
gn args out/x64.release_asan_no_inline:
is_debug = false
target_cpu = "x64"
v8_enable_sandbox = true
v8_enable_backtrace = true
dcheck_always_on = true
symbol_level = 2
is_clang = true
is_asan = true
v8_no_inline = true
Execute:
./out/x64.release_asan_no_inline/d8 --stack-size=128 repro_carrier.js -- outer_dup_donor_decl_switch 188 188 1
Result:
TRY outer_dup_donor_decl_switch n=188 len=5723
#
# Fatal error in ../../src/interpreter/bytecode-generator.cc, line 9047
# Debug check failed: feedback_slot_cache()->Get( FeedbackSlotCache::SlotKind::kClosureFeedbackCell, literal) == -1 (0 vs. -1).
#
#
#
#FailureMessage Object: 0x7bfff5dabc60
==== C stack trace ===============================
./out/x64.release_asan_no_inline/d8(__interceptor_backtrace+0x46) [0x5555608239f6]
./out/x64.release_asan_no_inline/d8(v8::base::debug::StackTrace::StackTrace()+0x34) [0x555568257554]
./out/x64.release_asan_no_inline/d8(+0x12cfcbdb) [0x555568250bdb]
./out/x64.release_asan_no_inline/d8(V8_Fatal(char const*, int, char const*, ...)+0x271) [0x555568218671]
./out/x64.release_asan_no_inline/d8(+0x12cc3e3f) [0x555568217e3f]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::GetNewClosureSlot(v8::internal::FunctionLiteral*)+0x5c) [0x555561ef06dc]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitFunctionLiteral(v8::internal::FunctionLiteral*)+0x1e2) [0x555561eed922]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitFunctionDeclaration(v8::internal::FunctionDeclaration*)+0x11b) [0x555561eed48b]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitDeclarations(v8::base::ThreadedListBase<v8::internal::Declaration, v8::base::EmptyBase, v8::base::ThreadedListTraits<v8::internal::Declaration>, false>*)+0x153) [0x555561ee9ce3]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockDeclarationsAndStatements(v8::internal::Block*)+0x157) [0x555561eeb677]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockMaybeDispose(v8::internal::Block*)+0x122) [0x555561eeca82]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlock(v8::internal::Block*)+0x184) [0x555561eec644]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitStatements(v8::internal::ZoneList<v8::internal::Statement*> const*, int)+0x292) [0x555561eec0e2]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockDeclarationsAndStatements(v8::internal::Block*)+0x1cd) [0x555561eeb6ed]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlockMaybeDispose(v8::internal::Block*)+0x122) [0x555561eeca82]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitBlock(v8::internal::Block*)+0x184) [0x555561eec644]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::VisitStatements(v8::internal::ZoneList<v8::internal::Statement*> const*, int)+0x292) [0x555561eec0e2]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBodyStatementsWithoutImplicitFinalReturn(int)+0x1ac) [0x555561eeac6c]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBodyStatements(int)+0xf) [0x555561ee900f]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::BytecodeGenerator::GenerateBytecode(unsigned long)+0x333) [0x555561ee6b63]
./out/x64.release_asan_no_inline/d8(+0xca12fc7) [0x555561f66fc7]
./out/x64.release_asan_no_inline/d8(v8::internal::interpreter::InterpreterCompilationJob::ExecuteJobImpl()+0x2b5) [0x555561f669a5]
./out/x64.release_asan_no_inline/d8(v8::internal::UnoptimizedCompilationJob::ExecuteJob()+0x167) [0x55556105b7e7]
./out/x64.release_asan_no_inline/d8(+0xbb684e1) [0x5555610bc4e1]
./out/x64.release_asan_no_inline/d8(+0xbb1febb) [0x555561073ebb]
./out/x64.release_asan_no_inline/d8(+0xbb231dc) [0x5555610771dc]
./out/x64.release_asan_no_inline/d8(v8::internal::Compiler::GetFunctionFromEval(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::String>, v8::internal::DirectHandle<v8::internal::SharedFunctionInfo>, v8::internal::DirectHandle<v8::internal::Context>, v8::internal::LanguageMode, v8::internal::ParseRestriction, int, int, v8::internal::ParsingWhileDebugging)+0x12e3) [0x55556107b133]
./out/x64.release_asan_no_inline/d8(+0xd68d446) [0x555562be1446]
./out/x64.release_asan_no_inline/d8(+0xd6879aa) [0x555562bdb9aa]
./out/x64.release_asan_no_inline/d8(v8::internal::Runtime_ResolvePossiblyDirectEval(int, unsigned long*, v8::internal::Isolate*)+0x19d) [0x555562bdb28d]
./out/x64.release_asan_no_inline/d8(+0x127dfa78) [0x555567d33a78]
[1] 893216 trace trap ./out/x64.release_asan_no_inline/d8 --stack-size=128 -- 188 188 1
Type of crash: dcheck failure
Suggesting Fix
suggest.patch:
diff --git a/src/parsing/rewriter.cc b/src/parsing/rewriter.cc
index 3ed2e764bfa..905eac437cc 100644
--- a/src/parsing/rewriter.cc
+++ b/src/parsing/rewriter.cc
@@ -20,6 +20,12 @@
Visit(param); \
if (CheckStackOverflow()) return;
+// Use this macro when a recursive Process() call may mutate state that would be
+// invalid to consume once stack overflow has been reported.
+#define PROCESS_AND_RETURN_IF_STACK_OVERFLOW(param) \
+ Process(param); \
+ if (CheckStackOverflow()) return;
+
namespace v8::internal {
class Processor final : public AstVisitor<Processor> {
@@ -124,7 +130,7 @@ void Processor::Process(ZonePtrList<Statement>* statements) {
// early.
for (int i = statements->length() - 1; i >= 0 && (breakable_ || !is_set_);
--i) {
- Visit(statements->at(i));
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(statements->at(i));
statements->Set(i, replacement_);
}
}
@@ -141,7 +147,7 @@ void Processor::VisitBlock(Block* node) {
// to prevent rewriting in that case.
if (!node->ignore_completion_value()) {
BreakableScope scope(this, node->is_breakable());
- Process(node->statements());
+ PROCESS_AND_RETURN_IF_STACK_OVERFLOW(node->statements());
}
replacement_ = node;
}
@@ -161,12 +167,12 @@ void Processor::VisitIfStatement(IfStatement* node) {
// Rewrite both branches.
bool set_after = is_set_;
- Visit(node->then_statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->then_statement());
node->set_then_statement(replacement_);
bool set_in_then = is_set_;
is_set_ = set_after;
- Visit(node->else_statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->else_statement());
node->set_else_statement(replacement_);
replacement_ = set_in_then && is_set_ ? node : AssignUndefinedBefore(node);
@@ -182,7 +188,7 @@ void Processor::VisitIterationStatement(IterationStatement* node) {
DCHECK(breakable_ || !is_set_);
BreakableScope scope(this);
- Visit(node->body());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->body());
node->set_body(replacement_);
replacement_ = AssignUndefinedBefore(node);
@@ -297,7 +303,7 @@ void Processor::VisitSwitchStatement(SwitchStatement* node) {
ZonePtrList<CaseClause>* clauses = node->cases();
for (int i = clauses->length() - 1; i >= 0; --i) {
CaseClause* clause = clauses->at(i);
- Process(clause->statements());
+ PROCESS_AND_RETURN_IF_STACK_OVERFLOW(clause->statements());
}
replacement_ = AssignUndefinedBefore(node);
@@ -318,7 +324,7 @@ void Processor::VisitBreakStatement(BreakStatement* node) {
void Processor::VisitWithStatement(WithStatement* node) {
- Visit(node->statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->statement());
node->set_statement(replacement_);
replacement_ = is_set_ ? node : AssignUndefinedBefore(node);
@@ -328,7 +334,7 @@ void Processor::VisitWithStatement(WithStatement* node) {
void Processor::VisitSloppyBlockFunctionStatement(
SloppyBlockFunctionStatement* node) {
- Visit(node->statement());
+ VISIT_AND_RETURN_IF_STACK_OVERFLOW(node->statement());
node->set_statement(replacement_);
replacement_ = node;
}
@@ -417,6 +423,11 @@ std::optional<VariableProxy*> Rewriter::RewriteBody(
result, info->ast_value_factory(), info->zone());
processor.Process(body);
+ if (processor.HasStackOverflow()) {
+ *out_has_stack_overflow = true;
+ return std::nullopt;
+ }
+
if (processor.result_assigned()) {
int pos = kNoSourcePosition;
VariableProxy* result_value =
@@ -429,15 +440,11 @@ std::optional<VariableProxy*> Rewriter::RewriteBody(
}
return result_value;
}
-
- if (processor.HasStackOverflow()) {
- *out_has_stack_overflow = true;
- return std::nullopt;
- }
}
return nullptr;
}
+#undef PROCESS_AND_RETURN_IF_STACK_OVERFLOW
#undef VISIT_AND_RETURN_IF_STACK_OVERFLOW
} // namespace v8::internal
Run after patch:
➜ v8 git:(main) ✗ ./out/x64.release_asan_ubsan/d8 --stack-size=128 repro_carrier.js -- outer_dup_donor_decl_switch 158 158 1
TRY outer_dup_donor_decl_switch n=158 len=4823
STACK outer_dup_donor_decl_switch n=158: RangeError: Maximum call stack size exceeded
DONE outer_dup_donor_decl_switch
CREDIT INFORMATION
Reporter credit: Hyeonjun Ahn (@_deayzl)