Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in V8
DescriptionUse after free in V8
ComponentV8
Bug ClassUAF
Tracker531297707
Fix commit429577a9437e (v8/v8) +217/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
for
test/inspector/debugger/restart-frame/restart-osr-frame-expected.txt
modified
for
test/inspector/debugger/restart-frame/restart-osr-frame.js
modified
if
test/mjsunit/debug-osr-debug-break.js
modified
for
test/mjsunit/debug-osr-debug-break.js
modified
if
test/mjsunit/debug-osr-step-on-throw.js
modified

Files Changed

  • src/debug/debug.cc
  • test/inspector/debugger/restart-frame/restart-osr-frame-expected.txt
  • test/inspector/debugger/restart-frame/restart-osr-frame.js
  • test/mjsunit/debug-osr-debug-break.js
  • test/mjsunit/debug-osr-step-on-throw.js
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.