This Week in WebKit — May 02 - May 08, 2026
Featured
Style::SubstitutionResolver already had a working idiom for attr()-tokenizer output: an m_intermediateTokenStrings vector that anchors transient backing strings until CSSVariableData::create re-captures them. When CSS Mixins added @function resolution alongside it, the resolved CustomProperty got dropped at scope exit while the tokens it produced — non-owning views into its StringImpl payloads — sat in the resolver's buffer waiting for the outer substitute() call. By the time CSSVariableData::create reads those tokens, the backing has been freed. A heap-shaped reuse between drop and re-capture would let attacker-chosen bytes flow into the parsed token stream.
DocumentThreadableLoader is RefCounted and routinely outlives its Document: a frame detach during in-flight CORS preflight, redirects, error reporting after navigation. It cached the document as WeakPtr<Document> precisely to avoid extending its lifetime, then accessed it through a Document& document() { return *m_document; } accessor that hides the null case in the type system. WebKit's WeakPtr::operator* aborts on null in release builds — that is the safer-CPP win against UAF — but the abort lives at every callback after the document is gone. An iframe issuing a custom-headers fetch() and then removing itself reliably terminates the renderer.
When OMG lowered struct.new_default/array.new_default, it derived the IR type of each zero-initialiser purely from elementSize(): ≤ 4 bytes got Int32(0), 8 bytes got Int64(0). For an f32 field the bit pattern is correct (zero is zero) but the B3 type is wrong — and the moment B3's CSE spots a Float-typed load right after the Int32 store, Value::replaceWithIdentity trips a RELEASE_ASSERT and the renderer dies. A struct with a single f32 field plus struct.get in a hot loop is enough to crash any tab.
IPInt's baddpc macro adds the static immediate offset to a runtime pointer and traps on carry — the standard guard for Memory64's full 64-bit address arithmetic. Every regular load/store fast path used it; memory.atomic.wait32/wait64 used a bare addq. Hand a Memory64 module the pointer 0xFFFF_FFFF_FFFF_FFF8n with offset=8, and the engine sees address 0 — perfectly in-bounds — and proceeds to wait on a wrapped arbitrary location. The bounds check never noticed.
Under strict-dynamic, parser-inserted scripts must carry a nonce; only dynamically-inserted scripts ride on transitive trust. WebKit splits the check: allowScriptForStrictDynamic runs at request time and is the only gate for parser-inserted scripts, while allowScriptFromSource runs at fetch time and is hardcoded to return true under strict-dynamic on the assumption that stage one already ran. requestModuleScript — the external-module path — never made the stage-one call. Pages hardened with script-src 'nonce-X' 'strict-dynamic' were happily loading attacker-injected <script type=module src=…> without a nonce.
Security fixes
-
CSP object-src empty-source-list bypass for no-URL plugin elements
Medium WebCore Content Security Policy enforcement
-
WebGL GPU-process draft-extension setting unenforced across IPC
Medium WebKit GPU process — `GraphicsContextGLANGLE` / `GPUConnectionToWebProcess`
Notable development
-
[JSC] Implement `String#split` in C++
optimization
-
[Wasm] Use a lazy restore frame when returning from tail calls
refactor
-
[JSC] Add Array#concat DFG nodes
optimization
-
[JSC] Fix wasm type parsing regression by making RTT formal canonicalized types
refactor
-
[JSC] IPInt slow path for `memory.atomic.notify` truncates the Memory64 pointer and offset to 32 bits
bug
-
[JSC][WASM][Debugger] Fix STW deadlocks when VM blocks in memory.atomic.wait or WebCore operations
bug
-
[cocoa] AVStreamDataParser accepts media segments not preceded by an init segment
hardening
-
[JSC] GreedyRegAlloc: Add loop-aware live range splitting (disabled by default)
feature