Critical watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chrome for iOS
DescriptionUse after free in Chrome for iOS
ComponentChrome for iOS
Sub-componentMobile>iOSWeb
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513514692
Fix commit509fc948ac91 (chromium/src) +29/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-06-02
Patch gap18 days — fix public in source before the advisory

Background

`web::NavigationContextImpl`
the iOS //ios/web object that carries per-navigation state and whose lifetime is owned by the WebStateImpl it belongs to.
`WebStateImpl` observer callbacks
methods such as OnNavigationStarted, OnNavigationFinished, and the delegate call navigationObserver:didChangePageWithContext: that synchronously notify observers and can re-enter arbitrary embedder code.
`base::WeakPtr`
a non-owning pointer that becomes null once the referent is destroyed, used here via NavigationContextImpl::GetWeakPtr() to detect destruction across a re-entrant call.

Root Cause Analysis

Several navigation callbacks in the iOS web layer held a raw NavigationContextImpl* (existingContext, context, navigationContext, navContext) and kept dereferencing it after invoking WebStateImpl observer methods or the navigation delegate. As the commit message states, those observer calls can let a caller close the WebState, which destroys all of its NavigationContext objects; the subsequent OnNavigationFinished, GetUrl, GetError, IsLoadingErrorPage, or OnPageLoaded accesses then operated on freed memory.

The patch takes a base::WeakPtr<web::NavigationContextImpl> before the re-entrant observer/delegate call and gates every later use on the weak pointer still being valid, returning or skipping the dependent work when the context (and its owning WebState) has been torn down.

The fix works because the weak pointer is invalidated the instant the context is freed, so the post-callback branches no longer touch a dangling object.

Key insight
The code assumed a NavigationContextImpl stayed alive across synchronous observer/delegate notifications, but those notifications can destroy the owning WebState and the context; the fix converts the raw pointer to a base::WeakPtr and re-checks validity after each re-entrant call.

Attack Path

  1. Trigger a navigation callback Web content drives a navigation (e.g. a same-document change or a committed/failed load) so one of the patched handlers runs with a live NavigationContextImpl.
  2. Re-enter and close the WebState During the observer or delegate call (OnNavigationStarted, didChangePageWithContext:), embedder/observer code synchronously closes the WebState, destroying all of its NavigationContext objects.
  3. Dangling dereference Control returns to the handler, which, pre-patch, calls OnNavigationFinished, GetUrl, GetError, or OnPageLoaded on the now-freed context, a use-after-free in the browser process.
  4. Memory corruption The freed NavigationContextImpl slot may be reclaimed, so the stale access reads or writes attacker-influenced memory (inferred).

Impact Assessment

The vendor advisory rates this critical and describes it as a use-after-free in Chrome for iOS. An attacker who can get a navigation observer/delegate to close the WebState mid-callback gains a dangling-pointer dereference in the browser process; any escalation to a controlled read/write or code execution is (inferred) and not shown by the diff. The advisory does not state in-the-wild exploitation; the primary precondition is triggering a WebState teardown during a navigation notification.

Changed Functions

FunctionChangeNotes
if
ios/web/navigation/crw_web_view_navigation_observer.mm
modified
if
ios/web/navigation/crw_wk_navigation_handler.mm
modified
if
ios/web/web_state/ui/crw_web_controller.mm
modified

Files Changed

  • ios/web/navigation/crw_web_view_navigation_observer.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/web_state/ui/crw_web_controller.mm

Audit Directions

  • Raw pointers across observer/delegate notifications
    Audit //ios/web paths that hold a NavigationContextImpl* (or any WebState-owned object) and keep using it after OnNavigation*, delegate, or observer calls that may re-enter and close the WebState.
  • Synchronous WebState/WebContents teardown
    Look for handlers where a notification can synchronously destroy the owning container, then continue referencing members or sibling objects.
  • Missing WeakPtr re-checks
    Flag functions that call GetWeakPtr() once but then dereference the raw pointer again after a second re-entrant call without re-validating the weak pointer.
From 509fc948ac91f6bafb13e3419d48551b7b07363f Mon Sep 17 00:00:00 2001
From: Mike Dougherty <[email protected]>
Date: Fri, 15 May 2026 13:08:15 -0700
Subject: [PATCH] Ensure NavigationContext is not accessed after being freed

WebState observer methods could allow for a caller to close the WebState
which would destroy all NavigationContexts. Verify NavigationContexts
are still valid after observer calls before further use.

Fixed: 513514692
Change-Id: I0eab9559803668562f2e32d661a1cd9fb9de03ce
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7853140
Reviewed-by: Ginny Huang <[email protected]>
Commit-Queue: Mike Dougherty <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1631497}
---

diff --git a/ios/web/navigation/crw_web_view_navigation_observer.mm b/ios/web/navigation/crw_web_view_navigation_observer.mm
index b9130e5..df3319b1 100644
--- a/ios/web/navigation/crw_web_view_navigation_observer.mm
+++ b/ios/web/navigation/crw_web_view_navigation_observer.mm
@@ -6,6 +6,7 @@
 
 #import "base/check.h"
 #import "base/logging.h"
