Chrome · V8
CVE-2026-17920
UAF in V8
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/wasm/baseline/liftoff-assembler-inl.h |
modified | |
ifsrc/wasm/baseline/liftoff-assembler.cc |
modified |
Files Changed
src/wasm/baseline/liftoff-assembler-inl.hsrc/wasm/baseline/liftoff-assembler.ccsrc/wasm/baseline/liftoff-assembler.hsrc/wasm/baseline/liftoff-compiler.cc
Patch
From 3b8a85d10a8aa1e044938529e7a9fa64f822f6bb Mon Sep 17 00:00:00 2001
From: Clemens Backes <clemensb@chromium.org>
Date: Wed, 03 Jun 2026 13:32:01 +0200
Subject: [PATCH] [wasm] Reload cached memory start in out-of-line code
In Liftoff, the memory start address is often cached in a register for
performance. However, operations like memory.grow can move the memory,
invalidating this cached address. This is particularly problematic
during debugging, where breakpoint conditions might trigger memory
growth.
This CL ensures that the cached memory start register is reloaded after
any operation that might move memory, specifically during breakpoints,
stack checks, and tier-up checks.
As shared memory can never move, we could still push and pop the memory
start, but loading it from the instance avoids special code for
(non-movable) shared memory and saves a push / store to the stack.
R=mliedtke@chromium.org
Fixed: 513413942
Change-Id: I901d7dbed94919cc1009b6bcea2bf28772bb6595
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7864144
Reviewed-by: Matthias Liedtke <mliedtke@chromium.org>
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/main@{#107748}
---
diff --git a/src/wasm/baseline/liftoff-assembler-inl.h b/src/wasm/baseline/liftoff-assembler-inl.h
index 70d006e..6c2d9ee 100644
--- a/src/wasm/baseline/liftoff-assembler-inl.h
+++ b/src/wasm/baseline/liftoff-assembler-inl.h
@@ -310,6 +310,44 @@
#endif // V8_TARGET_ARCH_32_BIT
+void LiftoffAssembler::LoadMemoryStart(Register dst, Register instance_data,
+ int mem_index) {
+ if (mem_index == 0) {
+ LoadFromInstance(dst, instance_data,
+ WasmTrustedInstanceData::kMemory0StartOffset,
+ sizeof(size_t));
+ } else {
+ LoadProtectedPointer(
+ dst, instance_data,
+ WasmTrustedInstanceData::kProtectedMemoryBasesAndSizesOffset);
+ int buffer_offset = OFFSET_OF_DATA_START(TrustedFixedAddressArray) -
+ kHeapObjectTag + kSystemPointerSize * mem_index * 2;
+ LoadFullPointer(dst, dst, buffer_offset);
+ }
+}
+
+void LiftoffAssembler::RestoreCachedRegisters(Register instance_data,
+ bool reload_instance_data,
+ Register mem_start,
+ bool reload_mem_start,
+ int mem_index) {
+ if (reload_mem_start && instance_data == no_reg) {
+ // If there is no instance data available, load it first into the mem_start
+ // register.
+ DCHECK(!reload_instance_data);
+ instance_data = mem_start;
+ reload_instance_data = true;
+ }
+ if (reload_instance_data) {
+ DCHECK_NE(no_reg, instance_data);
+ LoadInstanceDataFromFrame(instance_data);
+ }
+ if (reload_mem_start) {
+ DCHECK_NE(no_reg, mem_start);
+ LoadMemoryStart(mem_start, instance_data, mem_index);
+ }
+}
+
// End of the partially platform-independent implementations of the
// platform-dependent part.
// =======================================================================
diff --git a/src/wasm/baseline/liftoff-assembler.cc b/src/wasm/baseline/liftoff-assembler.cc
index 9fae7fe..7124ca0 100644
--- a/src/wasm/baseline/liftoff-assembler.cc
+++ b/src/wasm/baseline/liftoff-assembler.cc
@@ -611,33 +611,9 @@
// Now execute stack transfers and register moves/loads.
parallel_move.Execute();
- if (reload_instance_data) {
- LoadInstanceDataFromFrame(target.cached_instance_data);
- }
- if (reload_mem_start) {
- // {target.cached_instance_data} already got restored above, so we can use
- // it if it exists.
- Register instance_data = target.cached_instance_data;
- if (instance_data == no_reg) {
- // We don't have the instance data available yet. Store it into the target
- // mem_start, so that we can load the mem0_start from there.
- instance_data = target.cached_mem_start;
- LoadInstanceDataFromFrame(instance_data);
- }
- if (target.cached_mem_index == 0) {
- LoadFromInstance(target.cached_mem_start, instance_data,
- WasmTrustedInstanceData::kMemory0StartOffset,
- sizeof(size_t));
- } else {
- LoadProtectedPointer(
- target.cached_mem_start, instance_data,
- WasmTrustedInstanceData::kProtectedMemoryBasesAndSizesOffset);
- int buffer_offset = OFFSET_OF_DATA_START(ByteArray) - kHeapObjectTag +
- kSystemPointerSize * target.cached_mem_index * 2;
- LoadFullPointer(target.cached_mem_start, target.cached_mem_start,
- buffer_offset);
- }
- }
+ RestoreCachedRegisters(target.cached_instance_data, reload_instance_data,
+ target.cached_mem_start, reload_mem_start,
+ target.cached_mem_index);
}
void LiftoffAssembler::Spill(VarState* slot) {
diff --git a/src/wasm/baseline/liftoff-assembler.h b/src/wasm/baseline/liftoff-assembler.h
index dc3fb82..5f28998 100644
--- a/src/wasm/baseline/liftoff-assembler.h
+++ b/src/wasm/baseline/liftoff-assembler.h
@@ -695,6 +695,12 @@
inline void CheckStackShrink();
inline void LoadConstant(LiftoffRegister, WasmValue);
inline void LoadInstanceDataFromFrame(Register dst);
+ inline void LoadMemoryStart(Register dst, Register instance_data,
+ int mem_index);
+ inline void RestoreCachedRegisters(Register instance_data,
+ bool reload_instance_data,
+ Register mem_start, bool reload_mem_start,
+ int mem_index);
inline void LoadTrustedPointer(Register dst, Register src_addr, int offset,
IndirectPointerTag tag);
inline void LoadFromInstance(Register dst, Register instance, int offset,
diff --git a/src/wasm/baseline/liftoff-compiler.cc b/src/wasm/baseline/liftoff-compiler.cc
index 5f8fefb..a61c1ed 100644
--- a/src/wasm/baseline/liftoff-compiler.cc
+++ b/src/wasm/baseline/liftoff-compiler.cc
@@ -712,6 +712,8 @@
WasmCodePosition position;
LiftoffRegList regs_to_save;
Register cached_instance_data{no_reg};
+ Register cached_mem_start{no_reg};
+ int cached_mem_index{-1};
OutOfLineSafepointInfo* safepoint_info;
// These two pointers will only be used for debug code:
SpilledRegistersForInspection* spilled_registers;
@@ -736,6 +738,7 @@
static OutOfLineCode* StackCheck(
Zone* zone, ZoneVector<OutOfLineCode*>* list, WasmCodePosition pos,
LiftoffRegList regs_to_save, Register cached_instance_data,
+ Register cached_mem_start, int cached_mem_index,
SpilledRegistersForInspection* spilled_regs,
OutOfLineSafepointInfo* safepoint_info,
DebugSideTableBuilder::EntryBuilder* debug_sidetable_entry_builder) {
@@ -748,6 +751,8 @@
ool->position = pos;
ool->regs_to_save = regs_to_save;
ool->cached_instance_data = cached_instance_data;
+ ool->cached_mem_start = cached_mem_start;
+ ool->cached_mem_index = cached_mem_index;
ool->safepoint_info = safepoint_info;
ool->spilled_registers = spilled_regs;
ool->debug_sidetable_entry_builder = debug_sidetable_entry_builder;
@@ -757,6 +762,7 @@
static OutOfLineCode* TierupCheck(
Zone* zone, ZoneVector<OutOfLineCode*>* list, WasmCodePosition pos,
LiftoffRegList regs_to_save, Register cached_instance_data,
+ Register cached_mem_start, int cached_mem_index,
SpilledRegistersForInspection* spilled_regs,
OutOfLineSafepointInfo* safepoint_info,
DebugSideTableBuilder::EntryBuilder* debug_sidetable_entry_builder) {
@@ -765,6 +771,8 @@
ool->position = pos;
ool->regs_to_save = regs_to_save;
ool->cached_instance_data = cached_instance_data;
+ ool->cached_mem_start = cached_mem_start;
+ ool->cached_mem_index = cached_mem_index;
ool->safepoint_info = safepoint_info;
ool->spilled_registers = spilled_regs;
ool->debug_sidetable_entry_builder = debug_sidetable_entry_builder;
@@ -1042,11 +1050,15 @@
if (!v8_flags.wasm_stack_checks) return;
LiftoffRegList regs_to_save = __ cache_state()->used_registers;
- // The cached instance data will be reloaded separately.
+ // The cached instance data and memory start will be reloaded separately.
if (__ cache_state()->cached_instance_data != no_reg) {
DCHECK(regs_to_save.has(__ cache_state()->cached_instance_data));
regs_to_save.clear(__ cache_state()->cached_instance_data);
}
+ if (__ cache_state()->cached_mem_start != no_reg) {
+ DCHECK(regs_to_save.has(__ cache_state()->cached_mem_start));
+ regs_to_save.clear(__ cache_state()->cached_mem_start);
+ }
SpilledRegistersForInspection* spilled_regs = nullptr;
OutOfLineSafepointInfo* safepoint_info =
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/test/mjsunit/regress/wasm/regress-513413942.js b/test/mjsunit/regress/wasm/regress-513413942.js
new file mode 100644
index 0000000..fbff72e
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-513413942.js
@@ -0,0 +1,71 @@
+// 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: --enable-inspector --allow-natives-syntax --no-wasm-tier-up
+// Flags: --no-wasm-trap-handler --stress-wasm-memory-moving
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+let wasmScriptId = null;
+let instance = null;
+
+globalThis.receive = function(m) {
+ let msg = JSON.parse(m);
+ if (msg.method === 'Debugger.scriptParsed') {
+ if (msg.params.url && msg.params.url.startsWith('wasm://')) {
+ wasmScriptId = msg.params.scriptId;
+ }
+ }
+};
+
+globalThis.handleInspectorMessage = function() {
+ send(JSON.stringify({id: 100, method: 'Debugger.resume'}));
+};
+
+const builder = new WasmModuleBuilder();
+builder.addMemory(1, 1000);
+builder.exportMemoryAs('memory', 0);
+
+const body = [
+ ...wasmI32Const(0),
+ kExprI32LoadMem, 0, 0,
+ kExprDrop,
+ kExprNop,
+ ...wasmI32Const(0),
+ ...wasmI32Const(123),
+ kExprI32StoreMem, 0, 0,
+];
+
+const target = builder.addFunction('target', kSig_v_v)
+ .addBody(body)
+ .exportFunc();
+
+const module_bytes = builder.toBuffer();
+const nop_offset = target.body_offset + 6;
+
+send(JSON.stringify({id: 1, method: 'Debugger.enable'}));
+const module = new WebAssembly.Module(module_bytes);
+instance = new WebAssembly.Instance(module);
+globalThis.instance = instance;
+globalThis.grown = false;
+
+const condition = `(function() {
+ if (!globalThis.grown) {
+ globalThis.instance.exports.memory.grow(1);
+ globalThis.grown = true;
+ return true;
+ }
+ return false;
+})()`;
+
+send(JSON.stringify({
+ id: 2,
+ method: 'Debugger.setBreakpoint',
+ params: {
+ location: {scriptId: wasmScriptId, lineNumber: 0, columnNumber: nop_offset},
+ condition: condition
+ }
+}));
+
+instance.exports.target();
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