Chrome · V8
CVE-2026-17952
Logic Error in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/wasm/wasm-debug.cc |
modified | |
iftest/debugger/regress/regress-crbug-840288.js |
modified |
Files Changed
src/wasm/wasm-debug.cctest/debugger/debugger.statustest/debugger/regress/regress-crbug-1032042.jstest/debugger/regress/regress-crbug-840288.jstest/debugger/regress/wasm/regress-517316174.js
Patch
From f3488673f2e2ba94683dde07919d433be9a8ce0c Mon Sep 17 00:00:00 2001
From: Clemens Backes <clemensb@chromium.org>
Date: Wed, 03 Jun 2026 18:13:22 +0200
Subject: [PATCH] [wasm][debug] Fix return address calculation during OSR patching
In UpdateReturnAddresses, the topmost Wasm frame was unconditionally
treated as being at a breakpoint (using kAfterBreakpoint semantics).
However, the frame could also be at a standard call site (e.g. calling
into JS). If there is a dead breakpoint at the same byte offset as the
call site, kAfterBreakpoint incorrectly picks the dead breakpoint's
code offset, leading to a miscalculated return address.
This CL fixes this by checking if there is a WASM_DEBUG_BREAK frame
above the Wasm frame on the stack. If so, we are actually at a
breakpoint. Otherwise, we treat it as a regular call site.
Also consolidate Wasm debugger regression tests into a new directory
test/debugger/regress/wasm/ and update debugger.status accordingly.
R=jkummerow@chromium.org
Fixed: 517316174
Change-Id: Ib4718a863d5f358a78c500dec6af4a3ebacca981
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7894740
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107766}
---
diff --git a/src/wasm/wasm-debug.cc b/src/wasm/wasm-debug.cc
index 1ae12eb..bb8d6f6 100644
--- a/src/wasm/wasm-debug.cc
+++ b/src/wasm/wasm-debug.cc
@@ -725,37 +725,24 @@
// code. The frame layout itself should be independent of breakpoints.
void UpdateReturnAddresses(Isolate* isolate, WasmCode* new_code,
StackFrameId stepping_frame) {
- auto matches = [new_code](WasmFrame* frame) {
- return frame->native_module() == new_code->native_module() &&
- FrameSummary::GetTop(frame).AsWasm().function_index() ==
- static_cast<uint32_t>(new_code->index()) &&
- frame->wasm_code()->is_liftoff();
- };
-
- // 1. Find and handle the first Wasm frame (potential breakpoint resume).
- DebuggableStackFrameIterator it(isolate);
- while (!it.done() && !it.is_wasm()) it.Advance();
-
- if (!it.done()) {
- WasmFrame* frame = WasmFrame::cast(it.frame());
- // We still need the flooded function for stepping.
- if (frame->id() != stepping_frame &&
-#if V8_ENABLE_DRUMBRAKE
- // TODO(paolosev@microsoft.com) - Implement for Wasm interpreter.
- !it.is_wasm_interpreter_entry() &&
-#endif
- matches(frame)) {
- UpdateReturnAddress(frame, new_code, kAfterBreakpoint);
- }
- it.Advance();
- }
-
- // 2. Handle all remaining frames (always at call sites).
+ StackFrameIterator it(isolate);
for (; !it.done(); it.Advance()) {
- if (!it.is_wasm()) continue;
+ bool at_breakpoint = it.frame()->is_wasm_debug_break();
+ if (at_breakpoint) {
+ it.Advance();
+ CHECK(!it.done());
+ }
+ if (!it.frame()->is_wasm()) continue;
+#if V8_ENABLE_DRUMBRAKE
+ if (it.frame()->is_wasm_interpreter_entry()) continue;
+#endif
WasmFrame* frame = WasmFrame::cast(it.frame());
- if (!matches(frame)) continue;
- UpdateReturnAddress(frame, new_code, kAfterWasmCall);
+ if (frame->native_module() != new_code->native_module()) continue;
+ WasmCode* code = frame->wasm_code();
+ if (!code->is_liftoff() || code->index() != new_code->index()) continue;
+ if (frame->id() == stepping_frame) continue;
+ UpdateReturnAddress(frame, new_code,
+ at_breakpoint ? kAfterBreakpoint : kAfterWasmCall);
}
}
diff --git a/test/debugger/debugger.status b/test/debugger/debugger.status
index 3f1459c..492e1e7 100644
--- a/test/debugger/debugger.status
+++ b/test/debugger/debugger.status
@@ -131,8 +131,7 @@
# TODO(v8:7777): Change this once wasm is supported in jitless mode.
['not has_webassembly or variant == jitless', {
'debug/wasm/*': [SKIP],
- 'regress/regress-crbug-840288': [SKIP],
- 'regress/regress-crbug-1032042': [SKIP],
+ 'regress/wasm/*': [SKIP],
}], # not has_webassembly or variant == jitless
##############################################################################
diff --git a/test/debugger/regress/regress-crbug-1032042.js b/test/debugger/regress/regress-crbug-1032042.js
deleted file mode 100644
index b92735f..0000000
--- a/test/debugger/regress/regress-crbug-1032042.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2019 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.
-
-d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
-
-const Debug = new DebugWrapper();
-Debug.enable();
-
-// Record the ID of the first script reported. This is to ignore
-// the (now deprecated) fake scripts that are generated for every
-// Wasm module.
-let scriptId;
-Debug.setListener((eventType, execState, eventData, data) => {
- assertEquals(Debug.DebugEvent.AfterCompile, eventType);
- if (scriptId === undefined) scriptId = eventData.scriptId;
-});
-
-// Create a simple Wasm script, which will be caught by the event listener.
-const builder = new WasmModuleBuilder();
-builder.addFunction('sub', kSig_i_ii)
-// input is 2 args of type int and output is int
-.addBody([
- kExprLocalGet, 0, // local.get i0
- kExprLocalGet, 1, // local.get i1
- kExprI32Sub]) // i32.sub i0 i1
-.exportFunc();
-const instance = builder.instantiate();
-
-// By now we should have recorded the ID of the Wasm script above.
-assertNotEquals(undefined, scriptId);
-
-// Disable and re-enable the Debugger and collect the reported
-// script IDs.
-const scriptIds = new Set();
-Debug.disable();
-Debug.setListener((eventType, execState, eventData, data) => {
- assertEquals(Debug.DebugEvent.AfterCompile, eventType);
- scriptIds.add(eventData.scriptId);
-});
-Debug.enable();
-
-// Make sure the Wasm script was reported.
-assertTrue(scriptIds.has(scriptId));
diff --git a/test/debugger/regress/regress-crbug-840288.js b/test/debugger/regress/regress-crbug-840288.js
deleted file mode 100644
index 6d34928..0000000
--- a/test/debugger/regress/regress-crbug-840288.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2018 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.
-
-d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
-
-Debug = debug.Debug
-
-function listener(event, exec_state, event_data, data) {
- if (event == Debug.DebugEvent.AfterCompile) {
- // The actual source doesn't matter, just don't crash.
- var source = event_data.script().source();
- // Source will be empty for the script representing the entire module,
- // disassembly for the script representing just the function.
- assertTrue(source == "func $main\nend\n" || source == "");
- }
-};
-
-// Add the debug event listener.
-Debug.setListener(listener);
-
-var builder = new WasmModuleBuilder();
-builder.addFunction('main', kSig_v_v).addBody([]).exportFunc();
-var promise = WebAssembly.compile(builder.toBuffer());
-
-// Clear the debug listener only after the event fired.
-promise.then(() => Debug.setListener(null), assertUnreachable);
diff --git a/test/debugger/regress/wasm/regress-517316174.js b/test/debugger/regress/wasm/regress-517316174.js
new file mode 100644
index 0000000..23c398d
--- /dev/null
+++ b/test/debugger/regress/wasm/regress-517316174.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.
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const num_params = 100;
+// Use a large signature to maximize the call instruction size.
+const sig_idx = builder.addType(
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/debugger/regress/regress-crbug-1032042.js b/test/debugger/regress/regress-crbug-1032042.js
deleted file mode 100644
index b92735f..0000000
--- a/test/debugger/regress/regress-crbug-1032042.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2019 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.
-
-d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
-
-const Debug = new DebugWrapper();
-Debug.enable();
-
-// Record the ID of the first script reported. This is to ignore
-// the (now deprecated) fake scripts that are generated for every
-// Wasm module.
-let scriptId;
-Debug.setListener((eventType, execState, eventData, data) => {
- assertEquals(Debug.DebugEvent.AfterCompile, eventType);
- if (scriptId === undefined) scriptId = eventData.scriptId;
-});
-
-// Create a simple Wasm script, which will be caught by the event listener.
-const builder = new WasmModuleBuilder();
-builder.addFunction('sub', kSig_i_ii)
-// input is 2 args of type int and output is int
-.addBody([
- kExprLocalGet, 0, // local.get i0
- kExprLocalGet, 1, // local.get i1
- kExprI32Sub]) // i32.sub i0 i1
-.exportFunc();
-const instance = builder.instantiate();
-
-// By now we should have recorded the ID of the Wasm script above.
-assertNotEquals(undefined, scriptId);
-
-// Disable and re-enable the Debugger and collect the reported
-// script IDs.
-const scriptIds = new Set();
-Debug.disable();
-Debug.setListener((eventType, execState, eventData, data) => {
- assertEquals(Debug.DebugEvent.AfterCompile, eventType);
- scriptIds.add(eventData.scriptId);
-});
-Debug.enable();
-
-// Make sure the Wasm script was reported.
-assertTrue(scriptIds.has(scriptId));
diff --git a/test/debugger/regress/regress-crbug-840288.js b/test/debugger/regress/regress-crbug-840288.js
deleted file mode 100644
index 6d34928..0000000
--- a/test/debugger/regress/regress-crbug-840288.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2018 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.
-
-d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
-
-Debug = debug.Debug
-
-function listener(event, exec_state, event_data, data) {
- if (event == Debug.DebugEvent.AfterCompile) {
- // The actual source doesn't matter, just don't crash.
- var source = event_data.script().source();
- // Source will be empty for the script representing the entire module,
- // disassembly for the script representing just the function.
- assertTrue(source == "func $main\nend\n" || source == "");
- }
-};
-
-// Add the debug event listener.
-Debug.setListener(listener);
-
-var builder = new WasmModuleBuilder();
-builder.addFunction('main', kSig_v_v).addBody([]).exportFunc();
-var promise = WebAssembly.compile(builder.toBuffer());
-
-// Clear the debug listener only after the event fired.
-promise.then(() => Debug.setListener(null), assertUnreachable);
diff --git a/test/debugger/regress/wasm/regress-517316174.js b/test/debugger/regress/wasm/regress-517316174.js
new file mode 100644
index 0000000..23c398d
--- /dev/null
+++ b/test/debugger/regress/wasm/regress-517316174.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.
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+const num_params = 100;
+// Use a large signature to maximize the call instruction size.
+const sig_idx = builder.addType(
+ makeSig(new Array(num_params).fill(kWasmI64), []));
+const imp_idx = builder.addImport('m', 'js', sig_idx);
+builder.addTable(kWasmFuncRef, 1);
+builder.addActiveElementSegment(0, [kExprI32Const, 0], [imp_idx]);
+
+const func_main = builder.addFunction('main', kSig_v_v)
+ .addBody([
+ kExprNop, // Offset 1
+ ...new Array(num_params).fill([kExprI64Const, 0]).flat(),
+ kExprI32Const, 0, // table index
+ kExprCallIndirect, sig_idx, 0,
+ kExprI64Const, 0,
+ kExprDrop,
+ kExprI64Const, 0,
+ kExprDrop,
+ ])
+ .exportFunc();
+
+const instance = builder.instantiate({
+ m: {
+ js: (...args) => {
+ if (globalThis.recompiled) return;
+ globalThis.recompiled = true;
+
+ // Trigger UpdateReturnAddresses(isolate, ..., kAfterBreakpoint)
+ // for the topmost Wasm frame ('main') which is currently at the call
+ // site.
+ debug.Debug.clearBreakPoint(bp_call);
+
+ // Set a new breakpoint to force recompilation.
+ debug.Debug.setBreakPoint(instance.exports.main, 0, 0);
+ }
+ }
+});
+
+// Calculate the offset of the call_indirect instruction.
+// 1 (nop) + 100 * 2 (i64.const 0) + 2 (i32.const 0) = 203.
+const call_offset = 1 + num_params * 2 + 2;
+
+// Breakpoint 1: At nop (offset 1). This ensures 'UpdateReturnAddresses'
+// has some existing state to work with and triggers the buggy path.
+debug.Debug.setBreakPoint(instance.exports.main, 0, 1);
+
+// Breakpoint 2: Specifically at the call site.
+const bp_call =
+ debug.Debug.setBreakPoint(instance.exports.main, 0, call_offset);
+
+instance.exports.main();
diff --git a/test/debugger/regress/wasm/regress-crbug-1032042.js b/test/debugger/regress/wasm/regress-crbug-1032042.js
new file mode 100644
index 0000000..b92735f
--- /dev/null
+++ b/test/debugger/regress/wasm/regress-crbug-1032042.js
@@ -0,0 +1,44 @@
+// Copyright 2019 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.
+
+d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
+
+const Debug = new DebugWrapper();
+Debug.enable();
+
+// Record the ID of the first script reported. This is to ignore
+// the (now deprecated) fake scripts that are generated for every
+// Wasm module.
+let scriptId;
+Debug.setListener((eventType, execState, eventData, data) => {
+ assertEquals(Debug.DebugEvent.AfterCompile, eventType);
+ if (scriptId === undefined) scriptId = eventData.scriptId;
+});
+
+// Create a simple Wasm script, which will be caught by the event listener.
+const builder = new WasmModuleBuilder();
+builder.addFunction('sub', kSig_i_ii)
+// input is 2 args of type int and output is int
+.addBody([
+ kExprLocalGet, 0, // local.get i0
+ kExprLocalGet, 1, // local.get i1
+ kExprI32Sub]) // i32.sub i0 i1
+.exportFunc();
+const instance = builder.instantiate();
+
+// By now we should have recorded the ID of the Wasm script above.
+assertNotEquals(undefined, scriptId);
+
+// Disable and re-enable the Debugger and collect the reported
+// script IDs.
+const scriptIds = new Set();
+Debug.disable();
+Debug.setListener((eventType, execState, eventData, data) => {
+ assertEquals(Debug.DebugEvent.AfterCompile, eventType);
+ scriptIds.add(eventData.scriptId);
+});
+Debug.enable();
+
+// Make sure the Wasm script was reported.
+assertTrue(scriptIds.has(scriptId));
diff --git a/test/debugger/regress/wasm/regress-crbug-840288.js b/test/debugger/regress/wasm/regress-crbug-840288.js
new file mode 100644
index 0000000..6d34928
--- /dev/null
+++ b/test/debugger/regress/wasm/regress-crbug-840288.js
@@ -0,0 +1,27 @@
+// Copyright 2018 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.
+
+d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
+
+Debug = debug.Debug
+
+function listener(event, exec_state, event_data, data) {
+ if (event == Debug.DebugEvent.AfterCompile) {
+ // The actual source doesn't matter, just don't crash.
+ var source = event_data.script().source();
+ // Source will be empty for the script representing the entire module,
+ // disassembly for the script representing just the function.
+ assertTrue(source == "func $main\nend\n" || source == "");
+ }
+};
+
+// Add the debug event listener.
+Debug.setListener(listener);
+
+var builder = new WasmModuleBuilder();
+builder.addFunction('main', kSig_v_v).addBody([]).exportFunc();
+var promise = WebAssembly.compile(builder.toBuffer());
+
+// Clear the debug listener only after the event fired.
+promise.then(() => Debug.setListener(null), assertUnreachable);
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