[Wasm] Fix JS Table.grow with a default value
A Wasm table slot that was full but looked empty to the collector.
Component: JSC WebAssembly | 36a3e59
Source/JavaScriptCore/wasm/WasmTable.cpp
Source/JavaScriptCore/wasm/WasmTable.h
Wasm tables store funcref entries as a pair of fields: m_value, a JS wrapper object tracked by a WriteBarrier so the GC knows to mark it, and m_function, metadata including an RTT pointer used for call_indirect signature checks. A prior commit (313985@main) added an optimization where Function::isEmpty() checks only m_function.rtt, and the GC's visitAggregateImpl used that check to skip marking m_value for slots it considered empty. Table::grow's JS-API path — unlike the Wasm-instruction table.grow / table.set path — populated only m_value and left m_function default with a null RTT, so a fully live slot looked empty to the collector. This commit routes the JS path through a new FuncRefTable::Function::setFunction() that writes both halves together, and removes the isEmpty() short-circuit from visitAggregateImpl as a safety net.
void FuncRefTable::Function::setFunction(VM& vm, JSCell* owner, WebAssemblyFunctionBase* function)
{
m_function = function->importableFunction();
m_value.set(vm, owner, function);
}
Before:
JS: table.grow(n, fn) ──► Table::grow()
├─ slot.m_value = fn (write barrier, live)
└─ slot.m_function = {} (rtt = null)
GC::visitAggregateImpl(slot):
if (slot.isEmpty()) // !m_function.rtt, true here
skip marking slot.m_value // wrapper unmarked despite being live
fullGC() ──► wrapper reclaimed ──► table.get(n) returns dangling cell
Significance
The bug is reachable directly from JS via the standard WebAssembly.Table API and produces a genuine use-after-free: table.get() can return a JSValue pointing to GC-reclaimed memory after a full collection. The half-populated slot also caused call_indirect traps, since the missing RTT is exactly what signature checking consults.
Audit directions
A GC-liveness invariant — isEmpty() used as a proxy for "has a value to mark" — was violated by a code path that updates only half of a two-field representation, creating a marking gap. Narrow: audit the other Wasm reference-type table operations and externref tables for paths that write m_value or its equivalent without the matching metadata write; the match tell is any assignment to a WriteBarrier-tracked field that is not adjacent to the metadata write a liveness predicate consults. Wider: sweep visitAggregateImpl and visitChildren implementations across JSC for any that gate marking on a cheap flag rather than directly checking the tracked value's nullness — the shape to notice in review is a continue or early return inside a marking loop whose condition reads a field other than the one being marked. Widest: the invariant is that a liveness predicate must be derived from the reference being kept alive, not from a sibling field that a partial write can leave stale — it transfers to any tracing collector with multi-field slot representations, and to generation/epoch flags used to skip work in mark phases.