← WebKit Silent-Fix Report — 2026-W22

1ad0d2a7c3  Cross-Process Page Identity Confusion in didPostMessage

severity medium class CrossOrigin confidence 0.70 WebKit UIProcess site-isolation exploitable-grade
Chris Dumez Fri May 29 14:17:54 2026 -0700 full: 1ad0d2a7c3b7af30ae4b1baf4fdd544d52276b53 bug report ↗ view on GitHub ↗
Primitive: WebPage::Close routed to wrong page identity
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&&); adds HashCountedSet<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

  1. 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.
  2. Compromise — An attacker compromises a WebProcess (e.g. via a renderer bug) and controls the WebPageProxyIdentifier it sends.
  3. 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.
  4. Fix — A MESSAGE_CHECK requires isAssociatedWithPage(pageID) for the sender; forged identifiers are rejected and the connection flagged.
  5. 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

  1. Compromise a WebProcess — Gain code execution in a renderer/WebProcess via a separate bug – the assumed starting point for site-isolation IPC attacks.
  2. 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).
  3. 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.
  4. 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.

Before / after

Loading diff…