Medium chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in V8
DescriptionType confusion in V8
ComponentV8
Sub-componentCompiler
Bug ClassType Confusion
Tracker558539954
Fix commitbdba9a2b54ca (v8/v8) +197/-35
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Files Changed

  • test/mjsunit/regress/wasm/regress-558539954.js
  • src/compiler/wasm-inlining-into-js.cc
From bdba9a2b54ca68d7b706864e17415d7aa0ec0ff6 Mon Sep 17 00:00:00 2001
From: Daniel Lehmann <[email protected]>
Date: Wed, 09 Sep 2026 15:45:14 +0200
Subject: [PATCH] [compiler][wasm] Fix null check for abstract ref.cast in JS inlining

When inlining a Wasm function into JS in TurboFan, ParseRefCast for
abstract array casts omitted a null check when null_succeeds was false.
When passed WasmNull, the subsequent HasInstanceType check dereferenced
the map of WasmNull (which resides on a guard page), causing a SEGV.

This change delegates abstract array casts in ParseRefCast to
WasmTypeCastAbstract, unifying them with concrete casts and ensuring
WasmGCLowering handles null checks and instance type checks. It also
factors out repeated null check queries into Value::null_check().

[email protected]

TAG=agy
CONV=ae44a1bc-4aad-4057-9192-5e342b9721be

Fixed: 558539954
Change-Id: Ia38436f9a26f480f9e9ffdb7327dfd47800ae92d
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/8378401
Reviewed-by: Matthias Liedtke <[email protected]>
Commit-Queue: Daniel Lehmann <[email protected]>
Cr-Commit-Position: refs/heads/main@{#109751}
---
diff --git a/src/compiler/wasm-inlining-into-js.cc b/src/compiler/wasm-inlining-into-js.cc
index a8ef32e..560e9b4 100644
--- a/src/compiler/wasm-inlining-into-js.cc
+++ b/src/compiler/wasm-inlining-into-js.cc
@@ -27,6 +27,10 @@
   struct Value {
     Node* node = nullptr;
     wasm::ValueType type = wasm::kWasmBottom;
+
+    CheckForNull null_check() const {
+      return type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
+    }
   };
 
  public:
@@ -219,10 +223,8 @@
       return {};
     }
     const bool is_signed = opcode == wasm::kExprStructGetS;
-    const CheckForNull null_check =
-        struct_val.type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
     Node* member = gasm_.StructGet(struct_val.node, struct_type, field_index,
-                                   is_signed, null_check);
+                                   is_signed, struct_val.null_check());
     SetSourcePosition(member);
     return TypeNode(member, struct_type->field(field_index).Unpacked());
   }
@@ -237,10 +239,8 @@
       is_inlineable_ = false;
       return;
     }
-    const CheckForNull null_check =
-        wasm_struct.type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
     gasm_.StructSet(wasm_struct.node, value.node, struct_type, field_index,
-                    null_check);
+                    wasm_struct.null_check());
     SetSourcePosition(gasm_.effect());
   }
 
@@ -254,29 +254,14 @@
         is_inlineable_ = false;
         return {};
       }
-      auto done = gasm_.MakeLabel();
-      // Abstract cast to array.
-      if (input.type.is_nullable() && null_succeeds) {
-        gasm_.GotoIf(gasm_.IsNull(input.node, input.type), &done);
-      }
-      gasm_.TrapIf(gasm_.IsSmi(input.node), TrapId::kTrapIllegalCast);
-      gasm_.TrapUnless(gasm_.HasInstanceType(input.node, WASM_ARRAY_TYPE),
-                       TrapId::kTrapIllegalCast);
-      SetSourcePosition(gasm_.effect());
-      gasm_.Goto(&done);
-      gasm_.Bind(&done);
-      // Add TypeGuard for graph typing.
-      TFGraph* graph = mcgraph_->graph();
-      wasm::ValueType result_type = wasm::ValueType::Generic(
+      wasm::ValueType target_type = wasm::ValueType::Generic(
           wasm::GenericKind::kArray,
           null_succeeds ? wasm::kNullable : wasm::kNonNullable,
           SharedFlag{false});
-      Node* type_guard =
-          graph->NewNode(mcgraph_->common()->TypeGuard(
-                             Type::Wasm(result_type, module_, graph->zone())),
-                         input.node, gasm_.effect(), gasm_.control());
-      gasm_.InitializeEffectControl(type_guard, gasm_.control());
-      return TypeNode(type_guard, result_type);
+      Node* cast =
+          gasm_.WasmTypeCastAbstract(input.node, {input.type, target_type});
+      SetSourcePosition(cast);
+      return TypeNode(cast, target_type);
     }
     wasm::ModuleTypeIndex target_type_index{static_cast<uint32_t>(heap_index)};
     if (module_->has_signature(target_type_index)) {
@@ -311,9 +296,7 @@
                wasm::IndependentHeapType{wasm::GenericKind::kArray,
                                          wasm::kNullable, SharedFlag{true}},
                module_));