+#import "base/memory/weak_ptr.h"
 #import "base/metrics/histogram_functions.h"
 #import "base/sequence_checker.h"
 #import "base/strings/sys_string_conversions.h"
@@ -237,10 +238,14 @@
       }
       existingContext->SetIsSameDocument(isSameDocumentNavigation);
       existingContext->SetHasCommitted(!isSameDocumentNavigation);
+      base::WeakPtr<web::NavigationContextImpl> weakContext =
+          existingContext->GetWeakPtr();
       self.webStateImpl->OnNavigationStarted(existingContext);
-      [self.delegate navigationObserver:self
-               didChangePageWithContext:existingContext];
-      self.webStateImpl->OnNavigationFinished(existingContext);
+      if (weakContext) {
+        [self.delegate navigationObserver:self
+                 didChangePageWithContext:weakContext.get()];
+        self.webStateImpl->OnNavigationFinished(weakContext.get());
+      }
     }
   }
 
diff --git a/ios/web/navigation/crw_wk_navigation_handler.mm b/ios/web/navigation/crw_wk_navigation_handler.mm
index 7320446..c1d2620 100644
--- a/ios/web/navigation/crw_wk_navigation_handler.mm
+++ b/ios/web/navigation/crw_wk_navigation_handler.mm
@@ -9,6 +9,7 @@
 #import "base/apple/foundation_util.h"
 #import "base/feature_list.h"
 #import "base/ios/ns_error_util.h"
+#import "base/memory/weak_ptr.h"
 #import "base/metrics/histogram_functions.h"
 #import "base/metrics/histogram_macros.h"
 #import "base/metrics/user_metrics.h"
@@ -945,15 +946,17 @@
   [self.navigationStates setState:web::WKNavigationState::COMMITTED
                     forNavigation:navigation];
 
+  base::WeakPtr<web::NavigationContextImpl> weakContext =
+      context ? context->GetWeakPtr() : nullptr;
   if (!committedNavigation && context && !context->IsLoadingErrorPage()) {
     self.webStateImpl->OnNavigationFinished(context);
   }
 
   // The actual navigation item will not be committed until the native content
   // or WebUI is shown.
-  if (context && !context->GetUrl().SchemeIs(url::kAboutScheme)) {
+  if (weakContext && !weakContext->GetUrl().SchemeIs(url::kAboutScheme)) {
     [self.delegate webViewHandlerUpdateSSLStatusForCurrentNavigationItem:self];
-    if (!context->IsLoadingErrorPage()) {
+    if (!weakContext->IsLoadingErrorPage()) {
       [self setLastCommittedNavigationItemTitle:webView.title];
     }
   }
@@ -1951,8 +1954,12 @@
         // WKWebView will revert the url to about:blank. Simply discard pending
         // item and fail the navigation.
         navigationContext->ReleaseItem();
+        base::WeakPtr<web::NavigationContextImpl> weakContext =
+            navigationContext->GetWeakPtr();
         self.webStateImpl->OnNavigationFinished(navigationContext);
-        self.webStateImpl->OnPageLoaded(navigationContext->GetUrl(), false);
+        if (weakContext) {
+          self.webStateImpl->OnPageLoaded(weakContext->GetUrl(), false);
+        }
         return;
       }
     }
@@ -2264,13 +2271,17 @@
         // `OnNavigationFinished` callback.
         navContext->SetUrl(failingURL);
         navContext->SetHasCommitted(true);
+        base::WeakPtr<web::NavigationContextImpl> weakContext =
+            navContext->GetWeakPtr();
         self.webStateImpl->OnNavigationFinished(navContext);
 
         // For SSL cert error pages, SSLStatus needs to be set manually because
         // the placeholder navigation for the error page is committed and
         // there is no server trust (since there's no network navigation), which
         // is required to create a cert in CRWSSLStatusUpdater.
-        if (web::IsWKWebViewSSLCertError(navContext->GetError()) && info.cert) {
+        if (weakContext &&
+            web::IsWKWebViewSSLCertError(weakContext->GetError()) &&
+            info.cert) {
           web::SSLStatus& SSLStatus =
               self.navigationManagerImpl->GetLastCommittedItem()->GetSSL();
           SSLStatus.cert_status = info.cert_status;
diff --git a/ios/web/web_state/ui/crw_web_controller.mm b/ios/web/web_state/ui/crw_web_controller.mm
index 55c37bf..13067f0 100644
--- a/ios/web/web_state/ui/crw_web_controller.mm
+++ b/ios/web/web_state/ui/crw_web_controller.mm
@@ -15,6 +15,7 @@
 #import "base/ios/block_types.h"
 #import "base/ios/ios_util.h"
 #import "base/json/string_escape.h"
+#import "base/memory/weak_ptr.h"
 #import "base/metrics/histogram_functions.h"
 #import "base/metrics/user_metrics.h"
 #import "base/metrics/user_metrics_action.h"
@@ -1823,7 +1824,12 @@
           contextForPendingMainFrameNavigationWithURL:newURL];
     }
     navigationContext->SetIsSameDocument(true);
