← All issues

[3] Use-after-free in LocalFrameView::scrollToAnchorFragment via NavigateEvent finish

Severity: High | Component: WebCore Navigation API / frame scrolling | 7b74291

Rated High because the diff adds a strong reference around a LocalFrameView receiver that a beforematch handler can free mid-call, establishing a web-reachable UAF; escalation to a memory-corruption primitive depends on reclaiming the freed view slot, which this change does not demonstrate.

Deployed smart pointers to fix a use-after-free of LocalFrameView. NavigateEvent::processScrollBehavior called LocalFrameView methods through a raw document.frame()->view() pointer; the fix wraps the view receiver in a ref-holding smart pointer so it stays alive for the duration of the call.

Source/WebCore/page/NavigateEvent.cpp

void NavigateEvent::processScrollBehavior(Document& document)
...
if (!document.url().hasFragmentIdentifier()) {
if (m_navigationType == NavigationNavigationType::Reload)
document.frame()->loader().history().restoreScrollPositionAndViewState();
else
- protect(document)->frame()->view()->setScrollPosition({ 0, 0 });
+ protect(protect(document)->frame()->view())->setScrollPosition({ 0, 0 });
return;
}
...
if (!document.haveStylesheetsLoaded())
document.setGotoAnchorNeededAfterStylesheetsLoad(true);
else
- protect(document)->frame()->view()->scrollToFragment(document.url());
+ protect(protect(document)->frame()->view())->scrollToFragment(document.url());

LayoutTests/navigation-api/resources/navigation-api-fragment-intercept-crash-inner.html

+<span id="target" hidden="until-found">trigger text</span>
+<script>
+document.addEventListener('beforematch', function (event) {
+ parent.document.querySelector('iframe').remove();
+}, {once: true});
+
+navigation.addEventListener('navigate', function (event) {
+ if (event.destination.url.includes('#target'));
+ event.intercept();
+});
+
+onload = () => {
+ setTimeout(() => {
+ navigation.navigate(location.href + '#target');
+ parent.scheduleFinish();
+ }, 0);
+}
+</script>

Two call sites in processScrollBehavior are changed. protect(document)->frame()->view()->setScrollPosition({ 0, 0 }) becomes protect(protect(document)->frame()->view())->setScrollPosition({ 0, 0 }), and the scrollToFragment call is wrapped identically. The added inner protect(...) wraps the LocalFrameView* in a ref-holding smart pointer so the view survives the call. The page/NavigateEvent.cpp entry is also removed from both UncheckedCallArgsCheckerExpectations and UncountedCallArgsCheckerExpectations, confirming the file now satisfies the SaferCPP checkers — the previously flagged uncounted call-argument was exactly this raw view() receiver.

Failure to hold a strong reference on the this receiver across a synchronous script-dispatch (beforematch) re-entrancy boundary.

The Navigation API lets script observe and intercept navigations via a navigate event; calling event.intercept() commits the navigation to the app and causes WebKit to run its own scroll-behavior step (NavigateEvent::processScrollBehavior) for fragment navigations. hidden="until-found" is a content-visibility feature: an element hidden this way is revealed — and fires a synchronous beforematch event to script — when the browser needs to scroll it into view, e.g. during fragment-anchor scrolling. LocalFrameView is the reference-counted view object owned by a frame; it is destroyed when the frame is detached (for an iframe, when the <iframe> is removed from its parent document). protect(...) in this file returns a ref-holding smart pointer, keeping the pointee alive for the enclosing expression. In WebKit's normal model, a method should hold a strong reference to this whenever it can synchronously run script that might drop the last reference.

This is a use-after-free driven by JS re-entrancy across an event-dispatch boundary. Before the fix, processScrollBehavior obtained the target LocalFrameView as a bare LocalFrameView* and invoked scrollToFragment() / setScrollPosition() on it. Only the Document was ref-protected; the view receiver — this inside those methods — was not. LocalFrameView::scrollToFragmentscrollToAnchorFragment reveals a hidden="until-found" anchor, which synchronously fires beforematch into page script (the scroll/anchor path is not shown in the diff, so this trigger sequence is inferred from the test).

The test's beforematch handler removes the containing <iframe> from the parent document, which detaches and tears down the child frame and destroys its LocalFrameView. Since the view was the un-refcounted receiver of the in-progress method call, this becomes a dangling pointer, and execution continuing inside scrollToAnchorFragment after the event dispatch dereferences freed memory.

The immediate observed effect is a crash/ASAN abort. Escalation to a memory-corruption primitive would depend on reclaiming the freed view slot under controlled heap conditions: grooming the freed LocalFrameView slot could turn the continued member access into a controlled read or type-confused dereference, and attacker control over the reclaiming allocation would determine primitive quality.

This vulnerability weakens memory safety inside the WebContent process. The security model assumes an object is kept alive for the full duration of a method invoked on it; before the fix that invariant was violated for the LocalFrameView receiver of scrollToFragment, because a beforematch handler could destroy the frame's view mid-call. On its own it still requires a separate sandbox escape.

This is the classic WebKit "re-entrancy frees the receiver" pattern: a native method running on a raw this that synchronously dispatches an event which can destroy this. The SaferCPP checker expectations lists are effectively a to-do list of call sites passing un-refcounted receivers; NavigateEvent.cpp graduating off both lists shows the fix and the static-analysis policy converging. hidden=until-found/beforematch is an underappreciated synchronous script-execution point during layout/scroll and a good ingredient for re-entrancy bugs.

Note: The synchronous beforematch dispatch inside the scroll/anchor path, the exact SaferCPP entry, and the synchronous LocalFrameView destruction on iframe removal are inferred from the test and surrounding patterns rather than shown in the diff. The receiver-lifetime fix and its crash conditions are directly supported by the patch.