Chrome · DevTools › Debug
CVE-2026-106346
Input Validation in DevTools (Debug)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/debug/debug.cc |
modified | |
whiletest/inspector/debugger/pause-on-oom-in-eval.js |
modified | |
evaltest/inspector/debugger/pause-on-oom-in-eval.js |
modified |
Files Changed
test/inspector/debugger/pause-on-oom-in-eval.jstest/inspector/debugger/pause-on-oom-in-eval-expected.txtsrc/debug/debug-scopes.ccsrc/debug/debug.hsrc/debug/debug.cc
Patch
From 7075c436f40149aabd8b9ab8ad187f260450a1f4 Mon Sep 17 00:00:00 2001 From: Simon Zünd <[email protected]> Date: Wed, 30 Sep 2026 11:03:20 +0000 Subject: [PATCH] [debug] Step into function when OOM break fires at function entry When the inspector's near-heap-limit callback requests an OOM break and the interrupt fires at the function-entry stack check of a function or eval, the function's context has not been created and pushed yet. Pausing there makes the ScopeIterator observe an inconsistent context chain and trips a DCHECK. Handle kOOM breaks at function entry the same way as scheduled breaks: step into the function and pause at the first breakable position after the prologue. The original break reasons are preserved across the step so the pause is still reported as OOM. Also extend the ScopeIterator::NeedsContext invariant check to eval scopes. Fixed: 566404364 Change-Id: I8b58cd0cb8bdeab4385b4d68182642ab53e31df5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8480873 Reviewed-by: Yang Guo <[email protected]> Auto-Submit: Simon Zünd <[email protected]> Commit-Queue: Simon Zünd <[email protected]> Cr-Commit-Position: refs/heads/main@{#110193} --- diff --git a/src/debug/debug-scopes.cc b/src/debug/debug-scopes.cc index e2d3ab8..efb7922 100644 --- a/src/debug/debug-scopes.cc +++ b/src/debug/debug-scopes.cc @@ -274,7 +274,9 @@ // context should the function need one. In that case the function has already // pushed the context and we are good. CHECK_IMPLIES(needs_context && current_scope_index_ == closure_scope_index_ && - current_scope().is_function_scope() && !function_.is_null(), + (current_scope().is_function_scope() || + current_scope().is_eval_scope()) && + !function_.is_null(), function_->context() != *context_); return needs_context; diff --git a/src/debug/debug.cc b/src/debug/debug.cc index adac5a7..def10a5 100644 --- a/src/debug/debug.cc +++ b/src/debug/debug.cc @@ -514,7 +514,7 @@ base::Relaxed_Store(&thread_local_.current_debug_scope_, static_cast<base::AtomicWord>(0)); thread_local_.break_on_next_function_call_ = false; - thread_local_.scheduled_break_on_next_function_call_ = false; + thread_local_.scheduled_break_on_next_function_call_ = {}; UpdateHookOnFunctionCall(); thread_local_.muted_function_ = Smi::zero(); thread_local_.muted_position_ = -1; @@ -722,8 +722,9 @@ if (!break_points_hit.is_null() || break_on_next_function_call() || scheduled_break) { StepAction lastStepAction = last_step_action(); - debug::BreakReasons break_reasons; - if (scheduled_break) { + debug::BreakReasons break_reasons = + thread_local_.scheduled_break_on_next_function_call_; + if (shouldPauseAfterInstrumentation) { break_reasons.Add(debug::BreakReason::kScheduled); } // If it's a debugger statement, add the reason and then mute the location @@ -1710,7 +1711,7 @@ thread_local_.last_frame_count_ = -1; thread_local_.target_frame_count_ = -1; thread_local_.break_on_next_function_call_ = false; - thread_local_.scheduled_break_on_next_function_call_ = false; + thread_local_.scheduled_break_on_next_function_call_ = {}; clear_restart_frame(); UpdateHookOnFunctionCall(); } @@ -2936,14 +2937,15 @@ *function, LazyDeoptimizeReason::kDebugger, frame->LookupCode()); } - // kScheduled breaks are triggered by the stack check. While we could - // pause here, the JSFunction didn't have time yet to create and push - // it's context. Instead, we step into the function and pause at the + // kScheduled and kOOM breaks are triggered by the stack check. While we + // could pause here, the JSFunction didn't have time yet to create and + // push its context. Instead, we step into the function and pause at the // first official breakable position. // This behavior mirrors "BreakOnNextFunctionCall". - if (break_reasons.contains(v8::debug::BreakReason::kScheduled) && + if ((break_reasons.contains(v8::debug::BreakReason::kScheduled) || + break_reasons.contains(v8::debug::BreakReason::kOOM)) && BreakLocation::IsPausedInJsFunctionEntry(frame)) { - thread_local_.scheduled_break_on_next_function_call_ = true; + thread_local_.scheduled_break_on_next_function_call_.Add(break_reasons); PrepareStepIn(function); return; } diff --git a/src/debug/debug.h b/src/debug/debug.h index 7907de4..4a09380 100644 --- a/src/debug/debug.h +++ b/src/debug/debug.h @@ -458,7 +458,7 @@ } bool scheduled_break_on_function_call() const { - return thread_local_.scheduled_break_on_next_function_call_; + return !thread_local_.scheduled_break_on_next_function_call_.empty(); } bool IsRestartFrameScheduled() const { @@ -670,10 +670,10 @@ // debugger to break on next function call. bool break_on_next_function_call_; - // This flag is true when we break via stack check (BreakReason::kScheduled) - // We don't stay paused there but instead "step in" to the function similar - // to what "BreakOnNextFunctionCall" does. - bool scheduled_break_on_next_function_call_; + // Non-empty when we break via stack check (BreakReason::kScheduled or + // BreakReason::kOOM). We don't stay paused there but instead "step in" to + // the function similar to what "BreakOnNextFunctionCall" does. + v8::debug::BreakReasons scheduled_break_on_next_function_call_; // Frame ID for the frame that needs to be restarted. StackFrameId::NO_ID // otherwise. The unwinder uses the id to restart execution in this frame diff --git a/test/inspector/debugger/pause-on-oom-in-eval-expected.txt b/test/inspector/debugger/pause-on-oom-in-eval-expected.txt new file mode 100644 index 0000000..cddc096 --- /dev/null +++ b/test/inspector/debugger/pause-on-oom-in-eval-expected.txt @@ -0,0 +1,2 @@ +Tests that an OOM break triggered at the entry of a top-level strict eval pauses inside the eval +reason: OOM diff --git a/test/inspector/debugger/pause-on-oom-in-eval.js b/test/inspector/debugger/pause-on-oom-in-eval.js new file mode 100644 index 0000000..dbbfb20 --- /dev/null +++ b/test/inspector/debugger/pause-on-oom-in-eval.js @@ -0,0 +1,40 @@ +// 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. + +// Flags: --max-old-space-size=16 + +const {session, contextGroup, Protocol} = InspectorTest.start( + 'Tests that an OOM break triggered at the entry of a top-level strict ' + + 'eval pauses inside the eval'); + +Protocol.Debugger.onPaused(async message => { + InspectorTest.log(`reason: ${message.params.reason}`); + await Protocol.Debugger.evaluateOnCallFrame({ + callFrameId: message.params.callFrames[0].callFrameId, + expression: 'arr = []; stop = true;' + }); + Protocol.Debugger.resume(); +}); + +(async function test() { + await Protocol.Debugger.enable(); + // The loop body only allocates before entering the eval, so the OOM + // interrupt requested by the near-heap-limit callback fires at the + // function-entry stack check of the eval, i.e. before the eval context is + // pushed. The eval is called from the top level of the script so that the + // current context is the script context, whose ScopeInfo shares its unique + // ID with the eval scope. + await Protocol.Runtime.evaluate({ + expression: ` + 'use strict'; + let arr = []; + let stop = false; + while (!stop) { + arr.push(new Array(10000).fill(0)); + eval('let x = 1; function inner() { return x; }'); + } + //# sourceURL=test.js` + }); + InspectorTest.completeTest(); +})();
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page