CVE-2026-43704
Overview
Background
- WebExtension events
- chrome/browser-style extension events (runtime.Port.onMessage/onDisconnect, devtools panel onShown) dispatch to registered JS listeners.
- Iterator invalidation
- Mutating a collection while iterating by reference invalidates iterators and can free elements still being visited.
- Snapshot + RefPtr
- Copying the list and holding a RefPtr per element makes dispatch resilient to add/remove during callbacks.
Root Cause Analysis
This fixes a use-after-free caused by mutating an event-listener list while iterating it during dispatch in the WebKit web-extension APIs. Three dispatch routines iterated their listener collections by reference and invoked each listener’s JS callback: WebExtensionContextProxy::dispatchDevToolsExtensionPanelShownEvent looped over extensionPanel->onShown().listeners(), and WebExtensionAPIPort::fireMessageEventIfNeeded / fireDisconnectEventIfNeeded looped over m_onMessage->listeners() / m_onDisconnect->listeners(), each calling listener->call(…). Calling into JS can run extension code that adds or removes listeners, mutating the very collection being iterated; the reference-based range-for then walks invalidated iterators / freed listener objects — a use-after-free.
The fix takes a copy of the listener list before iterating (auto listenersCopy = …listeners()) and iterates it as for (RefPtr listener : listenersCopy), so iteration proceeds over a stable snapshot and each listener is retained (RefPtr) across its call().
The restored invariant is that dispatch iterates an immutable snapshot and keeps every listener alive across the callback, regardless of listener mutations triggered by the callback. The regression test registers many onMessage/onDisconnect listeners, and inside a firing listener adds 1024 more, asserting the dispatch counts match the listeners present when the event fired (i.e. no crash and snapshot semantics).
Attack Path
- Register listeners A malicious/buggy web extension registers listeners on a runtime port (onMessage/onDisconnect) or a DevTools panel onShown event.
- Mutate during dispatch When the event fires, a listener callback adds or removes listeners, mutating the collection mid-iteration.
- Iterate freed entries The reference-based loop walks invalidated iterators / freed listener objects.
- Use-after-free Dispatch dereferences a freed listener in the WebContent (extension world) process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebExtensionContextProxy::dispatchDevToolsExtensionPanelShownEventSource/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mm |
modified | Copies onShown().listeners() and iterates the snapshot as RefPtr listeners before calling each, so mutations during call() are safe. |
WebExtensionAPIPort::fireMessageEventIfNeededSource/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm |
modified | Iterates a copied m_onMessage listener list with RefPtr elements to avoid UAF when call() mutates the listeners. |
WebExtensionAPIPort::fireDisconnectEventIfNeededSource/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm |
modified | Same snapshot-and-retain fix for the onDisconnect listener dispatch. |
Files Changed
Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIDevToolsExtensionPanelCocoa.mmSource/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mmTools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIRuntime.mm
Audit Directions
- Other extension dispatch loopsGrep the WebExtension API .mm files for for (… : …listeners()) followed by listener->call() without a prior copy; each is a candidate UAF.
- Observer callbacks that reenterAudit event/observer dispatch across WebKit for iteration over live lists whose callbacks can mutate the list.