1ad0d2a7c3 Cross-Process Page Identity Confusion in didPostMessage
Triage note: Sends page-close with an explicit page identifier instead of ambient routing, fixing a cross-process page-identity confusion under site isolation.
Contents
The bug at a glance
A cross-process page-identity confusion / missing IPC authorization: WebProcessProxy::didPostMessage() looked up a WebPageProxy by a WebPageProxyIdentifier supplied by a (potentially compromised) WebProcess without verifying that page belongs to the sending process, letting one web process deliver a userscript message as/into a page owned by another process – a site-isolation boundary and cross-origin integrity violation. The primitive is message injection into another process’s page rather than direct memory corruption, and it requires an already-compromised WebProcess, so medium; but the class (unauthenticated pageID in a UIProcess IPC handler) is exactly what site isolation must prevent.
The UIProcess trusted a page identifier coming over IPC. WebPageProxy::fromIdentifier(pageID) resolves any page in the whole UIProcess, not just pages owned by the sender, so a compromised WebProcess could pass another process’s WebPageProxyIdentifier and have didPostMessage operate on it. The fix adds a MESSAGE_CHECK that the page isAssociatedWithPage(pageID) for the sending process, and – crucially – extends isAssociatedWithPage to also count remote pages and pages pending close so legitimate site-isolation flows still validate.
Root cause
WebProcessProxy::didPostMessage() handles a userscript/postMessage-style IPC from a WebProcess carrying a WebPageProxyIdentifier. Pre-patch it did RefPtr page = WebPageProxy::fromIdentifier(pageID) and, if found, proceeded to look up the WebUserContentControllerProxy and deliver the message. fromIdentifier is a global registry lookup: it returns the WebPageProxy for that identifier regardless of which WebProcess owns it. A compromised or malicious WebProcess can therefore forge a WebPageProxyIdentifier belonging to a page hosted in a different process and cause the UIProcess to act on that page’s behalf – cross-process page identity confusion.
The fix inserts MESSAGE_CHECK_COMPLETION(isAssociatedWithPage(pageID), completionHandler(makeUnexpected(String()))) right after the fromIdentifier lookup. MESSAGE_CHECK validates that the sending process is actually associated with the page and, on failure, rejects the message (and typically flags the connection as misbehaving). WebProcessProxy::isAssociatedWithPage was a pre-existing utility but only checked m_pageMap, m_provisionalPages, and m_suspendedPages. Under site isolation a process can legitimately host a page via a RemotePageProxy (a frame of the page living in another process) or be mid-close, so the patch extends isAssociatedWithPage to also iterate m_remotePages and to return true when m_pagesPendingClose.contains(pageID) – otherwise valid site-isolation messages would be wrongly rejected.
The m_pagesPendingClose set is introduced (a HashCountedSet<WebPageProxyIdentifier>) and maintained by a new WebProcessProxy::sendPageCloseMessage(pageProxyID, pageID, completionHandler): it adds pageProxyID to m_pagesPendingClose before sending Messages::WebPage::Close(), and removes it (and calls reportProcessDisassociatedWithPageIfNecessary) in the async reply. All the call sites that previously sent a bare Messages::WebPage::Close() with ambient routing – ProvisionalPageProxy::~ProvisionalPageProxy, RemotePageProxy::disconnect, SuspendedPageProxy::close, WebPageProxy::close, WebPageProxy::commitProvisionalPage – are converted to sendPageCloseMessage with an explicit page identifier so the pending-close window is tracked and the close is routed to the correct page/process. This keeps isAssociatedWithPage accurate during the teardown window and lets the new MESSAGE_CHECK accept in-flight-close pages without opening a hole. isAssociatedWithPage also loses its NODELETE annotation as part of the change.
(The commit message lists WebPage.cpp closeWithReply, WebPage.messages.in and WebUserContentController.cpp among touched files; those hunks are not present in the provided diff, which contains the UIProcess-side changes.)
Key code
Authorization gate plus the pending-close-aware association check (WebProcessProxy.cpp)
RefPtr page = WebPageProxy::fromIdentifier(pageID);
if (!page)
return completionHandler(makeUnexpected(String()));
MESSAGE_CHECK_COMPLETION(isAssociatedWithPage(pageID), completionHandler(makeUnexpected(String())));
// ... in isAssociatedWithPage():
for (Ref remotePage : m_remotePages) {
if (remotePage->page() && remotePage->page()->identifier() == pageID)
return true;
}
for (Ref provisionalPage : m_provisionalPages) {
if (provisionalPage->page() && provisionalPage->page()->identifier() == pageID)
return true;
}
for (auto& suspendedPage : m_suspendedPages) {
if (suspendedPage.page() && suspendedPage.page()->identifier() == pageID)
return true;
}
if (m_pagesPendingClose.contains(pageID))
return true;
return false;
Patch walkthrough
Source/WebKit/UIProcess/WebProcessProxy.cpp— Adds the MESSAGE_CHECK_COMPLETION(isAssociatedWithPage(pageID), …) authorization gate in didPostMessage after fromIdentifier. Adds sendPageCloseMessage(), which records pageProxyID in m_pagesPendingClose, sends WebPage::Close(), and on reply removes it and calls reportProcessDisassociatedWithPageIfNecessary. Extends isAssociatedWithPage to iterate m_remotePages (as Ref) and m_provisionalPages, and to return true when m_pagesPendingClose contains the id.Source/WebKit/UIProcess/WebProcessProxy.h— Declares sendPageCloseMessage(std::optional<WebPageProxyIdentifier>, PageIdentifier, CompletionHandler&&); addsHashCountedSet<WebPageProxyIdentifier> m_pagesPendingClose;and its <wtf/HashCountedSet.h> include; removes the NODELETE annotation from isAssociatedWithPage.Source/WebKit/UIProcess/WebPageProxy.cpp— WebPageProxy::close() now captures pageProxyID = identifier() and calls protect(process)->sendPageCloseMessage(pageProxyID, pageID, scope-holding handler) instead of a bare sendWithAsyncReply(WebPage::Close()). commitProvisionalPage() closes the previous page via protect(legacyMainFrameProcess())->sendPageCloseMessage(identifier(), webPageIDInMainFrameProcess()).Source/WebKit/UIProcess/ProvisionalPageProxy.cpp— ~ProvisionalPageProxy replaces sendWithAsyncReply(WebPage::Close()) with process->sendPageCloseMessage(page->identifier(), m_webPageID), routing the close with an explicit page identity and tracking it as pending-close.Source/WebKit/UIProcess/RemotePageProxy.cpp— disconnect() takes a RefPtr page = m_page and sends the close via m_process->sendPageCloseMessage(page ? std::optional { page->identifier() } : std::nullopt, m_webPageID), passing the page identifier when available.Source/WebKit/UIProcess/SuspendedPageProxy.cpp— close() replaces the bare Close() send with m_process->sendPageCloseMessage(page ? std::optional { page->identifier() } : std::nullopt, m_webPageID).
Background
WebPageProxyIdentifier — A UIProcess-wide identifier for a WebPageProxy. Because WebPageProxy::fromIdentifier resolves it globally, an IPC handler that trusts a sender-supplied identifier without an ownership check can be steered onto another process’s page.
WebProcessProxy::didPostMessage — UIProcess IPC handler delivering a userscript/postMessage to a WebUserContentControllerProxy for a given page. The missing check let it act on pages the sending WebProcess does not own.
isAssociatedWithPage() — WebProcessProxy utility answering whether this process hosts a given page. Originally checked only m_pageMap/provisional/suspended; the patch adds remote pages and pages-pending-close so it is accurate under site isolation and during teardown.
MESSAGE_CHECK — WebKit’s IPC hardening macro that validates an invariant on a received message and, if it fails, rejects the message and marks the connection as having sent bad data – the standard way to enforce process/page ownership in UIProcess handlers.
m_pagesPendingClose (HashCountedSet) — New per-process counted set tracking pages for which a WebPage::Close() is in flight, so isAssociatedWithPage still returns true during the close window and the MESSAGE_CHECK does not reject legitimate late messages.
RemotePageProxy — Represents a page’s presence in a process that hosts one of its frames under site isolation; adding m_remotePages to the association check is what keeps site-isolation tests (and real cross-process messaging) working with the new gate.
Vulnerability window
- Baseline — didPostMessage resolves pageID via fromIdentifier and, if the page exists anywhere in the UIProcess, delivers the message – no check that the sending process owns the page.
- Compromise — An attacker compromises a WebProcess (e.g. via a renderer bug) and controls the WebPageProxyIdentifier it sends.
- Confusion — The malicious process supplies another process’s page identifier; the UIProcess delivers the userscript message as/into that foreign page, crossing the site-isolation boundary.
- Fix — A MESSAGE_CHECK requires isAssociatedWithPage(pageID) for the sender; forged identifiers are rejected and the connection flagged.
- Compatibility — isAssociatedWithPage is extended to remote pages and pending-close pages, and all Close() sends route through sendPageCloseMessage with explicit identity so legitimate site-isolation/teardown messages still validate.
Triggering
No LayoutTest in the diff (the fix is an IPC hardening check against a compromised WebProcess, not reproducible from web content alone). Conceptual trigger: from a compromised WebProcess, send the didPostMessage IPC with a WebPageProxyIdentifier belonging to a page hosted by a different process; pre-patch the UIProcess resolves and operates on that foreign page, post-patch MESSAGE_CHECK_COMPLETION(isAssociatedWithPage(pageID)) rejects it.
Exploitation
- Compromise a WebProcess — Gain code execution in a renderer/WebProcess via a separate bug – the assumed starting point for site-isolation IPC attacks.
- Forge identifier — Send didPostMessage with a WebPageProxyIdentifier for a page owned by another process (e.g. a cross-origin site isolated into its own process).
- Cross-boundary delivery — Pre-patch the UIProcess delivers the userscript/postMessage into the foreign page’s WebUserContentControllerProxy context, injecting content or state across the origin/process boundary.
- Escalate — Depending on the message handler, this can spoof messages a page trusts, tamper with another origin’s userscript state, or feed further logic bugs – a stepping stone in a site-isolation escape chain rather than a direct memory write.
Detection & hunting
For defenders and SOC / detection engineers:
- MESSAGE_CHECK failure in didPostMessage — Post-patch, a rejected didPostMessage with connection-flagged-bad-data indicates a process sending a page identifier it does not own – a strong compromise signal worth alerting on.
- pageID in didPostMessage not owned by sender — Audit/telemetry comparing the sending process against WebPageProxy ownership for postMessage IPCs surfaces cross-process identity confusion attempts.
- Unexpected userContentController messages across origins — Detect userscript message delivery whose target page origin/process differs from the sender’s, especially under site isolation.
Audit directions
- Other UIProcess IPC handlers taking a WebPageProxyIdentifier — Systematically audit WebProcessProxy/WebPageProxy IPC endpoints that resolve a page via fromIdentifier and act on it without a MESSAGE_CHECK(isAssociatedWithPage(…)); this bug class recurs wherever a sender-supplied page/frame id is trusted.
- Completeness of isAssociatedWithPage — Verify the association check now covers every way a process can legitimately reference a page (page map, remote, provisional, suspended, pending-close) so the new MESSAGE_CHECK neither over-rejects nor leaves a bypass; check FrameProcess/site-isolation edge cases.
- sendPageCloseMessage lifecycle — Confirm every Close() path now routes through sendPageCloseMessage with the correct identifier and that m_pagesPendingClose add/remove is balanced (HashCountedSet) across all teardown orders, so a page cannot be stuck ‘pending close’ and remain wrongly associated.
- WebPage.cpp closeWithReply / messages.in — The commit message references WebProcess-side WebPage::Close reply changes not shown in this diff; review the corresponding WebPage.messages.in / closeWithReply to ensure the reply that clears pending-close is always delivered.