CVE-2025-31257
Overview
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.
Attack Path
- 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.
- 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.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
BackForwardCache::markPagesForContentsSizeChangedSource/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::setContentsSizeSource/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.cppSource/WebCore/page/LocalFrameView.cpp
Audit Directions
- Raw Page*/Frame* held across re-entrant callsIn LocalFrameView and neighboring layout code, grep for
Page* page = m_frame->page()and any rawFrame*/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 meantGrep 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 framesIn 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.