CVE-2026-79045
Overview
Background
- `WasmFrame`
- V8 stack-frame abstraction for an executing WebAssembly function, exposing accessors such as
pc(),fp(), andcallee_fp(). - `WASM_DEBUG_BREAK` frame
- a synthetic stack frame pushed when execution stops at a Wasm breakpoint, whose frame pointer holds the spilled register values the debugger reads.
- `callee_fp()`
- the frame pointer of the frame called by the current
WasmFrame, which is only meaningful as a debug-break register source when the callee actually is aWASM_DEBUG_BREAKframe. - Frame type marker
- an
intptr_tstored atTypedFrameConstants::kFrameTypeOffsetinside a typed frame, comparable againstStackFrame::TypeToMarker(...)to identify the frame’s kind.
Root Cause Analysis
When the debugger inspected local and stack values for a paused Wasm frame, it passed frame->callee_fp() directly to debug_info->GetLocalValue and debug_info->GetStackValue as the debug_break_fp used to locate spilled register values. The code assumed the callee frame was always a WASM_DEBUG_BREAK frame, but when a trap occurs immediately after a breakpoint the callee frame can be an unrelated C++ frame with a different layout, violating the invariant that debug_break_fp points at a debug-break register save area. Register-backed WasmValues were then reconstructed by reading register offsets relative to that mismatched frame pointer, producing a type-confused read of arbitrary stack memory as a typed Wasm value.
The fix introduces GetDebugBreakFp, which reads the callee frame’s type marker and returns kNullAddress unless it equals the WASM_DEBUG_BREAK marker, so a non-debug-break callee no longer supplies a frame pointer. GetLocalValue/GetStackValue now additionally bail out and return an empty value when debug_break_fp == kNullAddress before dereferencing a register, closing the confusion.
callee_fp() to always denote a WASM_DEBUG_BREAK frame without validating its frame-type marker; the fix verifies that marker (GetDebugBreakFp) and refuses register retrieval when it is absent.Attack Path
- Set a Wasm breakpoint An attacker-controlled page installs a breakpoint in a WebAssembly module and drives execution to hit it, entering the debugger’s value-inspection path.
- Trigger a trap after the break
Craft the module so a trap fires immediately after the breakpoint, leaving a callee C++ frame that is not a
WASM_DEBUG_BREAKframe beneath the pausedWasmFrame. - Force value inspection
Cause the debugger to enumerate locals or stack slots, invoking
debug_info->GetLocalValue/GetStackValuewith the mismatchedframe->callee_fp()asdebug_break_fp. - Misread register slots
For a register-backed value,
GetLocalValuecomputes an address off the wrong frame pointer and reinterprets unrelated stack bytes as a typed Wasm value, achieving type confusion / out-of-bounds stack disclosure.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forsrc/debug/debug-wasm-objects.cc |
modified |
Files Changed
src/debug/debug-wasm-objects.ccsrc/wasm/wasm-debug.cc
Audit Directions
- Unvalidated frame pointersSearch for other debugger or stack-walker paths that consume
callee_fp(),caller_fp(), or rawfpvalues without first checking aStackFrametype marker. - Frame-type assumptionsReview code that assumes a specific adjacent frame layout (e.g.,
WASM_DEBUG_BREAK, builtin exit, or trap frames) and reads spilled registers or arguments by fixed offset without confirming the frame kind. - Register reconstructionAudit
WasmValue/LiftoffRegisterreconstruction sites that translate register codes to memory addresses relative to a caller-supplied base, ensuring each base is verified non-null and correctly typed before dereference.
Patch
From 01b5e5a2cda3d4d89434aa614753798b17bad438 Mon Sep 17 00:00:00 2001 From: Clemens Backes <[email protected]> Date: Wed, 08 Jul 2026 10:21:45 +0200 Subject: [PATCH] [wasm][debug] Fix trap frame confusion in Wasm debugger When a trap occurs immediately after a breakpoint, the debugger might attempt to inspect values from an unrelated C++ stack frame if it misidentifies the callee frame layout. This CL adds a verification step to ensure the callee frame is a valid WASM_DEBUG_BREAK frame before using its frame pointer for register retrieval. TAG=agy [email protected] Bug: 531245718 Change-Id: I2654d29e10d32b09891db443e016f1d606b8b572 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8053369 Commit-Queue: Clemens Backes <[email protected]> Reviewed-by: Simon Zünd <[email protected]> Cr-Commit-Position: refs/heads/main@{#108515} --- diff --git a/src/debug/debug-wasm-objects.cc b/src/debug/debug-wasm-objects.cc index 9ed9085..bbf988d 100644 --- a/src/debug/debug-wasm-objects.cc +++ b/src/debug/debug-wasm-objects.cc @@ -35,6 +35,17 @@ base::VectorOf(sb.start(), sb.length())); } +Address GetDebugBreakFp(WasmFrame* frame) { + Address callee_fp = frame->callee_fp(); + if (callee_fp == kNullAddress) return kNullAddress; + intptr_t marker = + base::Memory<intptr_t>(callee_fp + TypedFrameConstants::kFrameTypeOffset); + if (marker != StackFrame::TypeToMarker(StackFrame::WASM_DEBUG_BREAK)) { + return kNullAddress; + } + return callee_fp; +} + enum DebugProxyId { kFunctionsProxy, kGlobalsProxy, @@ -431,7 +442,7 @@ for (int i = 0; i < count; ++i) { auto value = WasmValueObject::New( isolate, debug_info->GetLocalValue(i, frame->pc(), frame->fp(), - frame->callee_fp(), isolate)); + GetDebugBreakFp(frame), isolate)); values->set(i, *value); } values->set(count + 0, frame->wasm_instance()->module_object()); @@ -478,7 +489,7 @@ for (int i = 0; i < count; ++i) { auto value = WasmValueObject::New( isolate, debug_info->GetStackValue(i, frame->pc(), frame->fp(), - frame->callee_fp(), isolate)); + GetDebugBreakFp(frame), isolate)); values->set(i, *value); } return IndexedDebugProxy::Create(isolate, values); diff --git a/src/wasm/wasm-debug.cc b/src/wasm/wasm-debug.cc index 0a1a6b8..4b2bd13 100644 --- a/src/wasm/wasm-debug.cc +++ b/src/wasm/wasm-debug.cc @@ -640,6 +640,7 @@ } if (value->is_register()) { + if (debug_break_fp == kNullAddress) return {}; auto reg = LiftoffRegister::from_liftoff_code(value->reg_code); auto gp_addr = [debug_break_fp](Register reg) { return debug_break_fp +