← All issues

Site Isolation UI process crash via unchecked mouse event queue access

a73af3d

Source/WebKit/UIProcess/WebPageProxy.cpp

void WebPageProxy::mouseEventHandlingCompleted(std::optional<WebEventType> eventType, bool handled, std::optional<RemoteUserInputEventData> remoteUserInputEventData)
{
+ MESSAGE_CHECK(m_legacyMainFrameProcess, !internals().mouseEventQueue.isEmpty());
if (remoteUserInputEventData) {
CheckedRef event = internals().mouseEventQueue.first();
...
}
 
// Retire the last sent event now that WebProcess is done handling it.
- MESSAGE_CHECK(m_legacyMainFrameProcess, !internals().mouseEventQueue.isEmpty());
auto event = internals().mouseEventQueue.takeFirst();

In WebKit's multi-process architecture, the web content process sends DidReceiveEvent IPC messages back to the UI process to acknowledge input event handling. Under Site Isolation, events routed to cross-origin subframes carry a remoteUserInputEventData field that tells the UI process which frame the event should be forwarded to. The mouseEventQueue is a deque in the UI process that tracks in-flight mouse events awaiting acknowledgment. MESSAGE_CHECK is a macro that kills the connection to the sending process when a validation condition fails — it's the primary defense against malformed IPC from compromised renderers.

The bug was structural: the MESSAGE_CHECK(!queue.isEmpty()) guard only protected the takeFirst() call at the bottom of the function. But when remoteUserInputEventData was present, the function took an earlier branch that called queue.first() without any guard. A compromised web content process could send a crafted DidReceiveEvent with remoteUserInputEventData set while the queue was empty, triggering an unchecked deque::first() on an empty container — crashing the UI process rather than killing the compromised renderer's connection.

Before:                                    After:
mouseEventHandlingCompleted()              mouseEventHandlingCompleted()
  │                                          │
  ├─ if (remoteUserInputEventData) {         ├─ MESSAGE_CHECK(!queue.isEmpty())
  │     queue.first()  ← NO CHECK            │
  │     ...                                  ├─ if (remoteUserInputEventData) {
  │  }                                       │     queue.first()  ← now guarded
  │                                          │     ...
  ├─ MESSAGE_CHECK(!queue.isEmpty())         │  }
  │                                          │
  └─ queue.takeFirst()                       └─ queue.takeFirst()

A compromised web content process could deliberately crash the UI process by sending a crafted DidReceiveEvent IPC message — a useful primitive in a multi-stage exploit for disrupting browser process integrity. This class of bug — IPC validation bypass via alternate code paths where a MESSAGE_CHECK guards one path but not a conditional branch above it — is historically common in WebKit's process separation layer.

🔒

Sibling IPC handlers may share this guard placement pattern — and the fixed path still has processing worth examining.

Subscribe to read more