← All issues

[5] OMG Wasm B3 IR type-tag mismatch on struct/array.new_default

OMG picked its zero-initialiser type from `elementSize()`. An `f32` field is 4 bytes — same width as `i32` — so the JIT tagged the zero `Int32`.

Severity: Low | Component: JSC OMG Wasm tier — WasmOMGIRGenerator | 7842f48

Rated Low because the diff prevents a RELEASE_ASSERT in B3's CSE / store-to-load forwarding triggered when an Int32(0) or Int64(0) constant is used as the zero-initializer for an f32/f64 Wasm field. Bit patterns of zero coincide between integer and float slots, so the harm is confined to the assertion abort itself; no downstream miscompilation or memory-safety primitive is visible in this commit.

The struct.new_default Wasm instruction was writing Int32(0) for f32 types. If store-to-load forwarding fired, B3::Value::replaceWithIdentity failed a RELEASE_ASSERT because the expected type was f32, not i32. The patch adjusts OMGIRGenerator::addStructNewDefault and OMGIRGenerator::addArrayNewDefault to construct a constant of the correct type via toB3Type(<wasmType>.unpacked()). A regression test (new_default-f32.js) builds a struct-of-f32 and an array-of-f32, calls the corresponding *.new_default followed by an immediate read, and runs the function in a hot loop to trigger OMG tier compilation.

Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp

// addArrayNewDefault
initValue = m_currentBlock->appendNew<WasmConstRefValue>(m_proc, origin(), JSValue::encode(jsNull()));
else if (elementType.elementSize() == 16)
initValue = constant(V128, v128_t { });
- else if (elementType.elementSize() <= 4)
- initValue = constant(Int32, 0);
else
- initValue = constant(Int64, 0);
+ initValue = constant(toB3Type(elementType.unpacked()), 0);
 
// addStructNewDefault
initValue = m_currentBlock->appendNew<WasmConstRefValue>(m_proc, origin(), JSValue::encode(jsNull()));
else if (typeSizeInBytes(fieldType) == 16)
initValue = constant(V128, v128_t { });
- else if (typeSizeInBytes(fieldType) <= 4)
- initValue = constant(Int32, 0);
else
- initValue = constant(Int64, 0);
+ initValue = constant(toB3Type(fieldType.unpacked()), 0);

JSTests/wasm/stress/new_default-f32.js

+ // (type (struct (field f32))) ; struct.new_default 0; struct.get 0 0; global.set 0
+ for (let i = 0; i < wasmTestLoopCount; ++i) fn();
+ // (type (array f32)) ; array.new_default 0; array.get 0; global.set 0
+ for (let i = 0; i < wasmTestLoopCount; ++i) fn();

The patch collapses the two byte-size-keyed branches (<= 4Int32(0), elseInt64(0)) into a single call that derives the B3 type from the declared Wasm value type via toB3Type(<wasmType>.unpacked()). Both addArrayNewDefault and addStructNewDefault receive the same treatment. The 16-byte vector branch is unchanged, and the reference-type branch is unchanged. The new regression test exercises f32 specifically — both struct field and array element variants — under a hot loop sized to promote into OMG.

Type-tagging mismatch in JIT IR construction — B3 constant produced from byte-size heuristic instead of from the declared Wasm value type, breaking the store-to-load forwarding type invariant.

B3 is JSC's mid-level optimizing IR shared by FTL and OMG; every B3 Value carries a Type (Int32, Int64, Float, Double, V128, etc.) and IR-level invariants require that operations agree on operand types. CSE (common subexpression elimination) and store-to-load forwarding are B3 optimizations that detect a load from an address that was just written and replace the load with the stored value via Value::replaceWithIdentity, which requires the source and target B3 types to be equal. RELEASE_ASSERT in WebKit fires in both debug and release builds and aborts the process when its condition is false.

OMG is JSC's optimizing Wasm JIT tier; OMGIRGenerator walks the Wasm bytecode and emits B3 IR. struct.new_default / array.new_default are Wasm GC instructions that allocate a struct or array and zero-initialize every field/element. Wasm value types include i32, i64, f32, f64, v128, and reference types; f32 is 4 bytes, f64 is 8 bytes. toB3Type is the helper that maps a Wasm value type to its B3 type (f32 → Float, f64 → Double).

Pre-fix, when OMG lowered struct.new_default and array.new_default to B3 IR, it picked the type of the zero-initializer purely from the element/field's byte size: <= 4 got Int32(0), otherwise Int64(0). For Wasm f32 fields/elements (4 bytes) this produced an Int32 B3 value that was fed into WasmStructSet / WasmArraySet, even though the field's declared B3 type is Float. The same mismatch existed for f64 — the old else branch produced Int64(0) for an 8-byte field whose B3 type is Double.

B3's CSE / store-to-load forwarding then identifies a later load from the same struct field or array slot and tries to replace that load with the previously stored value via Value::replaceWithIdentity. replaceWithIdentity requires the replacement value's B3 type to equal the load's type; here the load is Float (or Double) but the stored constant is Int32 (or Int64), so the RELEASE_ASSERT in B3 fires during OMG compilation and terminates the WebContent process. Routing the type through toB3Type(<wasmType>.unpacked()) restores B3's type invariant: the stored value's type matches the field/element's declared type.

This is a class of bug that recurs in JIT IR generators that pick IR types from byte-size or storage-size heuristics rather than from the source language's declared type. The same pattern — if (size == 4) Int32 else Int64 — silently works for integer-typed fields but breaks the moment a floating-point or vector field of the same size enters the picture.

The regression test is essentially the PoC: declare a struct or array type with at least one f32 (or f64) field, call struct.new_default / array.new_default, then immediately read the field back and store the result somewhere observable. The inner sequence struct.new_default 0 ; struct.get 0 0 ; global.set 0 (and the array variant) is exactly the shape that lets B3's store-to-load forwarding spot the redundant load and attempt the type-mismatched replaceWithIdentity.

The diff shows no evidence of a downstream memory-safety primitive: the bit pattern of Int32(0) and Float(0.0) is identical, so even if the assertion were elided the stored bits would be correct. A type-confused B3 IR could in principle mislead a later optimization pass, but no such downstream miscompilation is visible in the patch or surrounding code.

This vulnerability weakens WebContent process availability. The HTML/Wasm trust boundary assumes that any well-formed Wasm module — even adversarial ones — should compile through OMG without aborting the renderer. Pre-fix, a one-line Wasm module containing struct.new_default or array.new_default over an f32/f64 type, executed in a hot loop to promote it into OMG, reliably trips the assertion and kills the renderer. An attacker who serves such a module from web content could deterministically crash any tab that loads their page.

Note: B3's exact assertion site (Value::replaceWithIdentity in CSE / store-to-load forwarding), toB3Type's mapping, and the framing that Int32(0)/Float(0.0) bit-pattern coincidence renders the case harmless absent the assert are inferred from B3 conventions rather than visible in this commit. The corrective IR-type plumbing is fully visible.