← All issues

[10] Wasm nonnullable reference type confusion at JS-to-Wasm boundary

Severity: Medium | Component: JSC WebAssembly | 2d64f53

Rated Medium because the observable effect is a type system invariant violation (null smuggled into nonnullable reference) confirmed at confidence 0.92 by the regression test, with the immediate consequence being a null dereference — escalation to controlled memory access via struct.get at large offsets is projected but depends on unverified claims about the Wasm compiler eliding null checks for nonnullable types.

Adds null checks at the JS-to-Wasm return boundary for nonnullable reference types across all four marshalling paths: three C++ slow paths and one JIT-compiled stub.

Source/JavaScriptCore/wasm/WasmOperations.cpp

default: {
if (Wasm::isRefType(returnType)) {
+ JSValue value = JSValue::decode(std::bit_cast<EncodedJSValue>(returned));
+ if (value.isNull() && !returnType.isNullable()) [[unlikely]] {
+ throwTypeError(globalObject, scope, "Host function incorrectly returned null for a nonnullable reference type"_s);
+ OPERATION_RETURN(scope);
+ }
+
if (Wasm::isExternref(returnType)) {
// Do nothing.
} else if (Wasm::isFuncref(returnType)) {
// operationConvertToFuncref
- JSValue value = JSValue::decode(std::bit_cast<EncodedJSValue>(returned));

Source/JavaScriptCore/wasm/js/WasmToJS.cpp

default: {
if (Wasm::isRefType(returnType)) {
+ if (!returnType.isNullable()) {
+ auto isNotNull = jit.branchIfNotNull(JSRInfo::returnValueJSR);
+ jit.move(GPRInfo::wasmContextInstancePointer, GPRInfo::argumentGPR0);
+ emitThrowWasmToJSException(jit, GPRInfo::argumentGPR0, ExceptionType::TypeErrorUnexpectedNullReference);
+ isNotNull.link(&jit);
+ }

JSTests/wasm/function-references/nullability.js

+ let instance = new WebAssembly.Instance(module("..."), {
+ env: {
+ getFunc: () => null
+ }
+ });
+ assert.throws(
+ () => {
+ for (let i = 0; i < 1000; i++) {
+ instance.exports.callGetFunc();
+ }
+ },
+ TypeError,
+ "Host function incorrectly returned null for a nonnullable reference type"
+ )

Four code paths are patched. In operationWasmToJSExitMarshalReturnValues (both single-return and multi-return branches) and operationIterateResults, a value.isNull() && !returnType.isNullable() check is added before type-specific conversion, throwing TypeError on violation. In the JIT-compiled wasmToJS stub, a branchIfNotNull guard is emitted that throws ExceptionType::TypeErrorUnexpectedNullReference when the return type is nonnullable. A new exception type is added to WasmExceptionType.h.

The WebAssembly type system distinguishes nullable reference types (ref null T) from nonnullable ones (ref T). Nullable references permit null; nonnullable references guarantee the value is always a valid reference. The engine may omit null checks on operations applied to nonnullable references because the type system guarantees non-nullness — this is a correctness and performance optimization.

When a Wasm module imports a JS function, calls to that function cross the Wasm-JS boundary. The return value must be converted from a JS value back into the declared Wasm return type. This marshalling occurs in JIT-compiled stubs (wasmToJS) for the fast path and in C++ slow-path operations for complex cases. The (ref func) type is a nonnullable function reference introduced by the function-references proposal, distinct from funcref which is (ref null func) — nullable.

Missing nullability enforcement at the JS-to-Wasm return boundary for nonnullable reference types.

Before the fix, no null check was performed when a JS host function returned a value to Wasm code expecting a nonnullable reference type. The return value was decoded and passed through type-specific conversion (funcref validation, anyref internalization, externref passthrough) but the nonnullability constraint was never enforced. This allowed a JS function to return null for a Wasm import declared with a nonnullable return type. The null value then propagated into Wasm code that, per the type system, assumes the reference is guaranteed non-null. Downstream Wasm instructions operating on nonnullable references (e.g., call_ref, struct.get, ref.test, ref.cast) may omit their own null guards because the type system already guarantees non-nullness.

🔒

Explores whether this type system violation can be escalated beyond a simple crash under realistic conditions

Subscribe to read more

🔒

Multiple reusable audit patterns identified, with concrete starting points for variant discovery across JS-Wasm boundary code

Subscribe to read more