CVE-2026-106341
Overview
Files Changed
test/mjsunit/regress/wasm/regress-558539954.jssrc/compiler/wasm-inlining-into-js.cc
Patch
From bdba9a2b54ca68d7b706864e17415d7aa0ec0ff6 Mon Sep 17 00:00:00 2001
From: Daniel Lehmann <dlehmann@chromium.org>
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().
R=mliedtke@chromium.org
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 <mliedtke@chromium.org>
Commit-Queue: Daniel Lehmann <dlehmann@chromium.org>
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);
Regression Test / PoC
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);
Original Bug Report
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.hv8/src/compiler/turboshaft/wasm-lowering-reducer.hv8/src/compiler/turboshaft/wasm-in-js-inlining-reducer-inl.hv8/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
-
WasmNullLeak inToJS()(v8/src/compiler/turboshaft/wasm-wrappers-inl.h): When converting Wasm return values to JavaScript inWasmWrapperTSGraphBuilder<Assembler>::ToJS, the function attempts to check ifretisWasmNulland convert it to JSnull(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) andnullexternref(GenericKind::kNoExtern),type.use_wasm_null()evaluates tofalse. However, under WebAssembly GC subtyping, a function with return typeexternrefcan legally return(ref null noextern)(or field loads of null references). Becauseuse_wasm_null()is false,ToJS()returnsretunmodified, lettingWasmNullescape into JavaScript. -
Dereference in
ReduceAnyConvertExtern()(v8/src/compiler/turboshaft/wasm-lowering-reducer.h): WhenWasmNullis passed back to a Wasm function acceptingexternrefand inlined into JavaScript viaWasmInJsInlining:FromJS()(v8/src/compiler/turboshaft/wasm-wrappers.h) passesWasmNullthrough.ReduceAnyConvertExternchecksif (__ IsNull(object, wasm::kWasmExternRef))which tests againstkNullValue(JSnull). SinceobjectisWasmNullrather thankNullValue, this check fails.- The lowering then executes
GOTO_IF(__ HasInstanceType(object, HEAP_NUMBER_TYPE), heap_number_label);. HasInstanceTypeinvokesLoadMapField(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). Dereferencingobject - 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
- Run the PoC with
d8:d8 --turbofan poc.js - The process crashes with
AddressSanitizer: SEGV on unknown address 0x...10000during 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.