10740b3a1fa50537a0fbea349eb9130ac609d852 [Site Isolation] Cross-origin isolation should belong to the browsing context group
Triage note: Threads WebCore::CrossOriginMode through BrowsingContextGroup/WebProcessPool so an Isolated process is never reused, prewarmed, BFCache/suspended-reused, or shared with a Shared context (canReuseAnExistingProcess guards, crossOriginMode()==checks on swap/reuse paths), adds ServiceWorkerFetchTask COEP navigation-response blocking (doCrossOriginEmbedderPolicyHandlingOfNavigationResponse), and fixes Document::crossOriginIsolated() to use its own COOP value. These are concrete cross-origin-isolation enforcement/process-separation fixes (SharedArrayBuffer/high-res-timer-capable isolated docs must not co-reside with other-origin content) reachable from web content.
The bug at a glance
A set of cross-origin-isolation enforcement fixes in the UIProcess/NetworkProcess process-management layer. The pre-patch code never threaded WebCore::CrossOriginMode through BrowsingContextGroup / WebProcessPool, so a cross-origin-isolated WebProcess (one that has SharedArrayBuffer and high-resolution timers enabled because its top document is COOP:same-origin + COEP:require-corp) could be reused from the process cache, taken from the prewarm pool, reused via in-process/BFCache suspended pages, or shared with a window.open / related page belonging to a non-isolated (Shared) context. It additionally makes Document::crossOriginIsolated() consult the frame’s own COOP value instead of the main frame’s, and adds COEP navigation-response blocking for service-worker-served main resources. The proven defect is a security-boundary / process-separation failure reachable from web content; it does not corrupt WebKit’s own heap. Escalation to actual cross-origin memory disclosure requires a separate Spectre-class side-channel gadget (inferred), so this is a high-severity isolation-guarantee / mitigation-bypass bug rather than direct RCE.
The whole point of COOP+COEP cross-origin isolation is to guarantee that a SharedArrayBuffer-capable document never shares an address space with other-origin content; the hook is that every process-selection path (cache, prewarm, BFCache, single-process, opener/related-page sharing) silently ignored isolation state until this patch forced a CrossOriginMode match.
Root cause
The violated invariant: a WebProcess created with CrossOriginMode::Isolated must never host, and must never be reused/shared for, content of a different CrossOriginMode. Before the patch WebProcessPool::processForSite() took no CrossOriginMode argument, so webProcessCache().takeProcess()/takeSharedProcess()/takeCOOPProcess(), tryTakePrewarmedProcess(), the usesSingleWebProcess() fast path, and canShareProcessWithOpener() all returned existing processes without regard to isolation. BrowsingContextGroup had no crossOriginMode at all (default constructor), and getOrCreateBrowsingContextGroup()/suspendCurrentPageIfPossible()/browsingContextGroupForNavigation()/triggerBrowsingContextGroupSwitchForNavigation() minted fresh groups with no isolation tag, so suspended-page, BFCache-entry, and target-back/forward process reuse in WebProcessPool::processForNavigation()/processForNavigationInternal() could hand an isolated process to a Shared navigation (and vice-versa). The patch adds a const m_crossOriginMode to BrowsingContextGroup, threads CrossOriginMode into processForSite() and tryTakePrewarmedProcess(), and gates every reuse site on canReuseAnExistingProcess = (crossOriginMode == CrossOriginMode::Shared) or an explicit crossOriginMode()==equality check (prewarm, BFCache/suspended, swappedProcessesPerRegistrableDomain, canShareProcessWithOpener, APIPageConfiguration::preferredBrowsingContextGroup now requires the related page’s group to be Shared). Separately, Document::crossOriginIsolated() previously returned the main frame document’s COOP==SameOriginPlusCOEP, so a subframe inherited the top document’s isolation verdict instead of evaluating its own COOP; the fix returns this document’s own crossOriginOpenerPolicy().value. Finally NetworkResourceLoader::doCrossOriginEmbedderPolicyHandlingOfNavigationResponse() is added and called from ServiceWorkerFetchTask::processResponse() so a service-worker-produced main-resource navigation response that fails shouldInterruptNavigationForCrossOriginEmbedderPolicy() is rejected with an AccessControl ResourceError, closing a path that previously skipped COEP navigation enforcement.
Key code
processForSite() gains CrossOriginMode; reuse is only allowed for Shared
Ref<WebProcessProxy> WebProcessPool::processForSite(..., ProcessSwapDisposition processSwapDisposition, CrossOriginMode crossOriginMode, const std::optional<SecurityOriginData>& coopOrigin)
{
// Reusable processes are never isolated.
bool canReuseAnExistingProcess = crossOriginMode == CrossOriginMode::Shared;
if (isolatedProcessType == WebProcessProxy::IsolatedProcessType::Shared) {
ASSERT(canReuseAnExistingProcess);
...takeSharedProcess()...
} else if (canReuseAnExistingProcess && site && !site->isEmpty() && processSwapDisposition != ProcessSwapDisposition::COOP) {
...webProcessCache().takeProcess()...
} else if (canReuseAnExistingProcess && processSwapDisposition == ProcessSwapDisposition::COOP && coopOrigin) {
...takeCOOPProcess()...
}
if (RefPtr process = tryTakePrewarmedProcess(..., crossOriginMode)) { ... }
if (canReuseAnExistingProcess && usesSingleWebProcess()) { ... }
Ref process = createNewWebProcess(..., crossOriginMode, ...);
}
// canShareProcessWithOpener():
if (openerProcess.crossOriginMode() != pageConfiguration.crossOriginMode())
return false;
Patch walkthrough
Source/WebCore/dom/Document.cpp— Document::crossOriginIsolated() stops delegating to mainFrameDocument()->crossOriginOpenerPolicy() and instead evaluates this document’s own COOP (crossOriginOpenerPolicy().value == SameOriginPlusCOEP). A subframe no longer inherits the top frame’s isolation verdict; it must itself be SameOriginPlusCOEP to claim cross-origin isolation (SAB/precise timers/agent cluster).Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp / .h— Adds doCrossOriginEmbedderPolicyHandlingOfNavigationResponse(): for a main-resource whose response fails shouldInterruptNavigationForCrossOriginEmbedderPolicy(), it returns an AccessControl ResourceError ‘Navigation was blocked by Cross-Origin-Embedder-Policy’. Declared alongside the existing COOP navigation handler.Source/WebKit/NetworkProcess/ServiceWorker/ServiceWorkerFetchTask.cpp— processResponse() now calls the new COEP navigation handler (and didFail()s on error) before doCrossOriginOpenerHandlingOfResponse(), so a service-worker-served navigation response is subject to COEP enforcement that it previously bypassed.Source/WebKit/UIProcess/API/APIPageConfiguration.cpp / .h— preferredBrowsingContextGroup() will only adopt a related page’s group when that group’s crossOriginMode() == Shared (‘only an opener can bring a page into a cross-origin-isolated group’). Adds PageConfiguration::crossOriginMode(), derived from the preferred group (default Shared).Source/WebKit/UIProcess/BrowsingContextGroup.cpp / .h— BrowsingContextGroup gains a const m_crossOriginMode set at construction (create(CrossOriginMode)); sharedProcessForSite() refuses to create a shared site-isolation process when the group is Isolated; ensureProcessForSite() asserts the incoming process matches the group’s mode; the mode is forwarded into processForSite().Source/WebKit/UIProcess/ProvisionalPageProxy.cpp— Sets creationParameters.browsingContextGroupIdentifier to the provisional group’s identifier so the new WebPage is created in the correct (isolation-tagged) browsing context group.Source/WebKit/UIProcess/WebPageProxy.cpp— getOrCreateBrowsingContextGroup()/browsingContextGroupForNavigation() now create Shared groups explicitly and, for loads that skip COOP enforcement (substitute data, custom URL-scheme handlers, about:), downgrade a carried-over Isolated group to a fresh Shared group. launchProcess() and the opener/related-page reuse branch require the related page’s crossOriginMode to match. suspendCurrentPageIfPossible() keeps the suspended page’s mode on its placeholder group. triggerBrowsingContextGroupSwitchForNavigation() derives crossOriginMode from the BrowsingContextGroupSwitchDecision (NewIsolatedGroup => Isolated) and only reuses the provisional process when Shared.Source/WebKit/UIProcess/WebProcessPool.cpp / .h— processForSite() and tryTakePrewarmedProcess() take a CrossOriginMode; every cache/prewarm/single-process/COOP-cache reuse path is gated on canReuseAnExistingProcess (== Shared); processForNavigation()/processForNavigationInternal() derive the mode from the BrowsingContextGroup and add crossOriginMode()==equality checks to suspended-page, in-process BFCache, lastProcessIdentifier, and swappedProcessesPerRegistrableDomain reuse, plus a ‘process swap due to cross-origin isolation change’ branch. Adds crossOriginIsolatedProcessIdentifiers() (and WKProcessPool SPI _crossOriginIsolatedProcessIdentifiersForTesting) to let tests assert isolated processes are disjoint.Source/WebKit/UIProcess/API/Cocoa/WKProcessPool.mm / WKProcessPoolPrivate.h— Exposes -_crossOriginIsolatedProcessIdentifiersForTesting, the test-only observable backing the regression check that an isolated document’s process never coincides with any other process.
Background
Cross-origin isolation (COOP+COEP) — A document becomes ‘cross-origin isolated’ only when it is served Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (SameOriginPlusCOEP). This state re-enables SharedArrayBuffer and high-resolution performance.now()/timers, which were restricted after Spectre; the security bargain is that such a document must be in a process containing no other-origin content.
CrossOriginMode (Isolated vs Shared) — WebCore enum marking a WebProcess/BrowsingContextGroup as serving cross-origin-isolated content (Isolated) or ordinary content (Shared). Before this patch it was set on createNewWebProcess but not propagated through the process-selection and group machinery.
BrowsingContextGroup / WebProcessPool process reuse — WebKit aggressively reuses WebProcesses: a per-site process cache, a prewarm pool, COOP-origin caches, back/forward-cached and suspended pages, single-process mode, and opener/related-page sharing for window.open. Each is a place an isolated process could be handed to non-isolated content if isolation is not checked.
COEP navigation-response enforcement — Per HTML’s process-a-navigate-fetch, a navigation response to a COEP:require-corp context must itself satisfy COEP or the navigation is blocked. Service-worker-produced responses flow through ServiceWorkerFetchTask, a path that previously ran the COOP handler but not an equivalent COEP navigation handler.
SharedArrayBuffer as a Spectre primitive — A SharedArrayBuffer backed by a shared-memory thread gives a high-resolution timer and the memory substrate for Spectre-style transient-execution side channels; co-residency of an SAB-capable document with cross-origin data is what makes such a read possible, which is exactly the boundary process isolation enforces.
Vulnerability window
- Setup — Attacker (or a benign isolated site) serves a document with COOP:same-origin + COEP:require-corp so it is granted cross-origin isolation and runs in a CrossOriginMode::Isolated WebProcess.
- Trigger — A navigation or window creation reaches a reuse path that ignored isolation: a cached/prewarmed/single/COOP-cached process is selected, a BFCache/suspended page is resumed, or a window.open/related-page shares the opener’s process – without a CrossOriginMode match.
- Co-residency — An Isolated process ends up hosting (or being shared with) a Shared, other-origin document – or an isolated subframe is mis-classified by Document::crossOriginIsolated() off the main frame’s COOP.
- Capability grant — The co-resident content now sits in an address space where SharedArrayBuffer and precise timers are enabled, i.e. inside the process boundary that cross-origin isolation was supposed to keep it out of.
- Disclosure (inferred) — Using a Spectre/transient-execution gadget and the SAB-backed high-resolution timer, cross-origin memory in the shared process is read – the information-disclosure outcome the mitigation exists to prevent.
Proof of concept
No full regression test ships in this bundle; the only shipped artifact is the test-only SPI -[WKProcessPool _crossOriginIsolatedProcessIdentifiersForTesting] plus WebProcessPool::crossOriginIsolatedProcessIdentifiers(), which enumerate the PIDs of Isolated processes so a test can assert they are disjoint from all other process PIDs. Conceptual trigger (web path, inferred): host an isolated page A (COOP:same-origin, COEP:require-corp) so it runs in an Isolated process; then exercise a reuse path that pre-patch ignored isolation – e.g. A does window.open() of a non-isolated cross-origin URL, or navigate A away and back through BFCache, or rely on a warmed/single process – and observe (pre-patch) that the non-isolated document lands in the same PID as A. Demonstrating actual cross-origin reads from that co-residency additionally requires a Spectre gadget, which is not part of this diff.
Exploitation
- Reach — Fully web-reachable: COOP/COEP response headers, window.open, back/forward navigation, and service-worker fetch handlers are all script-controllable, and the process-selection paths run in the UIProcess/NetworkProcess.
- Trigger — Steer process selection so an Isolated process is reused for or shared with Shared content (prewarm/cache/single/BFCache/opener), or mis-report a subframe as isolated via the old mainFrameDocument() COOP check.
- Co-residency primitive — Achieve same-process co-residency of an SAB/precise-timer-capable isolated document with cross-origin content – the collapse of the process-isolation boundary.
- Side channel (inferred) — Build a Spectre/transient-execution read using the SAB-backed timer against the co-resident cross-origin memory.
- Impact — Cross-origin information disclosure (and a weakened COEP navigation boundary via the service-worker path); not memory corruption of WebKit’s own heap.
Detection & hunting
For defenders and detection engineers:
- Isolated process hosting Shared content — In tests/instrumentation, cross-reference _crossOriginIsolatedProcessIdentifiersForTesting against the PIDs of all other pages/frames; any intersection is the bug.
- Process reuse across CrossOriginMode — Audit WEBPROCESSPOOL_RELEASE_LOG(ProcessSwapping) ‘Using … from cache/prewarmed/COOP WebProcess’ lines for cases where the selected process’s crossOriginMode differs from the requested navigation’s.
- Service-worker navigation lacking COEP block — Watch ServiceWorkerFetchTask navigation responses into a COEP:require-corp context that are not rejected with errorDomainWebKitInternal ‘Navigation was blocked by Cross-Origin-Embedder-Policy’.
Audit directions
- Other process-selection entry points — Review every WebProcessPool/BrowsingContextGroup caller that obtains a process (GPU/Networking helpers, prewarm refill, site-isolation shared process, blob-URL dedicated process in createNewPage) for a missing CrossOriginMode match.
- Loads that skip COOP/COEP enforcement — The patch’s FIXMEs flag custom URL-scheme-handler and about:/substitute-data subframe loads that can still land in an isolated process; audit those and any other enforcement-skipping navigation for isolation downgrade gaps.
- Capability checks keyed off main frame — Look for other per-document capability queries that consult mainFrameDocument() instead of the document’s own security context (agentClusterID, SAB gating, timer resolution).
- BFCache / suspended-page resume — Verify all suspended-page and back/forward-cache resume paths (including multi-process BFCache) carry and re-check crossOriginMode, since these reuse long-lived processes across navigations.