← All issues

[11] Navigate-on-pageswap re-entrancy crash

Severity: Medium | Component: WebCore loader / Navigation API | e7745af

Rated Medium because the observable effect is a deterministic WebContent crash via pageswap-handler re-entrancy that clears m_provisionalDocumentLoader mid-commit, and escalation to a memory-safety primitive depends on loader ownership details (pdl Ref capture) not visible in the diff.

Source/WebCore/loader/FrameLoader.cpp

+SetForScope dispatchingPageSwapEvent(m_isDispatchingPageSwapEvent, true);
document->dispatchPageswapEvent(canTriggerCrossDocumentViewTransition, WTF::move(activation));

Source/WebCore/page/Navigation.cpp

-if (!protect(window->document())->isFullyActive() || window->document()->unloadCounter())
+if (!protect(window->document())->isFullyActive() || frame()->loader().isDispatchingPageSwapEvent() || window->document()->unloadCounter())
return createErrorResult(WTF::move(committed), WTF::move(finished), ExceptionCode::InvalidStateError, "Invalid state"_s);

FrameLoader::commitProvisionalLoad wraps document->dispatchPageswapEvent() in a SetForScope that sets m_isDispatchingPageSwapEvent = true for the dispatch's duration; a new bool m_isDispatchingPageSwapEvent { false } member backs a public getter isDispatchingPageSwapEvent(). Navigation::navigate() is amended to reject with InvalidStateError when that flag is true. A regression test installs onpageswap = async (e) => { await navigation.navigate('foo'); } to reproduce the crash.

Failure to forbid renavigation-triggering APIs during a synchronous JS event dispatched mid-commit, allowing the event handler to invalidate the very loader the commit depends on.

FrameLoader::commitProvisionalLoad promotes m_provisionalDocumentLoader into the active loader, dispatches pageswap synchronously so script can observe the cross-document transition, then calls transitionToCommitted and HistoryController::updateForStandardLoad. navigation.navigate(url) runs synchronous initial steps including a policy check that can stop or replace any in-flight provisional load. SetForScope<T> sets a variable for the lifetime of the scope and restores it on exit.

pageswap is dispatched synchronously inside the commit pipeline. Pre-fix, a script handler calling navigation.navigate('foo') ran synchronous policy/setup that could clear m_provisionalDocumentLoader; when the event returned, commitProvisionalLoad continued into transitionToCommittedupdateForStandardLoadDocumentLoader::urlForHistory operating on a stale or null loader.

🔒

Investigates the re-entrancy window opened by a synchronous DOM event fired mid-commit, and the lifetime implications for the provisional loader on either side of the dispatch.

Subscribe to read more

🔒

Multiple reusable audit patterns covering synchronous-event re-entrancy across the loader pipeline, with concrete entrypoint lists for variant discovery.

Subscribe to read more