[5] OMG emits an integer zero into a float Wasm field
Low — the payload is a zero either way, so the bits stored are correct regardless; what breaks is B3's type bookkeeping, and the release assert catches it by killing the process. Reliable and remote-triggerable, which is the only reason it isn't lower.
JSC compiles WebAssembly through several tiers, the topmost being OMG, an optimizing JIT that lowers Wasm bytecode into B3 — the mid-level IR shared with the FTL JavaScript compiler. Every value in B3 carries an explicit type, and optimization passes rely on those types matching: when a load reads back a slot that was just written, the CSE pass can replace the load with the stored value, but only if the two agree on type. Wasm GC's struct.new_default and array.new_default allocate an object and zero every field, so the IR generator must produce a zero constant for each field — carrying that field's declared type.
The angle: any page can serve a few-line Wasm module that deterministically aborts the WebContent process of every tab that loads it.
The
struct.new_defaultwasm instruction currently writesInt32(0)for f32 types. If store-to-load forwarding occurs,B3::Value::replaceWithIdentitywill fail aRELEASE_ASSERTbecause the expected type is f32, not i32. This patch adjustsOMGIRGenerator::addStructNewDefaultandOMGIRGenerator::addArrayNewDefaultto create a constant of the correct type instead of defaulting to int types.
Source/JavaScriptCore/wasm/WasmOMGIRGenerator.cpp
JSTests/wasm/stress/new_default-f32.js
Patch Details
Both OMGIRGenerator::addArrayNewDefault and OMGIRGenerator::addStructNewDefault previously chose the B3 type of their zero-initializer from the field's byte size: two branches, size <= 4 producing constant(Int32, 0) and everything else producing constant(Int64, 0). The patch collapses each pair into a single initValue = constant(toB3Type(<wasmType>.unpacked()), 0) call, deriving the type from the declared Wasm value type instead. The preceding reference-type and 16-byte V128 branches are untouched. The regression test builds two modules — a struct with one f32 field, and an array of f32 — calls struct.new_default/array.new_default followed by an immediate read of the field or element, and runs the function in a hot loop (wasmTestLoopCount) to promote it into OMG.
Type-tagging mismatch in JIT IR construction — B3 constant produced from a byte-size heuristic instead of from the declared Wasm value type.
Background
B3 and its type invariant.
B3 is JSC's mid-level optimizing IR, shared by the FTL JavaScript JIT and the OMG Wasm JIT. Every B3 Value carries a Type — Int32, Int64, Float, Double, V128, and others — and IR-level invariants require operations to agree on operand types.
CSE and store-to-load forwarding.
Common subexpression elimination and store-to-load forwarding are B3 optimizations that detect a load from an address just written and replace the load with the stored value, via Value::replaceWithIdentity. That replacement requires the source and target B3 types to be equal.
RELEASE_ASSERT.
A WebKit assertion macro that fires in both debug and release builds and aborts the process when its condition is false.
OMG and the Wasm GC default constructors.
OMG is JSC's optimizing Wasm JIT tier, sitting above BBQ; OMGIRGenerator walks Wasm bytecode and emits B3 IR. struct.new_default and array.new_default are Wasm GC instructions that allocate a struct or array and zero-initialize every field or element.
Wasm value types and toB3Type.
Wasm value types include i32, i64, f32, f64, v128, and reference types; f32 occupies 4 bytes and f64 occupies 8. toB3Type is the helper mapping a Wasm value type to its B3 counterpart — f32 to Float, f64 to Double.
Analysis
The bug is a type-tagging error at IR construction time: OMG derived the initializer's B3 type from storage size rather than from the declared type.
Wasm decl old size branch B3 constant field's B3 type
────────── ─────────────── ─────────── ───────────────
i32 (4B) size <= 4 Int32(0) Int32 ✓
f32 (4B) size <= 4 Int32(0) Float ✗ mismatch
i64 (8B) else Int64(0) Int64 ✓
f64 (8B) else Int64(0) Double ✗ mismatch
struct.new_default struct.get
────────────────── ──────────
WasmStructSet(field, ────► WasmStructGet(field) : Float
Int32(0)) │
▼ B3 CSE: store-to-load forwarding
replaceWithIdentity(Int32 → Float)
└─ RELEASE_ASSERT → abort
For a 4-byte f32 field the size heuristic produced an Int32 B3 value fed into WasmStructSet/WasmArraySet even though the field's declared B3 type is Float; the else branch produced the analogous Int64-into-Double mismatch for f64. As long as nothing looked at the stored value again, the mismatch stayed latent. The moment B3's CSE spots a later load from the same struct field or array slot and tries to forward the stored value through Value::replaceWithIdentity, the type equality requirement fails — load is Float, replacement is Int32 — and the RELEASE_ASSERT fires during OMG compilation, terminating the WebContent process. That replaceWithIdentity is the specific assertion site is relayed from the commit message; B3's CSE implementation is not part of the supplied context. The fix routes the type through toB3Type(<wasmType>.unpacked()), restoring the invariant that a stored value's type matches the slot's declared type.
The regression test is effectively the PoC, and the shape is what matters:
- Declare a struct type with one
f32field (or an array off32). - Call
struct.new_default/array.new_default. - Immediately read the field back with
struct.get/array.get. - Store the result somewhere observable —
global.set 0in the test. - Call the exported function in a tight loop until OMG promotes it.
Step 3 is the load that CSE will try to forward from step 2's store; steps 4 and 5 exist to keep the sequence alive and reach the optimizing tier. Delivered from the web this is WebAssembly.instantiate on a handful of bytes plus a loop — a reliable, any-origin renderer crash.
There is no architectural evidence in the change of a downstream memory-safety primitive. The bit patterns of Int32(0) and Float(0.0), and of Int64(0) and Double(0.0), are identical, so even if the assertion were absent the stored bytes would be correct; the harm is the IR-level type confusion itself, which the RELEASE_ASSERT catches by aborting. A type-confused B3 graph could in principle mislead a later optimization pass, but no such downstream miscompilation is visible in the patch or the surrounding code. Since this lives in the WebContent process, a crash here terminates the renderer hosting the offending tab and does not yield a sandbox escape.
The trigger shape — a tiny module with a single f32 field plus a hot loop — is exactly what a Wasm grammar fuzzer restricted to GC types produces, making fuzzing the most likely discovery path. Variant analysis is also plausible: once one B3 type-mismatch assertion crash is known, grepping OMGIRGenerator for constant(Int32, 0) and constant(Int64, 0) surfaces these two sites immediately.
This vulnerability weakens WebContent process availability. The Wasm trust boundary assumes any well-formed module, adversarial or not, compiles through OMG without aborting the renderer; before the fix, a one-line module using struct.new_default or array.new_default over an f32/f64 type reliably killed the tab.
Insight: this class recurs wherever a JIT IR generator picks IR types from byte-size or storage-size heuristics instead of the source language's declared type. The if (size == 4) Int32 else Int64 shape works silently for integer fields and breaks the moment a floating-point or vector field of matching size appears. Routing the type through a toB3Type(declaredType) helper rather than rederiving it from size is the canonical mitigation.
Audit directions
- JIT IR generators picking operand types from byte-size heuristics. This silently misclassifies floating-point and vector fields whose size collides with an integer width and breaks downstream IR-level type invariants. Audit
WasmOMGIRGenerator.cpp,WasmBBQJIT64.cpp, and the BBQ/IPInt array and struct lowerings (addArrayNew,addArrayFill,addArrayCopy,addStructSet) for any constant or load/store that selects its B3/Air type fromelementSize()ortypeSizeInBytes()instead oftoB3Type(unpacked()). In review, anyif (size == 4)/size <= 4branch whose two arms differ in IR type rather than IR width is the tell. - Type-mismatched producers feeding typed slots. B3 store-to-load forwarding asserts type equality, so any IR site that writes a value of one B3 type into a typed slot followed by a load of a different B3 type from the same slot will
RELEASE_ASSERT. GrepSource/JavaScriptCore/b3/forreplaceWithIdentityandB3CSE, then for each producer of struct, array, or global stores inWasmOMGIRGeneratorconfirm the produced B3 type matches the slot's declared type. Particular candidates: default initializers, generic boxing helpers, and anyconstant(Int32, 0)/constant(Int64, 0)pattern feeding a non-integer slot. - Cross-tier divergence in Wasm GC lowering. These paths are relatively new and have four parallel implementations (OMG, BBQ, IPInt, ConstExprGenerator); a bug in one tier may not exist in the others, masking it during cross-tier testing. Verify that all four
addStructNewDefault/addArrayNewDefaultimplementations agree on the type of the zero-initializer forf32,f64, and the packed typesi8andi16— diff them side by side inWasmBBQJIT64.cpp,WasmIPIntGenerator.cpp,WasmConstExprGenerator.cpp, andWasmOMGIRGenerator.cpp. unpacked()vs. rawStorageTypedivergence.f32,f64,i32, andi64are unpacked, buti8/i16packed fields unpack toi32for read and write. Audit whether any default-init or generic-set helper uses the packed storage type to choose the B3 constant type while a siblingstruct.get_s/struct.get_ureads with the unpacked type — a smaller-scale variant of the same store-to-load type-mismatch class.