CVE-2025-31257
Overview
Impact:
Description: Processing maliciously crafted web content may lead to an unexpected Safari crash
Researchers: Juergen Schmied of Lynck GmbH
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.
Patch Details
diff --git a/Source/WebCore/history/BackForwardCache.cpp b/Source/WebCore/history/BackForwardCache.cpp
index 31fd4b5414bc..2fc767eb9fcd 100644
--- a/Source/WebCore/history/BackForwardCache.cpp
+++ b/Source/WebCore/history/BackForwardCache.cpp
@@ -389,7 +389,7 @@ void BackForwardCache::markPagesForContentsSizeChanged(Page& page)
ASSERT(!m_items.contains(item.key));
continue;
}
- if (&page.mainFrame() == &(*cachedPage)->cachedMainFrame()->view()->frame())
+ if (page.localMainFrame() == &(*cachedPage)->cachedMainFrame()->view()->frame())
(*cachedPage)->markForContentsSizeChanged();
}
}
diff --git a/Source/WebCore/page/LocalFrameView.cpp b/Source/WebCore/page/LocalFrameView.cpp
index 64e43a577807..c080140b17b1 100644
--- a/Source/WebCore/page/LocalFrameView.cpp
+++ b/Source/WebCore/page/LocalFrameView.cpp
@@ -542,7 +542,7 @@ void LocalFrameView::setContentsSize(const IntSize& size)
ScrollView::setContentsSize(size);
contentsResized();
- Page* page = m_frame->page();
+ RefPtr page = m_frame->page();
if (!page)
return;
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-31257 |
| Bugzilla | 290985 |
| Component | WebCore |
| Bug Class | UAF |
| Severity | medium |
| Commit | ddbf9329b2ca0320… |
| Advisory | Apple Advisory |
Terminology
- 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
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.
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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
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 Files
Source/WebCore/history/BackForwardCache.cppSource/WebCore/page/LocalFrameView.cpp