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 iOS
DescriptionUse after free in iOS
ComponentChromium
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.
Tracker493221953
Fix commit2ed116751ff0 (chromium/src) +16/-1
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-03-16 (per vendor advisory)
Fix landed2026-04-17 (public git)
Disclosed2026-04-28
Patch gap11 days — fix public in source before the advisory

Background

`WebStateImpl::RealizedWebState`
the iOS //ios/web object that backs a realized tab/web page and fans navigation lifecycle callbacks out to its registered observers.
`NavigationContextImpl`
the per-navigation context object passed to observers describing an in-flight navigation (redirect, finish, same-document, etc.).
Observer reentrancy
an observer notified inside an observers() loop can cancel the navigation, which synchronously destroys the NavigationContextImpl while the loop is still iterating and still holds a raw context pointer.
`kDetectDestroyedNavigationContexts`
the feature flag that gates the new liveness check, so the guard can be rolled out behind base::FeatureList::IsEnabled(...).

Root Cause Analysis

OnNavigationRedirected and the DidFinishNavigation dispatch loop iterated over observers() and called observer.DidRedirectNavigation(owner_, context) / observer.DidFinishNavigation(owner_, context) with a raw NavigationContextImpl* context. An observer invoked during that loop could cancel the navigation, which destroys the NavigationContextImpl; the loop then continued and passed the now-freed context to the remaining observers, a use-after-free. The finish path additionally read context->IsSameDocument() after the loop, another post-free dereference.

The fix captures base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr() before the loop and breaks out as soon as !weak_context (gated by the kDetectDestroyedNavigationContexts feature), and it snapshots const bool same_document = context->IsSameDocument() before the loop so the later favicon logic uses the cached value instead of touching a possibly-freed object.

Key insight
The dispatch loops kept using a raw context pointer that an observer could free mid-iteration; the fix watches the context with a WeakPtr and breaks the loop once it is destroyed, and caches IsSameDocument() beforehand so no access occurs after the free.

Attack Path

  1. Start a navigation that redirects or finishes Web content in a tab drives a navigation through the redirect or finish lifecycle on iOS, entering the affected RealizedWebState dispatch loop.
  2. Cancel during observer notification One of the notified observers cancels the navigation in response, synchronously destroying the NavigationContextImpl. (inferred)
  3. Continue iterating on freed context Pre-patch, the loop proceeds to the next observer and hands it the freed context (and the finish path later reads context->IsSameDocument()), dereferencing freed memory — the use-after-free. (inferred)

Impact Assessment

The advisory rates this critical, a use-after-free in iOS (//ios/web), reachable from ordinary navigation handling on iOS Chrome when an observer cancels a navigation during redirect/finish dispatch. An attacker who arranges that cancellation operates on a freed NavigationContextImpl passed to subsequent observers, potentially corrupting memory (inferred); note the guard ships behind the kDetectDestroyedNavigationContexts flag, so protection depends on that feature being enabled. The advisory does not report in-the-wild exploitation, and any escalation beyond the freed-object access is (inferred).

Changed Functions

FunctionChangeNotes
if
ios/web/web_state/web_state_impl_realized_web_state.mm
modified

Files Changed

  • ios/web/web_state/web_state_impl_realized_web_state.mm

Audit Directions

  • Observer loops holding raw pointers
    Audit every for (auto& observer : observers()) dispatch that passes a raw navigation/context/document pointer, since any observer can reentrantly destroy that object; prefer a WeakPtr guard that breaks the loop.
  • Post-loop dereferences
    Flag reads of the dispatched object after the observer loop (like the original context->IsSameDocument()), and cache needed values before notifying observers.
  • Feature-flag-gated safety checks
    Review guards added behind flags such as kDetectDestroyedNavigationContexts to confirm the flag’s default and that the unflagged path is not still exploitable.
From 2ed116751ff0f0b4f8d67f3298ceae16143a492b Mon Sep 17 00:00:00 2001
From: Mike Dougherty <[email protected]>
Date: Fri, 17 Apr 2026 14:07:12 -0700
Subject: [PATCH] Break from navigation observer loops if WebState is deallocated

Fixed: 493221953
Change-Id: I02ecc528453f8d7895f5f9bddb39d05d04ecf748
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7766683
Reviewed-by: Andrew Liu <[email protected]>
Auto-Submit: Mike Dougherty <[email protected]>
Commit-Queue: Andrew Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1616847}
---

diff --git a/ios/web/web_state/web_state_impl_realized_web_state.mm b/ios/web/web_state/web_state_impl_realized_web_state.mm
index c56ff4c..2a1de50 100644
--- a/ios/web/web_state/web_state_impl_realized_web_state.mm
+++ b/ios/web/web_state/web_state_impl_realized_web_state.mm
@@ -296,7 +296,14 @@
 
 void WebStateImpl::RealizedWebState::OnNavigationRedirected(
     NavigationContextImpl* context) {
+  base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr();
   for (auto& observer : observers()) {
+    // Observers might cancel this navigation, destroying the context. Guard
+    // against that by checking if the context is still alive.
+    if (!weak_context && base::FeatureList::IsEnabled(
+                             features::kDetectDestroyedNavigationContexts)) {
+      break;
+    }
     observer.DidRedirectNavigation(owner_, context);
   }
 }
@@ -309,12 +316,20 @@
     return;
   }
 
