This Week in WebKit — March 14–20, 2026
Featured
The surrounding queryCounterEXT code was meticulous — protect() wrappers around the script execution context, the event loop, the GL context — but the query parameter itself was captured plain by [&] into a microtask. Microtasks run after the JS frame returns, which gives JavaScript a clean window to drop every reference to the query and force GC. When the microtask fires it calls makeResultAvailable() through the dangling reference; reclaim the slot with a controlled allocation and the method body operates on attacker bytes. Reachable from any page that creates a WebGL 2 context.
Wasm prefixed opcodes (0xFB GC, 0xFD SIMD) use a VarUInt32 LEB128 sub-opcode, and the spec explicitly permits redundant encodings — ref.i31 can be 1, 2, or even 5 bytes for the same value 28. The validator in WasmFunctionParser decodes LEB128 properly. IPInt — JSC's lowest-tier in-place interpreter — kept a hardcoded advancePC(2). Craft a module with a 3-byte sub-opcode encoding and the PC drifts one byte while the metadata cursor advances normally; from that point every subsequent decode reads bytes at the wrong offset against metadata intended for a different instruction. Wrong type indices on GC ops, wrong branch targets, wrong memory operands — all from a module the validator just blessed.
The wrapper class ZStream carried a single m_isInitialized boolean — but no record of which init path had actually run. Both CompressionStream (deflateInit2) and DecompressionStream (inflateInit2) shared the same destructor, which unconditionally called deflateEnd. zlib's deflate and inflate maintain wholly different internal allocations and bookkeeping inside the same z_stream, so feeding inflate state into deflateEnd causes zlib to misinterpret structure pointers and free addresses derived from the wrong layout. The commit author notes they couldn't reproduce a crash on their test bench — which is the more uncomfortable outcome, because that means the heap was being silently corrupted on every DecompressionStream destruction.
WebPageProxy::mouseEventHandlingCompleted placed its MESSAGE_CHECK(!queue.isEmpty()) right before the takeFirst() at the bottom of the function — exactly where you'd expect the guard to live. But under Site Isolation, when the IPC carries a remoteUserInputEventData field for cross-origin frame routing, the handler enters an earlier branch that calls queue.first() with no guard at all. A compromised WebContent renderer sets the field while the queue is empty, the UI process dereferences an empty deque, and the UI process — not the malicious renderer — is the one that dies. The fix relocates the guard to function entry; the lesson is that 'guard before the dangerous call' is brittle the moment there's more than one dangerous call.
WebKit's old DocumentFragment append called the script-disallowed insertion path once per child node — meaning childrenChanged fired between siblings, MutationEvents dispatched mid-batch, and any MutationObserver scheduled by node N could re-enter and mutate the tree before node N+1 landed. Any insertion-time invariant that depended on sibling counts, parent ancestry, or post-insertion ordering had a re-entrancy window the width of an observer callback. The refactor drives the entire batch under a single ScriptDisallowedScope, fires childrenChanged once for the whole batch, and only dispatches insertion events after every node is in place. Code that assumed 'one MutationRecord per inserted node' will now see one record with every node in addedNodes.