← All reports

[JSC] Emit mutatorFence in BBQ JIT for WasmGC

Component: JSC WebAssembly | ce60d5d

Source/JavaScriptCore/wasm/WasmBBQJIT64.cpp

@@ emitAllocateGCStructUninitialized (default-init path)
- bool needsMutatorFence = false;
+ bool needsMutatorFence = structType.hasRefFieldTypes();
for (StructFieldCount i = 0; i < structType.fieldCount(); ++i) {
if (Wasm::isRefType(structType.field(i).type))
needsMutatorFence |= emitStructSet(resultGPR, structType, i, Value::fromRef(TypeKind::RefNull, JSValue::encode(jsNull())));
...
- // No write barrier needed here as all fields are set to constants.
- ASSERT_UNUSED(needsMutatorFence, !needsMutatorFence);
+ if (needsMutatorFence)
+ emitMutatorFence();
 
@@ emitAllocateGCStructUninitialized (args-init path)
- bool needsMutatorFence = false;
+ bool needsMutatorFence = structType.hasRefFieldTypes();
for (uint32_t i = 0; i < args.size(); ++i)
needsMutatorFence |= emitStructSet(resultGPR, structType, i, args[i]);

WasmGC structs are heap-allocated objects whose fields may hold references to other GC objects or plain scalars. JSC's collector runs concurrently with mutator execution, so when JIT code allocates and initializes an object it must emit a mutator fence — a barrier ensuring all field writes are visible and correctly ordered before the object can be discovered and traced. BBQ is JSC's fast baseline Wasm tier, prioritizing compile speed over optimization.

The fence-needed flag was computed only from whether the values being stored were literal constants. This patch derives it from structType.hasRefFieldTypes() instead, so a struct with a ref-typed field gets a fence even when every field is initialized with a constant such as null. The old code encoded the wrong assumption in an assertion — ASSERT_UNUSED(needsMutatorFence, !needsMutatorFence) asserted that no fence was ever needed on the default-init path, and passed cleanly rather than catching the issue.

The concurrent collector could observe a partially-initialized struct before the fence executed, letting it visit stale or incorrectly-typed memory. What matters to the collector is the declared field type, not the value stored into it — tracing logic scans fields by type, so a ref-typed field initialized with null still needs the barrier. Other tiers already emitted the fence correctly, making this specific to BBQ's allocation paths.

The reusable pattern is a barrier/fence condition derived from the value being written rather than the type of the slot receiving it. Narrow: audit the remaining struct and array allocation sites in WasmBBQJIT64.cpp for the same value-versus-type conflation — the match tell is any needsBarrier/needsFence boolean accumulated from per-value predicates (isConstant, isNull, fromRef) rather than from the declared field or element type. Wider: the same shape recurs anywhere a GC write barrier is elided on a "we know this value is not a pointer" argument — check JSC's other JIT tiers' emitPutById/storeValue barrier decisions and DFG/FTL's WriteBarrier node emission for conditions keyed on the stored value's proven type rather than the slot's declared type. Widest: in any concurrent-GC runtime, a barrier whose necessity is decided by the value written is fragile because the collector reads slots by declared type; the tell is an assertion that a barrier is never needed on some path — that assertion encodes the same assumption the code does and cannot falsify it.