[NavigationScheduler] history.back/forward/go(n) calls don't coalesce per spec when queued synchronously
NavigationScheduler is WebCore's per-frame pending-navigation slot: it holds one ScheduledNavigation in m_redirect and fires it at the next task boundary. The HTML spec, however, models history traversals as tasks on a single session-history-traversal queue keyed by the top-level traversable, so all history.back/forward/go calls queued synchronously across any frame must coalesce into one net delta before anything navigates. WebKit had no such queue: each scheduleHistoryNavigation() called schedule(), which called cancel() and replaced m_redirect, so a second call silently evicted the first.
Source/WebCore/loader/NavigationScheduler.cpp
The fix adds an accumulateHistorySteps virtual hook so steps accumulate numerically onto the already-pending ScheduledHistoryNavigation rather than replacing it, and forwards all subframe history traversals to the top-level LocalFrame's scheduler so iframe and main-frame calls coalesce at the correct scope. back();forward() now nets to zero and cancel()s instead of firing a spurious navigation; back();back() accumulates to -2 and fires once, skipping the intermediate entry. go(0) is explicitly excluded to preserve reload semantics.
Significance
This rewires how WebKit dispatches and merges cross-frame history navigations — a privileged path that determines which documents load, when load events fire, and how the back-forward list advances. The new iframe-scheduler-to-top-frame forwarding path and the step-accumulation boundary conditions around same-document traversals are novel logic on a security-sensitive control plane.
Audit directions
The originating frame's scheduler is cancelled and its loader completed before forwarding; verify cancel() cannot leave partially-initialised state that the top-frame fire() then touches through the originating-frame pointer threaded into accumulateHistorySteps. The same-document check returns NotHandled so hash/pushState traversals fall through to schedule()/cancel(), but that check runs at the calling frame's scope even after forwarding — probe what happens when an iframe's cross-document traversal looks same-document from the main frame's perspective. adjustPendingHistoryNavigationForNewBackForwardEntry now also forwards to the top frame; its early-return path must be safe when m_redirect is null after a scheduler has already forwarded. Finally, the commit explicitly leaves cross-process top frames (Site Isolation) unhandled, so an attacker-controlled cross-site iframe still observes the old eviction behaviour versus same-site iframes seeing coalescing — an observable timing channel until the follow-up UIProcess queue lands.