← All issues

[JSC] Use FrameTracer in wasm ref_func, table_get, and array_init_elem operations

Component: JSC WebAssembly | f771c50

In JSC's Wasm implementation, funcref values are lazily converted into full JS function wrapper objects the first time they're observed from JS or copied between tables and arrays (via ensureFunctionWrapper and friends). That wrapper allocation can trigger GC. Wasm tiers only update the VM's topCallFrame pointer just-in-time, so after a JS import call returns, topCallFrame can point at now-dead native state; a FrameTracer keeps the frame state consistent for the collector and for ShadowChicken — the mechanism that reconstructs native call stacks for profilers and debuggers — across such a GC.

Source/JavaScriptCore/wasm/WasmOperations.cpp

JSC_DEFINE_NOEXCEPT_JIT_OPERATION(operationGetWasmTableElement, EncodedJSValue, (JSWebAssemblyInstance* instance, unsigned tableIndex, uint64_t index))
{
+ // FuncRefTable::get materializes the funcref's JS wrapper on demand.
+ CallFrame* callFrame = DECLARE_WASM_CALL_FRAME(instance);
+ assertCalleeIsReferenced(callFrame, instance);
+ VM& vm = instance->vm();
+ WasmOperationPrologueCallFrameTracer tracer(vm, callFrame, OUR_RETURN_ADDRESS);
return tableGet(instance, tableIndex, index);
}

Source/JavaScriptCore/wasm/WasmIPIntSlowPaths.cpp

WASM_IPINT_EXTERN_CPP_DECL(ref_func, unsigned index)
{
+ WasmSlowPathWithoutCallFrameTracer tracer(instance->vm());
IPINT_RETURN(Wasm::refFunc(instance, index));
}

The commit adds a frame tracer to the ref.func, table.get, and array.init_elem operations across both the JIT operation path and the IPInt slow path, on the grounds that all three can materialize JS function wrappers for funcrefs and therefore all three can GC.

Without a FrameTracer, a GC during funcref wrapper materialization can hit a stale topCallFrame left by a prior JS import call, letting ShadowChicken read dead native state as a live JS CallFrame. That is a frame-tracing consistency bug reachable from ordinary Wasm code that uses tables or typed function references — no exotic module shape required.

The pattern worth carrying forward is a slow path that can allocate — and therefore GC — without establishing frame state, in a tier that updates topCallFrame only just-in-time. The array-init-elem test's own comment records that an earlier audit of Wasm GC slow paths missed this opcode precisely because it doesn't allocate the array itself, which is the discriminator to hunt on: allocation that happens inside a callee rather than at the opcode's own top level. Narrow: sweep WasmIPIntSlowPaths.cpp and WasmOperations.cpp for other operations that call ensureFunctionWrapper, copyElementSegment, or similar wrapper-materializing routines and still lack a tracer. Wider: use the accompanying stress tests' --slowPathAllocsBetweenGCs=1 --forceGCSlowPaths=true options as a template — that option pair converts the missed-tracer condition from a timing accident into a deterministic trigger, which is what makes the sweep tractable. Widest: any runtime with a lazily-maintained stack-walk anchor has this shape wherever a helper allocates transitively; the review tell is a slow-path function body whose only statement is a call into a helper, with no tracer declaration above it.