-    const CheckForNull null_check =
-        input.type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
-    Node* len = gasm_.ArrayLength(input.node, null_check);
+    Node* len = gasm_.ArrayLength(input.node, input.null_check());
     SetSourcePosition(len);
     return TypeNode(len, wasm::kWasmI32);
   }
@@ -327,10 +310,8 @@
       return {};
     }
     const bool is_signed = opcode == WasmOpcode::kExprArrayGetS;
-    const CheckForNull null_check =
-        array.type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
     // Perform bounds check.
-    Node* length = gasm_.ArrayLength(array.node, null_check);
+    Node* length = gasm_.ArrayLength(array.node, array.null_check());
     SetSourcePosition(length);
     gasm_.TrapUnless(gasm_.Uint32LessThan(index.node, length),
                      TrapId::kTrapArrayOutOfBounds);
@@ -349,10 +330,8 @@
       is_inlineable_ = false;
       return;
     }
-    const CheckForNull null_check =
-        array.type.is_nullable() ? kWithNullCheck : kWithoutNullCheck;
     // Perform bounds check.
-    Node* length = gasm_.ArrayLength(array.node, null_check);
+    Node* length = gasm_.ArrayLength(array.node, array.null_check());
     SetSourcePosition(length);
     gasm_.TrapUnless(gasm_.Uint32LessThan(index.node, length),
                      TrapId::kTrapArrayOutOfBounds);
