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 <dmercadier@chromium.org>
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 <dmercadier@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Reviewed-by: Leszek Swirski <leszeks@chromium.org>
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