Medium CVSS 4.7 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
4.7
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected Safari crash
ComponentWebCore History
Bug ClassUAF
Tracker290985
Fix commitddbf9329b2ca (WebKit/WebKit) +2/-2
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:N/I:N/A:L
CISA KEVNot listed
CreditedJuergen Schmied of Lynck GmbH
Disclosed2025-05-12

Background

Back/forward cache (BackForwardCache)
WebCore’s cache of fully-constructed, suspended pages kept so navigating back/forward is instant; entries hold cached frames and views that must be handled carefully because they are not the live foreground page.
Site isolation / RemoteFrame
A model where cross-site frames live in other processes, represented locally by a RemoteFrame that has no LocalFrameView, so a page’s main frame may not be a LocalFrame.
localMainFrame() vs mainFrame()
mainFrame() returns the abstract main Frame (possibly remote), while localMainFrame() returns a LocalFrame* that is null when the main frame is remote, making it the safe accessor when local-frame semantics are required.
RefPtr / re-entrancy
A RefPtr holds a strong reference that keeps an object alive; holding one across a call that can run script or layout (like contentsResized()) prevents that call from freeing the object out from under later code.

Root Cause Analysis

This is a lifetime/frame-type logic bug in WebCore’s layout and back/forward cache code that manifests as a Safari (WebContent) crash. Two related invariants are restored. First, in LocalFrameView::setContentsSize(), the code fetched the owning Page via a raw pointer (Page* page = m_frame->page();) immediately after calling contentsResized(). contentsResized() can run layout work and dispatch notifications that re-enter WebCore and can tear down or detach the frame/page; using a raw Page* across that call risks reading a pointer that has been (or is about to be) invalidated, and the subsequent if (!page) return; cannot catch a dangling — as opposed to null — pointer.

The fix changes it to RefPtr page = m_frame->page();, which takes a strong reference so the Page stays alive for the remainder of the function, closing a use-after-free window. Second, in BackForwardCache::markPagesForContentsSizeChanged(), the code compared &page.mainFrame() to the cached page’s main-frame view’s frame. mainFrame() returns the abstract Frame, which under site isolation may be a RemoteFrame that has no LocalFrameView; the comparison (and the surrounding assumption that the main frame is local) is unsafe when the main frame is remote.

The fix uses page.localMainFrame(), which returns a LocalFrame* (null when the main frame is remote), so the comparison only matches a genuine local main frame and simply skips the mark when the main frame is remote, avoiding a bad access. Both edits are defensive: they don’t change intended behavior for the common local, live-page case but remove the paths where a suspended/cached page or a re-entrant contentsResized() left code operating on a freed or wrong-typed frame/page object. Given the diff is a raw-pointer-to-RefPtr hardening plus a local-main-frame guard, the crash is best characterized as a use-after-free / dangling-pointer condition rather than a pure null-deref.

Key insight
The bug is a classic WebKit lifetime/type-assumption error: a raw Page* was held across a re-entrant contentsResized() call and a page’s main frame was assumed local, so a suspended-cache or site-isolated page could leave code touching freed or wrong-typed objects; the fix is a RefPtr plus the localMainFrame() accessor.

Attack Path

  1. Set up a cacheable / site-isolated page The attacker crafts content that either enters the back/forward cache or, under site isolation, has a remote main frame, so that markPagesForContentsSizeChanged and setContentsSize run against a cached or remote-main-frame page.
  2. Trigger a contents-size change Script or layout changes force LocalFrameView::setContentsSize(), whose contentsResized() call re-enters WebCore and can detach or destroy the frame/page while the raw Page* is still held.
  3. Dangling access After contentsResized() returns, the stale raw Page* (or, in the cache path, a wrong-typed remote main frame) is dereferenced, reading freed or type-mismatched memory.
  4. Crash The bad access faults, producing the reported unexpected Safari crash in the WebContent process; the patch’s RefPtr and localMainFrame() guards prevent the stale/invalid access.

Impact Assessment

As shown, the primitive is a use-after-free/dangling-pointer read reached through layout re-entrancy or a remote-main-frame mismatch, and the patch is a lifetime/type hardening rather than a bounds fix. In the general case a controlled UAF of a Page/Frame object could be groomed toward stronger corruption, but the diff itself only demonstrates a crash and gives no attacker-controlled write or allocation shaping, so realistic impact here is a denial-of-service crash confined to the WebContent (renderer) process inside the sandbox. Escalation to code execution is not established by this commit and would require additional primitives, consistent with the medium severity and “unexpected Safari crash” description.

Changed Functions

FunctionChangeNotes
BackForwardCache::markPagesForContentsSizeChanged
Source/WebCore/history/BackForwardCache.cpp
modified Compares the cached main frame against page.localMainFrame() (null for a remote main frame) instead of &page.mainFrame(), so it only marks a genuine local main frame and skips remote-main-frame pages safely.
LocalFrameView::setContentsSize
Source/WebCore/page/LocalFrameView.cpp
modified Holds the Page in a RefPtr instead of a raw Page* after contentsResized(), keeping it alive across the re-entrant call and closing the dangling-pointer window before the null check and later use.

Files Changed

  • Source/WebCore/history/BackForwardCache.cpp
  • Source/WebCore/page/LocalFrameView.cpp

Audit Directions

  • Raw Page*/Frame* held across re-entrant calls
    In LocalFrameView and neighboring layout code, grep for Page* page = m_frame->page() and any raw Frame*/Page* fetched before a call to contentsResized(), layout(), dispatch*, or updateLayout(); each is a candidate dangling-pointer site that should use RefPtr/Ref.
  • mainFrame() where localMainFrame() is meant
    Grep the codebase for mainFrame() uses that assume a LocalFrame or LocalFrameView (e.g. mainFrame().view(), &page.mainFrame() ==) and check whether localMainFrame() with a null-check is needed for site-isolation safety.
  • Other BackForwardCache iterations over cached frames
    In BackForwardCache.cpp audit every loop over m_items / cachedMainFrame()->view()->frame() (grep cachedMainFrame, markForContentsSizeChanged, cachedPage) for the same local-vs-remote main-frame assumption and for accesses to suspended pages that may have been destroyed.

Original Bug Report

The reporter's bug is still restricted on the tracker.