[4] Use-after-free in CSSFontFace::setStatus via CSSFontFace::load
The observer loop that kept every client alive — but not registered
Rated High because the diff adds a membership recheck guarding an observer callback that a re-entrant style recalc can detach mid-loop, producing a web-reachable use-after-free; escalation to a read/write primitive requires reclaiming the freed collateral, which the diff does not establish.
Fixed a use-after-free in CSS font-load client iteration. The iterateClients helper snapshotted the weak client set into a Vector<Ref<CSSFontFaceClient>> and unconditionally invoked each callback; the patch wraps the callback in a membership recheck.
Source/WebCore/css/CSSFontFace.cpp
LayoutTests/fonts/font-face-load-crash.html
Patch Details
The patch modifies the file-static iterateClients. Previously it snapshotted the weak client set into a Vector<Ref<CSSFontFaceClient>> via copyToVectorOf and unconditionally invoked callback(client) for every entry. The patch wraps the callback in if (clients.contains(client)) callback(client);. Every mutator that notifies observers routes through this helper (setFamily, setWeight, and per the commit title the setStatus/load path), so the guard applies to all client-notification loops.
Failure to re-validate observer-set membership across a JavaScript re-entrancy boundary in an iterate-then-callback loop.
Background
CSSFontFaceClient is an observer interface implemented by objects (CSS font selectors, FontFace wrappers) that want to be told when a CSSFontFace changes family/weight/loading status. WeakHashSet<CSSFontFaceClient> is a set of weak references to those clients, so entries drop automatically when a client is destroyed. copyToVectorOf<Ref<CSSFontFaceClient>>(clients) snapshots the weak set into strong references so the loop has a stable list and each referenced client stays alive while iterated. Re-entrancy here is a point where native C++ calls into JavaScript — promise resolution consulting FontFace.prototype.then — which can synchronously run script and mutate C++ state before returning. Style recalculation, forced by reading layout-dependent properties such as offsetHeight, can rebuild the font face set and register/unregister clients. The normal flow: FontFace.load() → CSSFontFace::load() → setStatus() → iterateClients() walks the snapshot and calls each observer to report the loading-state change.
Analysis
This is a use-after-free via stale-observer invocation driven by JavaScript re-entrancy during observer iteration. Before the fix, iterateClients copied the weak client set into a vector of strong Ref<CSSFontFaceClient> and invoked each client's callback without re-checking that the client was still a registered member. The Ref snapshot keeps each client object's memory alive for the loop, so this is not a raw dangling-pointer-to-client bug — the copy deliberately protects against the client object itself being freed. The problem is that a callback earlier in the loop can re-enter script (the load path resolves a promise, and the attacker's FontFace.prototype.then getter runs arbitrary JS), and that JS forces a style recalculation that unregisters a client and tears down the state its notification callback depends on.
The test poisons FontFace.prototype.then with a getter that removes a <style> element and reads document.body.offsetHeight to force synchronous style recalculation, then calls face.load(). When the loop reaches the now-stale but still Ref-alive client and invokes its callback, that callback operates on a client that has been logically detached — dereferencing associated objects (the CSSFontFace/font-selector/wrapper linkage) that the re-entrant recalc has already destroyed. The missing invariant is that a client snapshotted at loop entry is still a live registered observer at the moment its callback fires.
This is a web-reachable UAF in font-load client iteration, reliably a renderer crash. If the freed associated state is reclaimed by attacker-controlled heap content before the callback dereferences it, this would evolve into a controlled UAF primitive; realizing that best case would require heap grooming to place attacker data in the freed slot and a callback path that reads or writes through the dangling linkage.
This vulnerability weakens memory safety inside the WebContent process. The security model assumes observer callbacks in CSSFontFace::iterateClients are delivered only to clients that remain registered and whose backing state is live for the notification loop; before the fix this was violated whenever a callback re-entered script and mutated the font/style graph. An attacker running only ordinary web content could drive the UAF; a stronger primitive would still require a separate sandbox escape to leave the renderer.
This is the canonical WebKit re-entrancy pattern: a snapshot-then-iterate loop over an observer set where the snapshot protects object lifetime (via Ref) but not logical membership. Copying to Ref defends against the object being freed, which lulls reviewers into thinking the loop is safe — but the real hazard is that a re-entrant callback detaches an observer and tears down its dependencies. The fix (recheck contains before each callback) is the correct minimal pattern and is worth propagating to every iterateClients-style loop in WebCore.
Note: The routing of the load/setStatus path through iterateClients, the exact freed object and dangling linkage, and the escalation to a controlled primitive are inferred from the commit title and surrounding code rather than directly shown in the diff. The re-entrancy trigger and the membership-recheck fix are directly supported by the patch and test.
Audit directions
- Snapshot-into-strong-refs then callback without re-checking membership, where a callback can re-enter JS. Audit every WebCore helper that copies a
WeakHashSet/HashSetof clients into aVector<Ref<>>and loops calling back — grep forcopyToVectorOfandcopyToVectorfeeding a callback loop, and confirm each iteration re-validates the element is still registered before invoking it. - Notification paths that resolve promises or dispatch events while iterating internal state. Trace CSS font-loading notification callers (
CSSFontFace::load/setStatus/setFamily/setWeight→iterateClients) and verify none cache raw pointers to font selectors, wrappers, or cached fonts across the callback. Start withCSSFontFaceSetandFontFace::loadForBindings, which sit on the same re-entrant load path. - Forced style recalculation triggered from within observer callbacks. Investigate other WebCore subsystems that iterate observer lists during style/layout-adjacent operations (font selectors, image/resource clients, MediaQuery listeners) and check whether a re-entrant recalc can unregister and free a member mid-iteration.