e5c4d1be15 [Site Isolation] Don't seed new RemoteFrame with the dying LocalFrame's stale FrameTreeSyncData
Triage note: Reusing the dying frame's origin let the pre-swap origin spuriously match the active document, a same-origin-domain check bypass window.
Contents
The bug at a glance
During a Site-Isolation cross-origin navigation, a freshly created RemoteFrame was seeded with the dying LocalFrame’s FrameTreeSyncData, whose cached frameDocumentSecurityOrigin still reflected the pre-swap (old) document rather than the cross-origin document being navigated to. For a short window before the post-commit refresh IPC arrived, any task querying the remote window (e.g. a BroadcastChannel dispatch from a pagehide handler) could observe a stale origin that spuriously matched the active document — a transient same-origin-with-a-cross-origin-frame condition. The observed consequence in the patch is a debug ASSERT crash in DOMWindow::crossDomainAccessErrorMessage, and BindingSecurity still correctly denied the cross-process access, so this is scored as a correctness/origin-tracking hardening bug with security relevance rather than a demonstrated cross-origin read. It is worth attention because a stale cached origin on a cross-origin frame is exactly the kind of same-origin-policy desync that can become a real leak if any code path trusts the cached value instead of BindingSecurity.
The subtle part is the ordering: the RemoteFrame exists and is queryable before the authoritative FrameTreeSyncDataChangedInAnotherProcess IPC arrives to give it its true origin. Seeding it from the outgoing LocalFrame filled that gap with a plausible-but-wrong origin (the old document’s) instead of leaving it honestly unknown. The fix seeds an empty FrameTreeSyncData with an opaque origin, which by construction never compares same-origin with anything, so the window can only ever be seen as cross-origin until the real values land.
Root cause
Under Site Isolation, when a frame navigates cross-origin its content moves to a different web process. In the origin process the old LocalFrame is torn down and replaced by a RemoteFrame proxy that stands in for the now-out-of-process document. WebFrame::loadDidCommitInAnotherProcess performs this swap in the WebProcess, constructing the new RemoteFrame and its FrameTreeSyncData.
FrameTreeSyncData carries per-frame state that must be kept consistent across processes, including frameDocumentSecurityOrigin — the frame’s document origin as other processes should see it. The correct value for the new RemoteFrame is the origin of the cross-origin document being navigated to. But that value is not known locally at swap time; it is delivered shortly afterwards by the FrameTreeSyncDataChangedInAnotherProcess IPC from the process that now owns the document.
Before the patch the code did Ref frameTreeSyncData = localFrame->frameTreeSyncData(), reusing the dying LocalFrame’s sync data. That object’s frameDocumentSecurityOrigin still described the pre-swap document — the old origin. So during the interval between the swap and the arrival of the refreshing IPC, the RemoteFrame reported a stale origin. If the surrounding page and the old document were same-origin, the RemoteFrame’s cached origin would spuriously equal the active document’s origin, i.e. the code could observe a cross-process remote frame as same-origin.
The window is reachable by ordinary content: the swap is driven from the navigation/unload sequence, and lifecycle events like pagehide run synchronously in that sequence. A pagehide handler that triggers a BroadcastChannel dispatch (or otherwise reaches for the remote window) executes a task that queries the RemoteFrame while its origin is still stale. BindingSecurity::shouldAllowAccessTo correctly denied the access because the target frame is remote (cross-process), but the subsequent DOMWindow::crossDomainAccessErrorMessage path contains an ASSERT(!activeOrigin->isSameOriginDomain(targetOrigin)) — an invariant that the denied access must not be between same-origin-domain origins. The stale cached origin made that assertion false, flakily crashing the debug build (observed on http/tests/site-isolation/page-lifecycle/{pagehide,pageswap,unload}.html).
The fix replaces the reuse with Ref frameTreeSyncData = FrameTreeSyncData::create(), which constructs a fresh sync data whose document security origin is opaque (a unique origin that is never same-origin with anything). This matches the established pattern in WebFrame::createSubframe and WebFrameProxy::remoteProcessDidTerminate. Until the post-commit FrameTreeSyncDataChangedInAnotherProcess IPC arrives over the same connection with the real origin, the RemoteFrame reports an honest opaque “unknown” origin that can never spuriously compare same-origin, eliminating both the assertion crash and the transient same-origin misclassification.
Key code
WebFrame.cpp: seed the new RemoteFrame with a fresh opaque-origin FrameTreeSyncData instead of the dying LocalFrame’s
auto newFrame = [&]() {
// Don't reuse the dying LocalFrame's sync data — its pre-swap origin would remain visible
// until the post-commit FrameTreeSyncDataChangedInAnotherProcess IPC arrives. Opaque is
// an honest placeholder that never spuriously matches the active document.
Ref frameTreeSyncData = FrameTreeSyncData::create();
auto invalidator = protect(localFrameLoaderClient())->takeFrameInvalidator();
auto clientCreator = [protectedThis = Ref { *this }, invalidator = WTF::move(invalidator)] (auto&) mutable {
Patch walkthrough
Source/WebKit/WebProcess/WebPage/WebFrame.cpp— In WebFrame::loadDidCommitInAnotherProcess, the newFrame lambda no longer seeds the replacement RemoteFrame from the dying LocalFrame’s sync data (Ref frameTreeSyncData = localFrame->frameTreeSyncData()). It now creates a fresh FrameTreeSyncData::create() with an opaque origin, so the RemoteFrame carries an honest unknown origin until the authoritative FrameTreeSyncDataChangedInAnotherProcess IPC refreshes it. A comment documents why reuse is unsafe.LayoutTests/platform/mac-wk2/TestExpectations— Removes the flakey-crash expectations for http/tests/site-isolation/page-lifecycle/unload.html, pagehide.html and pageswap.html (previously Skipped in Debug due to the ASSERTION FAILED: !activeOrigin->isSameOriginDomain(targetOrigin) regression from 313507@main), since the underlying stale-origin assertion is now fixed.
Background
Site Isolation, LocalFrame and RemoteFrame — WebKit’s Site Isolation places cross-origin frames in separate web processes. Within any one process a frame is either a LocalFrame (its document lives in this process) or a RemoteFrame (a lightweight proxy for a document living in another process). A cross-origin navigation swaps a LocalFrame for a RemoteFrame in the originating process and vice versa in the destination; the proxies let script hold references (e.g. window.frames[i]) that are serviced across the process boundary under same-origin-policy rules.
FrameTreeSyncData and frameDocumentSecurityOrigin — FrameTreeSyncData is the per-frame state WebKit replicates across processes so each process has a consistent view of the frame tree — including frameDocumentSecurityOrigin, the origin other processes should attribute to the frame’s document. Because a RemoteFrame’s real document lives elsewhere, its FrameTreeSyncData is authoritatively updated by the owning process via the FrameTreeSyncDataChangedInAnotherProcess IPC. Any locally-seeded value is only a placeholder valid until that IPC arrives.
Opaque origin as an honest placeholder — An opaque (unique) origin is a special origin that is not same-origin or same-origin-domain with any other origin, including a copy of itself. Seeding a not-yet-known frame origin as opaque is the safe default: it can never spuriously grant same-origin access, so any policy check errs toward denial until the true origin is supplied. The patch notes this matches WebFrame::createSubframe and WebFrameProxy::remoteProcessDidTerminate, which already use fresh FrameTreeSyncData for the same reason.
BindingSecurity and crossDomainAccessErrorMessage — BindingSecurity is the JS-binding-layer gatekeeper (shouldAllowAccessTo) that enforces the same-origin policy for cross-window/cross-frame property access. When access is denied it builds a diagnostic via DOMWindow::crossDomainAccessErrorMessage, which in debug builds asserts ASSERT(!activeOrigin->isSameOriginDomain(targetOrigin)) — encoding the invariant that a denial should only ever occur between origins that are not same-origin-domain. A stale cached origin that makes the active and target origins look same-origin violates that invariant even though the access was (correctly) denied because the target is remote, producing the crash.
Lifecycle events and the swap window (pagehide/pageswap/unload, BroadcastChannel) — The pagehide, pageswap and unload events fire synchronously as part of the navigation/commit sequence that also performs the cross-process frame swap. Handlers for these events run while the swap is in flight, and can schedule tasks — for instance a BroadcastChannel.postMessage dispatch — that query the newly created RemoteFrame’s window before the authoritative FrameTreeSyncData refresh IPC has been processed. This is the concrete task that observes the stale origin in the failing tests.
Vulnerability window
- Prior change — Commit 313507@main altered the cross-process commit path such that loadDidCommitInAnotherProcess seeded the new RemoteFrame from the dying LocalFrame’s FrameTreeSyncData, carrying the pre-swap origin.
- Regression surfaces — Debug builds began flakily hitting ASSERTION FAILED: !activeOrigin->isSameOriginDomain(targetOrigin) in DOMWindow::crossDomainAccessErrorMessage on the site-isolation page-lifecycle tests; those tests were Skipped in Debug (bug 315211) to keep CI green.
- Diagnosis — Root cause traced to the RemoteFrame briefly exposing the stale pre-swap origin between the swap and the post-commit FrameTreeSyncDataChangedInAnotherProcess IPC, letting a pagehide-driven BroadcastChannel task observe a spuriously same-origin remote window.
- Fix — Zak Ridouh (reviewed by Sihui Liu) changed the seed to a fresh FrameTreeSyncData::create() with an opaque origin, matching createSubframe / remoteProcessDidTerminate, and removed the three flakey test expectations. Landed as bug 315216, commit 313620@main.
Triggering
No standalone PoC ships in the patch, but the reproducer is effectively the three LayoutTests un-skipped by it: http/tests/site-isolation/page-lifecycle/{pagehide,pageswap,unload}.html. Trigger shape: with Site Isolation enabled, embed a cross-origin subframe, then perform a navigation that swaps that frame’s LocalFrame to a RemoteFrame; from a pagehide/pageswap/unload handler on the outgoing document, run a task (e.g. a BroadcastChannel dispatch) that reaches for the remote frame’s window and provokes a cross-origin access check. On an unpatched debug build this hits the ASSERT because the RemoteFrame still reports the old (same-origin) origin; on a patched build the RemoteFrame reports an opaque origin and the check proceeds normally.
Exploitation
- Setup — Attacker page with Site Isolation active hosts a cross-origin frame and initiates a cross-origin navigation of it, arranging a pagehide/unload handler that schedules a query of the swapping frame’s window during the swap window.
- Observe stale origin — During the brief interval before FrameTreeSyncDataChangedInAnotherProcess arrives, the new RemoteFrame reports the outgoing document’s origin; if that origin matches the active document, the frame transiently appears same-origin.
- Practical limit — Observed impact is a debug-only assertion crash — crash-only, and only on debug builds. BindingSecurity still denied the actual cross-process access because the target is remote, so no cross-origin data read is demonstrated by the patch; the assertion fires on the diagnostic path after denial.
- Residual risk — The value of the fix is defensive: it removes a real same-origin-policy desync (a cross-origin remote frame briefly self-reporting an incorrect matching origin). Any code path that trusted the cached FrameTreeSyncData origin instead of routing through BindingSecurity’s remote-aware check could have turned this into a genuine leak; auditing for such paths is warranted.
Detection & hunting
For defenders and SOC / detection engineers:
- Debug-build assertion !activeOrigin->isSameOriginDomain(targetOrigin) in crossDomainAccessErrorMessage during site-isolation navigations — Treat crashes/asserts on this path around pagehide/pageswap/unload as the fingerprint of a stale RemoteFrame origin; correlate with cross-origin frame swaps in progress.
- Cross-origin frame reporting an origin equal to the embedder during a swap — Where WebProcess instrumentation exists, flag any interval where a RemoteFrame’s FrameTreeSyncData origin equals the active document origin before the corresponding FrameTreeSyncDataChangedInAnotherProcess IPC has been applied.
- Vulnerable build fingerprint — Exposure is limited to Site-Isolation-enabled WebKit builds between 313507@main and 313620@main; use build/version fingerprinting rather than a script probe, since the observable effect was a debug assertion.
Audit directions
- Every RemoteFrame seeding site — Enumerate all places that construct a RemoteFrame or its FrameTreeSyncData (loadDidCommitInAnotherProcess, createSubframe, remoteProcessDidTerminate, process-swap paths) and confirm each seeds an opaque/empty origin rather than copying a soon-to-be-invalid origin from an outgoing frame.
- Consumers of FrameTreeSyncData::frameDocumentSecurityOrigin — Trace all readers of the cached remote-frame origin and verify security decisions never trust it directly but always go through BindingSecurity’s remote-aware access check; a direct comparison against the cached value is the dangerous pattern.
- The swap-to-refresh window across all cross-process transitions — Audit the ordering between frame swap and FrameTreeSyncDataChangedInAnotherProcess for every transition type (navigation, crash recovery, BFCache restore) to ensure no security-relevant query can observe placeholder state as authoritative, and that opaque is the invariant placeholder everywhere.
- Lifecycle-event-driven tasks touching remote windows — Review which tasks can run during a swap (pagehide/pageswap/unload handlers, BroadcastChannel, MessagePort, scheduled microtasks) and confirm they tolerate an opaque remote-frame origin and cannot be manipulated to act on a transiently-matching one.