← All issues

[Site Isolation] Per-frame walk replaces navigatedFrameID heuristic in back/forward routing

0b955f5

Site Isolation assigns each cross-origin iframe to its own WebContent process, with the UIProcess orchestrating. Back/forward navigation requires the UIProcess to figure out which frames must move and send GoToBackForwardItem IPC to each owning process. The old code picked a single "primary" frame via navigatedFrameID — a field encoding which child frame's navigation produced an entry, not which frame the caller wants to traverse — which coincided with back intent but diverged on forward.

Source/WebKit/UIProcess/WebPageProxy.cpp

bool WebPageProxy::dispatchPerFrameTraversals(WebBackForwardListFrameItem& currentItem,
WebBackForwardListFrameItem& targetItem)
{
bool dispatched = false;
if (currentItem.itemSequenceNumber() != targetItem.itemSequenceNumber())
dispatched |= sendGoToBackForwardItemForFrame(targetItem);
 
if (currentItem.documentSequenceNumber() == targetItem.documentSequenceNumber()) {
for (auto& [frameID, childTarget] : targetItem.children()) {
if (auto* childCurrent = currentItem.childItemForFrameID(frameID))
dispatched |= dispatchPerFrameTraversals(*childCurrent, *childTarget);
}
}
return dispatched;
}

The UIProcess now walks the (current, target) WebBackForwardListFrameItem trees pair-wise and dispatches an independent GoToBackForwardItem to each frame's process whose itemSequenceNumber differs. Recursion is gated by documentSequenceNumber equality — cross-document subtrees stop the walk and defer to the existing pull-at-commit mechanism. The change is behind the useUIProcessForBackForwardItemLoading flag, with the legacy navigatedFrameID path retained otherwise.

This makes multi-process iframe traversal correct and symmetric for back and forward, directly affecting the correctness of cross-origin frame navigation at the security boundary.

Recursion stops when documentSequenceNumber differs — if an attacker can influence those values via rapid same-document then cross-document navigations, the walk may over-recurse (dispatching to frames that should not traverse) or under-recurse (silently dropping traversals); the new error-log path on silent drop is worth checking for reachability from a page. Each differing frame gets its own dispatch with no transactional rollback, so a crafted mixed same/cross-origin sequence could desynchronize frame trees across origins if one process crashes mid-dispatch. The flag gate keeps two code paths live — check whether a history entry created under one flag mode is replayed under the other, since the FrameState schema may differ. Finally, verify the children() child map is not mutated during the recursive IPC-dispatching walk.