+  const bool same_document = context->IsSameDocument();
+  base::WeakPtr<NavigationContextImpl> weak_context = context->GetWeakPtr();
   for (auto& observer : observers()) {
+    // Observers might cancel this navigation, destroying the context. Guard
+    // against that by checking if the context is still alive.
+    if (!weak_context && base::FeatureList::IsEnabled(
+                             features::kDetectDestroyedNavigationContexts)) {
+      break;
+    }
     observer.DidFinishNavigation(owner_, context);
   }
 
   // Update cached_favicon_urls_.
-  if (!context->IsSameDocument()) {
+  if (!same_document) {
     // Favicons are not valid after document change. Favicon URLs will be
     // refetched by CRWWebController and passed to OnFaviconUrlUpdated.
     cached_favicon_urls_.clear();
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in RealizedWebState via synchronous NavigationContext destruction

Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.

Overview: A potential Use-After-Free (UAF) exists in WebStateImpl::RealizedWebState when notifying observers of navigation events. If an observer synchronously destroys the NavigationContextImpl (e.g., by disabling web usage), subsequent observers and method accesses use a dangling raw pointer. While OnNavigationStarted mitigates this with a base::WeakPtr check, OnNavigationRedirected and OnNavigationFinished lack this protection, potentially leading to Remote Code Execution (RCE).

Affected files:

  • ios/web/web_state/web_state_impl_realized_web_state.mm

Estimated timestamp from git blame: 2025-01-23

Summary

A potential Use-After-Free (UAF) vulnerability has been identified in ios/web/web_state/web_state_impl_realized_web_state.mm. When notifying WebStateObservers of navigation events (DidRedirectNavigation, DidFinishNavigation), the code passes a raw pointer to a NavigationContextImpl object. If an observer synchronously triggers an action that causes this context to be destroyed, subsequent access to the pointer results in a UAF.

Technical Details

In WebStateImpl::RealizedWebState, the following methods iterate over observers using a raw NavigationContextImpl* pointer:

  1. OnNavigationStarted(NavigationContextImpl* context) (line 272): This method contains a mitigation using base::WeakPtr (line 284), gated behind the features::kDetectDestroyedNavigationContexts feature flag (line 288). If the context is destroyed during the loop, the loop breaks.

  2. OnNavigationRedirected(NavigationContextImpl* context) (line 296): This method iterates through observers and calls DidRedirectNavigation without any checks for the validity of the context between calls. If an observer causes the navigation to be canceled or the web view to be removed, the next observer receives a dangling pointer.

  3. OnNavigationFinished(NavigationContextImpl* context) (line 303): Similar to the above, it calls DidFinishNavigation without a WeakPtr check. Crucially, it also accesses context->IsSameDocument() on line 316 after the observer notification loop has completed. IsSameDocument() is a virtual method. If any observer caused the context’s destruction during the loop, this virtual method call on a freed object makes RCE a viable threat.

Potential Trigger Mechanism

NavigationContextImpl objects for pending navigations (including those in the REDIRECTED state or downloads) are owned by CRWWKNavigationStates via a std::unique_ptr. A synchronous destruction of these objects can be triggered by calling WebState::SetWebUsageEnabled(false), which eventually invokes CRWWebController removeWebView.

Here is a potential step-by-step sequence an attacker might use to trigger this vulnerability:

  1. Initiation: An attacker-controlled web page initiates a navigation (e.g., triggering a server-side redirect or a download).
  2. State Update: CRWWKNavigationHandler updates the state of the navigation in CRWWKNavigationStates to REDIRECTED or leaves it pending.
  3. Observer Notification: The handler invokes self.webStateImpl->OnNavigationRedirected(context) or self.webStateImpl->OnNavigationFinished(context), passing the raw pointer to WebStateImpl.
  4. Looping Observers: Execution enters WebStateImpl::RealizedWebState and begins iterating over WebStateObserver objects.
  5. Synchronous Destruction: During this iteration, an observer synchronously triggers an action that disables web usage for the WebState (e.g., via WebUsageEnablerBrowserAgent::SetWebUsageEnabled(false)). This calls [web_controller_ setWebUsageEnabled:NO], which calls [self removeWebView].
  6. Pending Navigations Cleared: Inside removeWebView, the code explicitly iterates over all pending navigations and calls [self.navigationHandler.navigationStates removeNavigation:navigation].
  7. Memory Freed: [CRWWKNavigationStates removeNavigation:] removes the navigation record and releases its std::unique_ptr<web::NavigationContextImpl>. The context is destroyed and freed.
  8. Use-After-Free: Execution returns to the observer loop in WebStateImpl::RealizedWebState. The context parameter is now a dangling pointer.
  9. Subsequent Access: The loop continues to the next observer, passing the dangling pointer. Furthermore, if this occurred in OnNavigationFinished, the code evaluates context->IsSameDocument() on the freed object. Because IsSameDocument() is virtual, this dereferences a hijacked vtable pointer.

Note: The LLM agent does not have the ability to run code or build a working PoC, so these steps are suggested/potential based on static analysis.

Impact

As ios/web runs within the main browser process on iOS, this Use-After-Free vulnerability allows for memory corruption. Because it involves a virtual method call (IsSameDocument()) on a synchronously freed object, an attacker could potentially overwrite the memory via heap spraying and hijack the control flow, leading to full Remote Code Execution (RCE) and a sandbox escape.

Evaluated with Chrome root at commit: 43c4f3945742db6f06efbaf9a77b90f34a720277


Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s 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