← WebKit Silent-Fix Report — 2026-W22

6512004572  [Site Isolation] Safari crashed after typing random characters in the smart search field and searching

severity medium class UAF confidence 0.65 WebKit Site Isolation (WebProcessPool) exploitable-grade
Rupin Mittal Tue May 26 11:12:03 2026 -0700 full: 6512004572e0ba805b5cbf7b4960d7a6d195ed2b bug report ↗ view on GitHub ↗
Primitive: process shutdown during in-flight navigation callback
Triage note: Adds a lifetime scope so the process can't shut down before the navigation callback runs, fixing a crash/UAF.
Contents

The bug at a glance

A lifetime bug in the UI-process navigation path under Site Isolation: a web content process chosen for a navigation can be moved into the process cache during an in-flight async IPC, so by the time the completion handler runs it hits RELEASE_ASSERT(!newProcess->isInProcessCache()), crashing Safari. The observed impact is a reliable release-assert crash reachable from ordinary navigation (typing in the smart search field and searching); the underlying condition is a use-of-cached/possibly-recyclable process, which is why it is treated as a UAF-adjacent lifetime bug. Medium severity/0.65: the crash is clear, but weaponizing the cache-recycle window into memory corruption is speculative, and the author calls the fix speculative.

WebProcessPool::processForNavigation() picks a not-in-cache process for a site, then does an async addAllowedFirstPartyForCookies IPC to the network process and only returns the process in the reply’s completion handler. Nothing prevented the process from being cached during that window, so the handler could return a now-cached process and trip the release assert in continueNavigationInNewProcess().

Root cause

Under Site Isolation, when a navigation needs a web content process for a given site, WebProcessPool::processForNavigation() asks the BrowsingContextGroup whether a process already exists for that site (browsingContextGroup.processForSite(site)). If one exists and satisfies the guards – same website data store, not currently in the process cache (!process->isInProcessCache()), matching lockdown mode, consistent enhanced-security state – it is the process the navigation intends to reuse.

Crucially the process is not returned synchronously. First an asynchronous IPC is sent to the network process, addAllowedFirstPartyForCookies(*process, mainFrameSite.domain(), LoadedWebArchive::No, completionHandler), and the chosen process is only handed back inside that completion handler when the reply arrives. Between sending the IPC and receiving the reply the UI process continues to run, and during that window the process can become eligible for and be placed into the process cache – for example if its current pages go away and WebProcessProxy::maybeShutDown() decides the process is idle and cacheable.

When the reply then arrives, the completion handler calls completionHandler(process.releaseNonNull(), …) to drive the navigation forward. Downstream, WebPageProxy::continueNavigationInNewProcess() executes RELEASE_ASSERT(!newProcess->isInProcessCache()). Because the process is now cached, the assert fires and the UI process crashes. More broadly, using a process that the cache believes is idle/recyclable for a live navigation is a lifetime hazard: the cache can suspend or repurpose a process the navigation still depends on.

The fix captures a shutdown-preventing token in the completion-handler lambda: preventProcessShutdownScope = process->shutdownPreventingScope(). Holding this scope from before the async IPC until the reply is received makes WebProcessProxy::canTerminateAuxiliaryProcess() return false, so if maybeShutDown() runs in the interim it will not terminate or cache the process. The process therefore cannot enter the cache while the navigation is pending, and the completion handler always returns a live, non-cached process, satisfying the release assert.

Key code

WebProcessPool.cpp: hold a shutdownPreventingScope across the async IPC

            protect(dataStore->networkProcess())->addAllowedFirstPartyForCookies(*process, mainFrameSite.domain(), LoadedWebArchive::No, [completionHandler = WTF::move(completionHandler), process, preventProcessShutdownScope = process->shutdownPreventingScope()] () mutable {
                completionHandler(process.releaseNonNull(), nullptr, "Found process for the same site"_s);
            });
            return;

Patch walkthrough

  • Source/WebKit/UIProcess/WebProcessPool.cpp — In processForNavigation(), the reused-process branch’s addAllowedFirstPartyForCookies completion-handler lambda capture list gains preventProcessShutdownScope = process->shutdownPreventingScope() alongside the existing completionHandler and process captures. The token is held for the lifetime of the lambda – i.e. until the async IPC reply arrives and completionHandler(process.releaseNonNull(), …) runs – so canTerminateAuxiliaryProcess() returns false and maybeShutDown() cannot cache or terminate the process during the in-flight IPC.

Background

Site Isolation process selection — With Site Isolation, cross-site frames/navigations run in per-site web content processes. WebProcessPool::processForNavigation() decides which process serves a navigation, consulting BrowsingContextGroup::processForSite() to reuse an existing same-site process.

Process cache — WebKit keeps recently-used web content processes in a cache for fast reuse. WebProcessProxy::maybeShutDown() decides whether an idle process is terminated or cached; a process in the cache reports isInProcessCache() == true and must not simultaneously be driving a live navigation.

shutdownPreventingScope() — A RAII token on WebProcessProxy; while any such scope is held, canTerminateAuxiliaryProcess() returns false so maybeShutDown() will neither terminate nor cache the process. Capturing it in a lambda keeps the process pinned until the lambda is destroyed.

continueNavigationInNewProcess() release assert — WebPageProxy::continueNavigationInNewProcess() enforces RELEASE_ASSERT(!newProcess->isInProcessCache()); a cached process reaching this point is a hard, shipping-configuration crash.

Vulnerability window

  1. Select — processForNavigation() finds an existing same-site process that is not in the cache and passes the reuse guards.
  2. Async gap — An addAllowedFirstPartyForCookies IPC is sent to the network process; the process is only returned in the reply’s completion handler, leaving a window where the UI process keeps running.
  3. Cached mid-flight — During the window the process’s pages go away and maybeShutDown() moves it into the process cache.
  4. Crash — The reply arrives, the handler returns the now-cached process, and continueNavigationInNewProcess() trips RELEASE_ASSERT(!newProcess->isInProcessCache()); reported at webkit.org/b/315465, rdar://176228268 as a smart-search-field crash.
  5. Fix — A shutdownPreventingScope() token is captured in the completion handler so the process cannot be cached before the reply (canonical 313901@main).

Exploitation

  1. Open the window — Drive a Site-Isolation navigation that reuses an existing same-site process via processForNavigation(), so the async addAllowedFirstPartyForCookies IPC is in flight.
  2. Force caching — Cause the process’s remaining pages to go away during the IPC window so maybeShutDown() cellars it into the process cache.
  3. Detonate — On the IPC reply the navigation resumes on the cached process and the release assert crashes the UI process (denial of service). Turning the reuse of a cache-eligible/suspended process into memory corruption is speculative and not demonstrated. INFERRED.

Detection & hunting

For defenders and SOC / detection engineers:

  • isInProcessCache release-assert crashes
  • Process cached during pending navigation

Audit directions

  • Async completion handlers holding a WebProcessProxy
  • Other reuse guards recomputed after async gaps
  • maybeShutDown() interactions

Before / after

Loading diff…