diff --git a/test/mjsunit/regress/wasm/regress-558539954.js b/test/mjsunit/regress/wasm/regress-558539954.js
new file mode 100644
index 0000000..90fd14c
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-558539954.js
@@ -0,0 +1,183 @@
+// 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: --allow-natives-syntax --turbofan --turbo-inline-js-wasm-calls
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+let array_type = builder.addArray(kWasmI32);
+let struct_type = builder.addStruct([makeField(kWasmI32, true)]);
+
+let make_array = builder.addFunction('make_array', makeSig([], [kWasmExternRef])).exportFunc();
+make_array.addBody([
+  kExprI32Const, 10,
+  kGCPrefix, kExprArrayNewDefault, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+let make_struct = builder.addFunction('make_struct', makeSig([], [kWasmExternRef])).exportFunc();
+make_struct.addBody([
+  kExprI32Const, 42,
+  kGCPrefix, kExprStructNew, struct_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 1. Abstract cast: ref.cast array
+let abstract_cast = builder.addFunction('abstract_cast', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+abstract_cast.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, kArrayRefCode,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 2. Abstract cast: ref.cast null array
+let abstract_cast_null = builder.addFunction('abstract_cast_null', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+abstract_cast_null.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, kArrayRefCode,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 3. Concrete cast: ref.cast <array_type>
+let concrete_cast = builder.addFunction('concrete_cast', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+concrete_cast.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 4. Concrete cast: ref.cast null <array_type>
+let concrete_cast_null = builder.addFunction('concrete_cast_null', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+concrete_cast_null.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 5. Abstract cast then array.len
+let cast_then_len = builder.addFunction('cast_then_len', makeSig([kWasmExternRef], [kWasmI32])).exportFunc();
+cast_then_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, kArrayRefCode,
+  kGCPrefix, kExprArrayLen,
+]);
+
+let cast_null_then_len = builder.addFunction('cast_null_then_len', makeSig([kWasmExternRef], [kWasmI32])).exportFunc();
+cast_null_then_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, kArrayRefCode,
+  kGCPrefix, kExprArrayLen,
+]);
+
+// 6. Direct array.len and struct.get with nullable parameter
+let array_len = builder.addFunction('array_len', makeSig([wasmRefNullType(array_type)], [kWasmI32])).exportFunc();
+array_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprArrayLen,
+]);
+
+let struct_get = builder.addFunction('struct_get', makeSig([wasmRefNullType(struct_type)], [kWasmI32])).exportFunc();
+struct_get.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprStructGet, struct_type, 0,
+]);
+
+const instance = builder.instantiate();
+const wasm_array = instance.exports.make_array();
+const wasm_struct = instance.exports.make_struct();
+
+function testOptimized(run, fctToOptimize) {
+  %DeoptimizeFunction(fctToOptimize);
+  %PrepareFunctionForOptimization(fctToOptimize);
+  run();
+  %OptimizeFunctionOnNextCall(fctToOptimize);
+  run();
+  assertOptimized(fctToOptimize);
+}
+
+// Test abstract cast (non-nullable)
+function test_abstract(x) {
+  return instance.exports.abstract_cast(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_abstract(wasm_array));
+  assertThrows(() => test_abstract(null), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract(undefined), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract({}), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract(123), WebAssembly.RuntimeError, /illegal cast/);
+}, test_abstract);
+
+// Test abstract cast (nullable)
+function test_abstract_null(x) {
+  return instance.exports.abstract_cast_null(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_abstract_null(wasm_array));
+  assertEquals(null, test_abstract_null(null));
+  assertThrows(() => test_abstract_null(undefined), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract_null({}), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract_null(123), WebAssembly.RuntimeError, /illegal cast/);
+}, test_abstract_null);
+
+// Test concrete cast (non-nullable)
+function test_concrete(x) {
+  return instance.exports.concrete_cast(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_concrete(wasm_array));
+  assertThrows(() => test_concrete(null), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_concrete(wasm_struct), WebAssembly.RuntimeError, /illegal cast/);
+}, test_concrete);
+
+// Test concrete cast (nullable)
+function test_concrete_null(x) {
+  return instance.exports.concrete_cast_null(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_concrete_null(wasm_array));
+  assertEquals(null, test_concrete_null(null));
+  assertThrows(() => test_concrete_null(wasm_struct), WebAssembly.RuntimeError, /illegal cast/);
+}, test_concrete_null);
+
+// Test cast then array.len
+function test_cast_len(x) {
+  return instance.exports.cast_then_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_cast_len(wasm_array));
+  assertThrows(() => test_cast_len(null), WebAssembly.RuntimeError, /illegal cast/);
+}, test_cast_len);
+
+function test_cast_null_len(x) {
+  return instance.exports.cast_null_then_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_cast_null_len(wasm_array));
+  assertThrows(() => test_cast_null_len(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_cast_null_len);
+
+// Test direct array.len on null
+function test_len(x) {
+  return instance.exports.array_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_len(wasm_array));
+  assertThrows(() => test_len(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_len);
+
+// Test direct struct.get on null
+function test_get(x) {
+  return instance.exports.struct_get(x);
+}
+testOptimized(() => {
+  assertEquals(42, test_get(wasm_struct));
+  assertThrows(() => test_get(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_get);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/mjsunit/regress/wasm/regress-558539954.js b/test/mjsunit/regress/wasm/regress-558539954.js
new file mode 100644
index 0000000..90fd14c
--- /dev/null
+++ b/test/mjsunit/regress/wasm/regress-558539954.js
@@ -0,0 +1,183 @@
+// 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: --allow-natives-syntax --turbofan --turbo-inline-js-wasm-calls
+
+d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
+
+const builder = new WasmModuleBuilder();
+let array_type = builder.addArray(kWasmI32);
+let struct_type = builder.addStruct([makeField(kWasmI32, true)]);
+
+let make_array = builder.addFunction('make_array', makeSig([], [kWasmExternRef])).exportFunc();
+make_array.addBody([
+  kExprI32Const, 10,
+  kGCPrefix, kExprArrayNewDefault, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+let make_struct = builder.addFunction('make_struct', makeSig([], [kWasmExternRef])).exportFunc();
+make_struct.addBody([
+  kExprI32Const, 42,
+  kGCPrefix, kExprStructNew, struct_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 1. Abstract cast: ref.cast array
+let abstract_cast = builder.addFunction('abstract_cast', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+abstract_cast.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, kArrayRefCode,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 2. Abstract cast: ref.cast null array
+let abstract_cast_null = builder.addFunction('abstract_cast_null', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+abstract_cast_null.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, kArrayRefCode,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 3. Concrete cast: ref.cast <array_type>
+let concrete_cast = builder.addFunction('concrete_cast', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+concrete_cast.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 4. Concrete cast: ref.cast null <array_type>
+let concrete_cast_null = builder.addFunction('concrete_cast_null', makeSig([kWasmExternRef], [kWasmExternRef])).exportFunc();
+concrete_cast_null.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, array_type,
+  kGCPrefix, kExprExternConvertAny,
+]);
+
+// 5. Abstract cast then array.len
+let cast_then_len = builder.addFunction('cast_then_len', makeSig([kWasmExternRef], [kWasmI32])).exportFunc();
+cast_then_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCast, kArrayRefCode,
+  kGCPrefix, kExprArrayLen,
+]);
+
+let cast_null_then_len = builder.addFunction('cast_null_then_len', makeSig([kWasmExternRef], [kWasmI32])).exportFunc();
+cast_null_then_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprAnyConvertExtern,
+  kGCPrefix, kExprRefCastNull, kArrayRefCode,
+  kGCPrefix, kExprArrayLen,
+]);
+
+// 6. Direct array.len and struct.get with nullable parameter
+let array_len = builder.addFunction('array_len', makeSig([wasmRefNullType(array_type)], [kWasmI32])).exportFunc();
+array_len.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprArrayLen,
+]);
+
+let struct_get = builder.addFunction('struct_get', makeSig([wasmRefNullType(struct_type)], [kWasmI32])).exportFunc();
+struct_get.addBody([
+  kExprLocalGet, 0,
+  kGCPrefix, kExprStructGet, struct_type, 0,
+]);
+
+const instance = builder.instantiate();
+const wasm_array = instance.exports.make_array();
+const wasm_struct = instance.exports.make_struct();
+
+function testOptimized(run, fctToOptimize) {
+  %DeoptimizeFunction(fctToOptimize);
+  %PrepareFunctionForOptimization(fctToOptimize);
+  run();
+  %OptimizeFunctionOnNextCall(fctToOptimize);
+  run();
+  assertOptimized(fctToOptimize);
+}
+
+// Test abstract cast (non-nullable)
+function test_abstract(x) {
+  return instance.exports.abstract_cast(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_abstract(wasm_array));
+  assertThrows(() => test_abstract(null), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract(undefined), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract({}), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract(123), WebAssembly.RuntimeError, /illegal cast/);
+}, test_abstract);
+
+// Test abstract cast (nullable)
+function test_abstract_null(x) {
+  return instance.exports.abstract_cast_null(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_abstract_null(wasm_array));
+  assertEquals(null, test_abstract_null(null));
+  assertThrows(() => test_abstract_null(undefined), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract_null({}), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_abstract_null(123), WebAssembly.RuntimeError, /illegal cast/);
+}, test_abstract_null);
+
+// Test concrete cast (non-nullable)
+function test_concrete(x) {
+  return instance.exports.concrete_cast(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_concrete(wasm_array));
+  assertThrows(() => test_concrete(null), WebAssembly.RuntimeError, /illegal cast/);
+  assertThrows(() => test_concrete(wasm_struct), WebAssembly.RuntimeError, /illegal cast/);
+}, test_concrete);
+
+// Test concrete cast (nullable)
+function test_concrete_null(x) {
+  return instance.exports.concrete_cast_null(x);
+}
+testOptimized(() => {
+  assertSame(wasm_array, test_concrete_null(wasm_array));
+  assertEquals(null, test_concrete_null(null));
+  assertThrows(() => test_concrete_null(wasm_struct), WebAssembly.RuntimeError, /illegal cast/);
+}, test_concrete_null);
+
+// Test cast then array.len
+function test_cast_len(x) {
+  return instance.exports.cast_then_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_cast_len(wasm_array));
+  assertThrows(() => test_cast_len(null), WebAssembly.RuntimeError, /illegal cast/);
+}, test_cast_len);
+
+function test_cast_null_len(x) {
+  return instance.exports.cast_null_then_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_cast_null_len(wasm_array));
+  assertThrows(() => test_cast_null_len(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_cast_null_len);
+
+// Test direct array.len on null
+function test_len(x) {
+  return instance.exports.array_len(x);
+}
+testOptimized(() => {
+  assertEquals(10, test_len(wasm_array));
+  assertThrows(() => test_len(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_len);
+
+// Test direct struct.get on null
+function test_get(x) {
+  return instance.exports.struct_get(x);
+}
+testOptimized(() => {
+  assertEquals(42, test_get(wasm_struct));
+  assertThrows(() => test_get(null), WebAssembly.RuntimeError, /null pointer/);
+}, test_get);
Loading diff…

Original Bug Report

reported by [email protected]

V8: WasmNull escapes to JS causing SEGV in Turboshaft Wasm-in-JS inlining

Flapjack has identified a security issue and generated a PoC.

Build variant: Asan

Build arguments: is_asan=true is_debug=false dcheck_always_on=false symbol_level=1 v8_enable_memory_corruption_api=true v8_enable_sandbox=true

Shell command: d8 --run-as-security-poc --omit-quit --allow-natives-syntax --turbofan --disable-in-process-stack-traces

Return code: 134

<details>

<summary>stdout</summary>

</details>

<details>

<summary>stderr</summary>

AddressSanitizer:DEADLYSIGNAL
=================================================================
==10==ERROR: AddressSanitizer: SEGV on unknown address 0x6eb300010000 (pc 0x596e70180256 bp 0x7fffe06c8598 sp 0x7fffe06c8578 T0)
==10==The signal is caused by a READ memory access.
    #0 0x596e70180256  (<unknown module>)
    #1 0x596e101c6180 in Builtins_InterpreterEntryTrampoline (/home/rjlothian_google_com/chromium_ro/src/out/Asan/d8+0x7885180) (BuildId: bf61b6d170bd6d61)
    #2 0x596e101c2d9b in Builtins_JSEntryTrampoline (/home/rjlothian_google_com/chromium_ro/src/out/Asan/d8+0x7881d9b) (BuildId: bf61b6d170bd6d61)
    #3 0x596e101c2aea in Builtins_JSEntry (/home/rjlothian_google_com/chromium_ro/src/out/Asan/d8+0x7881aea) (BuildId: bf61b6d170bd6d61)
    #4 0x596e0a69013a in v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) v8/src/execution/execution.cc:492:22
    #5 0x596e0a693110 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>) v8/src/execution/execution.cc:593:10
    #6 0x596e0a2d62f4 in v8::Script::Run(v8::Local<v8::Context>, v8::Local<v8::Data>) v8/src/api/api.cc:2057:7
    #7 0x596e09fd2fba in v8::Shell::ExecuteSource(v8::Isolate*, v8::Shell::Source const&, v8::Local<v8::String>, v8::Shell::ReportExceptions, v8::Global<v8::Value>*) v8/src/d8/d8.cc:1287:44
    #8 0x596e0a01a7bc in v8::SourceGroup::Execute(v8::Isolate*) v8/src/d8/d8.cc:6341:12
    #9 0x596e0a02979e in v8::Shell::RunMainIsolate(v8::Isolate*, bool) v8/src/d8/d8.cc:7461:37
    #10 0x596e0a028bfd in v8::Shell::RunMain(v8::Isolate*, bool) v8/src/d8/d8.cc:7375:18
    #11 0x596e0a02d767 in v8::Shell::Main(int, char**) v8/src/d8/d8.cc:8343:18
    #12 0x73fba90771c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #13 0x73fba907728a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
    #14 0x596e09eb2029 in _start (/home/rjlothian_google_com/chromium_ro/src/out/Asan/d8+0x1571029) (BuildId: bf61b6d170bd6d61)

==10==Register values:
rax = 0x0000000000000002  rbx = 0x0000596e0b87caf0  rcx = 0x00006eb300010001  rdx = 0x00006eb300000011  
rdi = 0x00007eee01000239  rsi = 0x00006eb30101e0f1  rbp = 0x00007fffe06c8598  rsp = 0x00007fffe06c8578  
 r8 = 0x00006eb30104835d   r9 = 0x00007eee0100032d  r10 = 0x0000000000000000  r11 = 0x00006eb30101e435  
r12 = 0x00007eee010000e9  r13 = 0x0000731ba83e1080  r14 = 0x00006eb300000000  r15 = 0x0000000000134a00  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (<unknown module>) 
==10==ABORTING

</details>

Overview: Turboshaft’s Wasm-to-JS wrapper conversion incorrectly skips converting WasmNull to JavaScript null for types where use_wasm_null() returns false (such as externref or nullexternref). When this escaped WasmNull object is passed into an inlined Wasm function, ReduceAnyConvertExtern fails to identify it as null and dereferences its map via HasInstanceType. On 64-bit builds with static roots, WasmNull resides in a decommitted memory region, causing an immediate unhandled read SIGSEGV crash in JIT-compiled JavaScript code.

Affected files:

  • v8/src/compiler/turboshaft/wasm-wrappers-inl.h
  • v8/src/compiler/turboshaft/wasm-lowering-reducer.h
  • v8/src/compiler/turboshaft/wasm-in-js-inlining-reducer-inl.h
  • v8/src/compiler/turboshaft/wasm-wrappers.h

Estimated timestamp from git blame: 2026-08-07

Summary

An engine invariant violation in V8’s Turboshaft JS-to-Wasm wrapper and body inlining allows the internal WasmNull sentinel object to leak into JavaScript user code. Subsequent inlined Wasm operations attempt to load the map field of WasmNull, triggering an unhandled SIGSEGV on the decommitted WasmNull guard page in JIT-compiled code.

Root Cause Analysis

  1. WasmNull Leak in ToJS() (v8/src/compiler/turboshaft/wasm-wrappers-inl.h): When converting Wasm return values to JavaScript in WasmWrapperTSGraphBuilder<Assembler>::ToJS, the function attempts to check if ret is WasmNull and convert it to JS null (RootIndex::kNullValue). However, it has the following early exit:

    // At this point, we only have to process WasmNull.
    // These two cases are never WasmNull.
    if (!type.is_nullable()) return ret;
    if (!type.use_wasm_null()) return ret;
    

    For externref (GenericKind::kExtern) and nullexternref (GenericKind::kNoExtern), type.use_wasm_null() evaluates to false. However, under WebAssembly GC subtyping, a function with return type externref can legally return (ref null noextern) (or field loads of null references). Because use_wasm_null() is false, ToJS() returns ret unmodified, letting WasmNull escape into JavaScript.

  2. Dereference in ReduceAnyConvertExtern() (v8/src/compiler/turboshaft/wasm-lowering-reducer.h): When WasmNull is passed back to a Wasm function accepting externref and inlined into JavaScript via WasmInJsInlining:

    • FromJS() (v8/src/compiler/turboshaft/wasm-wrappers.h) passes WasmNull through.
    • ReduceAnyConvertExtern checks if (__ IsNull(object, wasm::kWasmExternRef)) which tests against kNullValue (JS null). Since object is WasmNull rather than kNullValue, this check fails.
    • The lowering then executes GOTO_IF(__ HasInstanceType(object, HEAP_NUMBER_TYPE), heap_number_label);.
    • HasInstanceType invokes LoadMapField(object) (v8/src/compiler/turboshaft/assembler.h:3242).
    • In static roots builds (V8_STATIC_ROOTS_BOOL), WasmNull (StaticReadOnlyRoot::kWasmNull = 0x10001) resides on a 64KB unmapped page (v8/src/heap/setup-heap-internal.cc:1378, v8/src/wasm/wasm-objects.h:2122). Dereferencing object - 1 (0x...10000) triggers an immediate read page fault.
    • Because the fault occurs in JIT-compiled JavaScript code rather than registered WebAssembly memory ranges, V8’s Wasm signal trap handler does not intercept it, causing a crash with SIGSEGV.

Reproduction Steps

  1. Run the PoC with d8:
    d8 --turbofan poc.js
    
  2. The process crashes with AddressSanitizer: SEGV on unknown address 0x...10000 during execution of the optimized function.

Evaluated with Chrome root at commit: 533aa10cbdff8daaa6b1faf1d575f6cc7b2a022e


The description of the vuln is LLM-generated and can contain mistakes. Your feedback is appreciated, and will help us make improvement over time. The PoC was run in a VM and it seemed to be legit - if not, let us know and we can strengthen our checker. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

View on issue tracker