+    base::WeakPtr<web::NavigationContextImpl> weakContext =
+        navigationContext->GetWeakPtr();
     self.webStateImpl->OnNavigationStarted(navigationContext);
+    if (!weakContext) {
+      return;
+    }
     [self didStartLoading];
     self.navigationManagerImpl->CommitPendingItem(
         navigationContext->ReleaseItem());
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free of NavigationContextImpl in iOS Chrome Browser Process

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in iOS Chrome when a NavigationContextImpl is synchronously destroyed during observer notifications. While internal loops are protected, the calling functions continue to use dangling raw pointers to the deallocated context. This issue impacts the unsandboxed browser process and is not mitigated by MiraclePtr on the iOS platform.

Affected files:

  • ios/web/web_state/ui/crw_web_controller.mm
  • ios/web/web_state/web_state_impl_realized_web_state.mm
  • ios/web/navigation/crw_web_view_navigation_observer.mm
  • ios/web/navigation/crw_wk_navigation_handler.mm
  • ios/web/web_state/ui/crw_web_request_controller.mm

Estimated timestamp from git blame: 2019-03-02

Description

A potential Use-After-Free (UAF) vulnerability has been identified in the iOS implementation of Chromium. The issue arises from the synchronous destruction of web::NavigationContextImpl objects during WebState observer notifications (e.g., DidStartNavigation, DidFinishNavigation).

Chromium previously introduced a mitigation feature, kDetectDestroyedNavigationContexts, which uses a base::WeakPtr to guard the internal observer fan-out loops in WebStateImpl::RealizedWebState. However, this mitigation only protects the loop itself. When control returns to the calling function (typically in CRWWebViewNavigationObserver or CRWWebController), the caller proceeds to dereference its own raw pointer to the NavigationContextImpl, which may have been freed during the observer notification.

Root Cause Analysis

In ios/web/web_state/web_state_impl_realized_web_state.mm, methods like OnNavigationStarted check the context’s validity within the loop:

base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr();
for (auto& observer : observers()) {
  if (!weak_context && base::FeatureList::IsEnabled(
                           features::kDetectDestroyedNavigationContexts)) {
    break;
  }
  observer.DidStartNavigation(owner_, context);
}

If an observer synchronously destroys the WebState or cancels the navigation, the NavigationContextImpl (often owned by CRWWKNavigationStates) is deleted. The loop breaks correctly, but the caller in CRWWebViewNavigationObserver.mm or CRWWebController.mm continues execution using the dangling context pointer.

Impact and Exploitability

This is a high-impact vulnerability because it occurs in the unsandboxed browser process on iOS. Furthermore, MiraclePtr (BackupRefPtr) is not enabled on iOS due to platform-specific address space limitations, meaning these stack-based bare pointers are unprotected.

A particularly dangerous primitive exists in CRWWebViewNavigationObserver::webViewLoadingStateDidChange and CRWWebController::URLDidChangeWithoutDocumentChange::

// CRWWebViewNavigationObserver.mm
self.navigationManagerImpl->CommitPendingItem(
    existingContext->ReleaseItem());

If existingContext is freed and its memory reclaimed by an attacker, the ReleaseItem() call (which moves a std::unique_ptr<NavigationItemImpl>) can be manipulated to return a pointer to an attacker-controlled memory region. Subsequent operations on this NavigationItemImpl by the NavigationManager can lead to arbitrary code execution (RCE).

Suggested Potential Steps to Reproduce

  1. Host a malicious page that performs history manipulation (e.g., history.go(-1)) or fragment changes (location.hash = '#...') to trigger WKWebView KVO notifications.
  2. Implement a WebStateObserver that, upon receiving DidStartNavigation, synchronously destroys the WebState (e.g., by closing the tab or removing it from the WebStateList).
  3. The browser process will reach the KVO handler in CRWWebViewNavigationObserver or CRWWebController, call OnNavigationStarted, and then attempt to dereference the deallocated context pointer (e.g., calling ReleaseItem() or SetHasCommitted()).
  4. Reclaim the freed memory during the observer callback to control the values read during the UAF.

Proposed Fix

Callers that trigger observer notifications for a NavigationContextImpl must use base::WeakPtr<NavigationContextImpl> to track its lifetime and check for its existence before any subsequent dereference.

For example, in CRWWebViewNavigationObserver.mm:

base::WeakPtr<web::NavigationContextImpl> weakContext = existingContext->GetWeakPtr();
self.webStateImpl->OnNavigationStarted(existingContext);
if (!weakContext) {
  return;
}
// Safe to use weakContext.get() or existingContext here.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker