[NavigationScheduler] history.back/forward/go(n) calls don't coalesce per spec when queued synchronously
04c8ceb
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
ScheduledNavigation::AccumulateResult ScheduledHistoryNavigation::accumulateHistorySteps(int additionalSteps)
{
if (!additionalSteps) // go(0) is reload, not a delta
return AccumulateResult::NotHandled;
m_steps += additionalSteps;
m_historyItem = nullptr; // recompute target at fire() time
return AccumulateResult::Handled;
}
void NavigationScheduler::scheduleHistoryNavigation(Frame& originatingFrame, int steps)
{
if (auto* topLocalFrame = topLevelLocalFrame(); topLocalFrame != &m_frame) {
originatingFrame.loader().completed();
cancel();
topLocalFrame->navigationScheduler().scheduleHistoryNavigation(originatingFrame, steps);
return;
}
if (m_redirect) {
auto result = m_redirect->accumulateHistorySteps(steps);
if (result == ScheduledNavigation::AccumulateResult::Handled) {
if (!m_redirect->steps()) // net delta == 0: going nowhere
cancel();
return;
}
}
schedule(makeUnique<ScheduledHistoryNavigation>(steps));
}
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
Aaa Aaaaaaaaaaa Aaaaaaa Aaaaaaaaa Aa Aaaaaaaaa Aaa Aaa Aaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaaaa Aaaaaa Aaaaaaaaaa Aaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaa Aaaaa Aaaa Aaa Aaaaaaaaa Aaaaaaaa Aaaa Aaaaaaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaa Aaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaa Aa Aaaaaaaaaaaaaa Aaaaaaaaaa Aaaa Aaaaaaa Aa Aaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaaaa Aaaa Aa Aaa Aaaaaaa Aaaaaaa Aaaaa Aaaa Aaaaa Aaaaaaaaaa a Aaaaa Aaaa Aaaaaaa Aaaa Aa Aaaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaa Aaaaa Aaaaaaaaaaaaa Aaaa Aaa Aaaa Aaaaaaa Aaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaa Aaaaaaaa Aa Aaa Aaa Aaaaaa Aaa Aaaaaaaaaaaa Aaaa Aaaa Aa Aaaa Aaaa Aaaaaaaaaaaa Aa Aaaa Aaaaa a Aaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaa Aaaaaaaa Aaa Aaaaaa Aaaaaaaaaa Aaaaaa Aaaaaaaaaaaaa Aaa Aaaaaa Aaaaa Aaaaaaaaaa Aaaaaaaaaa Aa Aa Aaaaaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaaa Aaaaa Aaaaaaaa Aaa Aaa Aaaaaaaa Aaaaaaaaa Aaaaaa Aaaaaaaaa Aaaaaaa Aaaaaa Aaaaaaaaaa a Aa Aaaaaaaaaa Aaaaaa Aaaaaaa Aaaaa Aaa Aaaaaaaaa Aaaaaaaaa Aaaaa Aaaaaa
🔒New cross-frame forwarding and step-accumulation paths introduce several boundary conditions across the same-document/cross-document divide — audit directions included.
Subscribe to read more