Chrome · V8
CVE-2026-79197
UAF in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/inspector/debugger/restart-frame/restart-osr-frame-expected.txt |
modified | |
fortest/inspector/debugger/restart-frame/restart-osr-frame.js |
modified | |
iftest/mjsunit/debug-osr-debug-break.js |
modified | |
fortest/mjsunit/debug-osr-debug-break.js |
modified | |
iftest/mjsunit/debug-osr-step-on-throw.js |
modified |
Files Changed
src/debug/debug.cctest/inspector/debugger/restart-frame/restart-osr-frame-expected.txttest/inspector/debugger/restart-frame/restart-osr-frame.jstest/mjsunit/debug-osr-debug-break.jstest/mjsunit/debug-osr-step-on-throw.js
Patch
From 429577a9437e269d4bbb5d529458a1e74643c856 Mon Sep 17 00:00:00 2001 From: Darius Mercadier <[email protected]> Date: Tue, 28 Jul 2026 12:58:59 +0200 Subject: [PATCH] [debug] Deoptimize OSR frames on debugger break and restart When Deoptimizer::DeoptimizeFunction is called without explicitly passing the target frame's Code object, it defaults to unction->code(isolate), which points to the function's default entrypoint code (e.g. unoptimized bytecode, baseline code, or non-OSR optimized code) rather than the OSR machine code executing on the stack frame. TAG=agy CONV=978a8699-6f5a-469d-a61e-d8cbf7aa4c0d Fixed: 539453394, 531297707 Change-Id: Ibb577dfd2169e4d76517bb661410ff5609fb2ad5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8159443 Auto-Submit: Darius Mercadier <[email protected]> Commit-Queue: Leszek Swirski <[email protected]> Reviewed-by: Leszek Swirski <[email protected]> Cr-Commit-Position: refs/heads/main@{#108910} --- diff --git a/src/debug/debug.cc b/src/debug/debug.cc index b239a5c..9ba9a7e 100644 --- a/src/debug/debug.cc +++ b/src/debug/debug.cc @@ -2911,8 +2911,8 @@ // caller frames are at a call site, which acts as a memory serialization // barrier, forcing them to reload all heap state upon return anyway. if (frame->is_optimized()) { - Deoptimizer::DeoptimizeFunction(*function, - LazyDeoptimizeReason::kDebugger); + Deoptimizer::DeoptimizeFunction( + *function, LazyDeoptimizeReason::kDebugger, frame->LookupCode()); } // kScheduled breaks are triggered by the stack check. While we could @@ -3434,7 +3434,8 @@ int inlined_frame_index) { if (frame->is_optimized()) { Deoptimizer::DeoptimizeFunction(frame->function(), - LazyDeoptimizeReason::kDebugger); + LazyDeoptimizeReason::kDebugger, + frame->LookupCode()); } thread_local_.restart_frame_id_ = frame->id(); diff --git a/test/inspector/debugger/restart-frame/restart-osr-frame-expected.txt b/test/inspector/debugger/restart-frame/restart-osr-frame-expected.txt new file mode 100644 index 0000000..f36a138 --- /dev/null +++ b/test/inspector/debugger/restart-frame/restart-osr-frame-expected.txt @@ -0,0 +1,18 @@ +Checks that restarting an OSR-optimized frame works. +Paused at (after evaluation): +function foo() { + #debugger; +} + +Pause stack: + foo:2 (canBeRestarted = true) + osr_caller:9 (canBeRestarted = true) + +Restarting osr_caller frame... +Restarting function "osr_caller" ... +Paused at (after restart): +function osr_caller() { + for (let i = #0; i < 1000; i++) { + if (i == 10) %OptimizeOsr(); + +Resuming... diff --git a/test/inspector/debugger/restart-frame/restart-osr-frame.js b/test/inspector/debugger/restart-frame/restart-osr-frame.js new file mode 100644 index 0000000..591d7fc --- /dev/null +++ b/test/inspector/debugger/restart-frame/restart-osr-frame.js @@ -0,0 +1,41 @@ +// 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: --allow-natives-syntax --turbofan --no-maglev + +const {session, contextGroup, Protocol} = + InspectorTest.start('Checks that restarting an OSR-optimized frame works.'); + +session.setupScriptMap(); + +contextGroup.addScript(` +function foo() { + debugger; +} + +function osr_caller() { + for (let i = 0; i < 1000; i++) { + if (i == 10) %OptimizeOsr(); + } + foo(); +} +`, 0, 0, 'test.js'); + +(async () => { + await Protocol.Debugger.enable(); + await Protocol.Runtime.enable(); + + const { callFrames } = await InspectorTest.evaluateAndWaitForPause( + '%PrepareFunctionForOptimization(osr_caller); osr_caller();'); + + InspectorTest.log('Restarting osr_caller frame...'); + await InspectorTest.restartFrameAndWaitForPause(callFrames, 1); + + InspectorTest.log('Resuming...'); + Protocol.Debugger.resume(); + await Protocol.Debugger.oncePaused(); + await Protocol.Debugger.resume(); + + InspectorTest.completeTest(); +})(); diff --git a/test/mjsunit/debug-osr-debug-break.js b/test/mjsunit/debug-osr-debug-break.js new file mode 100644 index 0000000..bd65449 --- /dev/null +++ b/test/mjsunit/debug-osr-debug-break.js @@ -0,0 +1,44 @@ +// 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: --allow-natives-syntax --enable-inspector --turbofan --no-maglev + +let msgId = 1; +function cmd(method, params) { + return JSON.stringify({id: msgId++, method: method, params: params || {}}); +} + +function receive(msg) { + let obj = JSON.parse(msg); + if (obj.method === "Debugger.paused") { + send(cmd("Debugger.resume")); + } +} + +let top_frame_status_after_break = -1; +function check_deopt() { + eval(""); + top_frame_status_after_break = %GetOptimizationStatus(osr_top); +} +%NeverOptimizeFunction(check_deopt); + +function osr_top() { + for (let i = 0; i < 20; i++) { + if (i === 10) { + %OptimizeOsr(); + } + } + debugger; + check_deopt(); +} + +send(cmd("Debugger.enable")); +%PrepareFunctionForOptimization(osr_top); +osr_top(); + +// kTopmostFrameIsTurboFanned is bit 11 (1 << 11 = 2048) of GetOptimizationStatus +const kTopmostFrameIsTurboFanned = 1 << 11; +const isTopFrameTurboFanned = (top_frame_status_after_break & kTopmostFrameIsTurboFanned) !== 0; + +assertFalse(isTopFrameTurboFanned, "OSR topmost frame should be deoptimized after debugger break"); diff --git a/test/mjsunit/debug-osr-step-on-throw.js b/test/mjsunit/debug-osr-step-on-throw.js new file mode 100644 index 0000000..de09c11 --- /dev/null +++ b/test/mjsunit/debug-osr-step-on-throw.js @@ -0,0 +1,58 @@ +// 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: --allow-natives-syntax --enable-inspector --turbofan --no-maglev + +let msgId = 1; +function cmd(method, params) { + return JSON.stringify({id: msgId++, method: method, params: params || {}}); +} + +let paused_locations = []; + +function receive(msg) { + let obj = JSON.parse(msg); + if (obj.method === "Debugger.paused") { + let fnName = obj.params.callFrames[0].functionName; + paused_locations.push(fnName); + if (fnName !== "caught_target") { + send(cmd("Debugger.stepInto")); + } else { + send(cmd("Debugger.resume")); + } + }
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/debug-osr-debug-break.js b/test/mjsunit/debug-osr-debug-break.js
new file mode 100644
index 0000000..bd65449
--- /dev/null
+++ b/test/mjsunit/debug-osr-debug-break.js
@@ -0,0 +1,44 @@
+// 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: --allow-natives-syntax --enable-inspector --turbofan --no-maglev
+
+let msgId = 1;
+function cmd(method, params) {
+ return JSON.stringify({id: msgId++, method: method, params: params || {}});
+}
+
+function receive(msg) {
+ let obj = JSON.parse(msg);
+ if (obj.method === "Debugger.paused") {
+ send(cmd("Debugger.resume"));
+ }
+}
+
+let top_frame_status_after_break = -1;
+function check_deopt() {
+ eval("");
+ top_frame_status_after_break = %GetOptimizationStatus(osr_top);
+}
+%NeverOptimizeFunction(check_deopt);
+
+function osr_top() {
+ for (let i = 0; i < 20; i++) {
+ if (i === 10) {
+ %OptimizeOsr();
+ }
+ }
+ debugger;
+ check_deopt();
+}
+
+send(cmd("Debugger.enable"));
+%PrepareFunctionForOptimization(osr_top);
+osr_top();
+
+// kTopmostFrameIsTurboFanned is bit 11 (1 << 11 = 2048) of GetOptimizationStatus
+const kTopmostFrameIsTurboFanned = 1 << 11;
+const isTopFrameTurboFanned = (top_frame_status_after_break & kTopmostFrameIsTurboFanned) !== 0;
+
+assertFalse(isTopFrameTurboFanned, "OSR topmost frame should be deoptimized after debugger break");
diff --git a/test/mjsunit/debug-osr-step-on-throw.js b/test/mjsunit/debug-osr-step-on-throw.js
new file mode 100644
index 0000000..de09c11
--- /dev/null
+++ b/test/mjsunit/debug-osr-step-on-throw.js
@@ -0,0 +1,58 @@
+// 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: --allow-natives-syntax --enable-inspector --turbofan --no-maglev
+
+let msgId = 1;
+function cmd(method, params) {
+ return JSON.stringify({id: msgId++, method: method, params: params || {}});
+}
+
+let paused_locations = [];
+
+function receive(msg) {
+ let obj = JSON.parse(msg);
+ if (obj.method === "Debugger.paused") {
+ let fnName = obj.params.callFrames[0].functionName;
+ paused_locations.push(fnName);
+ if (fnName !== "caught_target") {
+ send(cmd("Debugger.stepInto"));
+ } else {
+ send(cmd("Debugger.resume"));
+ }
+ }
+}
+
+function caught_target() {
+ eval("");
+ // Should step into here after exception unwinds to catch handler in osr_caller()
+}
+
+function thrower() {
+ eval("");
+ debugger;
+ throw new Error("test");
+}
+
+%NeverOptimizeFunction(thrower);
+%NeverOptimizeFunction(caught_target);
+
+function osr_caller() {
+ for (let i = 0; i < 20; i++) {
+ if (i === 10) {
+ %OptimizeOsr();
+ }
+ }
+ try {
+ thrower();
+ } catch (e) {
+ caught_target();
+ }
+}
+
+send(cmd("Debugger.enable"));
+%PrepareFunctionForOptimization(osr_caller);
+osr_caller();
+
+assertEquals(["thrower", "thrower", "osr_caller", "caught_target"], paused_locations);
diff --git a/test/mjsunit/debug-osr-step-out.js b/test/mjsunit/debug-osr-step-out.js
new file mode 100644
index 0000000..9044d75
--- /dev/null
+++ b/test/mjsunit/debug-osr-step-out.js
@@ -0,0 +1,52 @@
+// 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: --allow-natives-syntax --enable-inspector --turbofan --no-maglev
+
+let msgId = 1;
+function cmd(method, params) {
+ return JSON.stringify({id: msgId++, method: method, params: params || {}});
+}
+
+let paused_locations = [];
+function receive(msg) {
+ let obj = JSON.parse(msg);
+ if (obj.method === "Debugger.paused") {
+ let fnName = obj.params.callFrames[0].functionName;
+ paused_locations.push(fnName);
+ if (fnName === "foo") {
+ send(cmd("Debugger.stepOut"));
+ } else if (fnName === "osr_caller") {
+ send(cmd("Debugger.stepInto"));
+ } else {
+ send(cmd("Debugger.resume"));
+ }
+ }
+}
+
+function bar() {
+ // Should pause here when stepping out of foo() from osr_caller()
+}
+%NeverOptimizeFunction(bar);
+
+function foo() {
+ debugger;
+}
+%NeverOptimizeFunction(foo);
+
+function osr_caller() {
+ for (let i = 0; i < 20; i++) {
+ if (i === 10) {
+ %OptimizeOsr();
+ }
+ }
+ foo();
+ bar();
+}
+
+send(cmd("Debugger.enable"));
+%PrepareFunctionForOptimization(osr_caller);
+osr_caller();
+
+assertEquals(["foo", "osr_caller", "bar"], paused_locations);
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