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>iOSWebView
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.
Tracker504072665
Fix commit67184bab7ef9 (chromium/src) +19/-18
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-04-18 (per vendor advisory)
Fix landed2026-04-23 (public git)
Disclosed2026-06-02
Patch gap40 days — fix public in source before the advisory

Background

`CWVWebView`
The Objective-C++ wrapper class in Chrome for iOS (web_view) that owns a web::WebState (_webState) and exposes web-content APIs to embedders.
`_backForwardList` / `navigationManager`
CWVBackForwardList exposes navigation history; its navigationManager property points into state owned by the _webState and is unsafe to use once the web state is torn down.
`shutDown` vs `dealloc`
shutDown is an explicit teardown entry point an embedder may call; dealloc is the Objective-C destructor that always runs, and the two are independent teardown paths that may each run alone.
`WebViewHolder` / `_webStateObserver`
Helper that associates a CWVWebView with its web::WebState and the observer registered on that web state; both must be detached during cleanup.

Root Cause Analysis

Teardown of a CWVWebView could occur via two independent paths — shutDown and dealloc — but only shutDown reset _backForwardList.navigationManager = nil; the old dealloc merely removed the web-state observer and called WebViewHolder::RemoveFromWebState without clearing the back-forward list’s manager pointer. Because shutDown “may not be called before dealloc” (per the commit), an object destroyed directly through dealloc would leave _backForwardList holding a navigationManager pointer into state that is destroyed with the web state, creating a dangling pointer and a use-after-free when the back-forward list is subsequently touched.

The fix extracts the shared teardown into a new cleanupWebStateReference helper that always nils _backForwardList.navigationManager, removes the observer, and calls WebViewHolder::RemoveFromWebState, and invokes it from both shutDown and dealloc, so the “CWVBackForwardList is unsafe to use after shutting down” invariant now holds on every destruction path. The change is purely additive cleanup consolidation — it tightens teardown by ensuring the navigationManager reset is no longer exclusive to shutDown.

Key insight
The core mistake was that the navigationManager nil-out (and the rest of the cleanup) lived only on the shutDown path, so a dealloc-only destruction left _backForwardList with a dangling pointer; the fix centralizes cleanup in cleanupWebStateReference and calls it from both destruction paths.

Attack Path

  1. Create a web view Content (or an embedder driven by content) causes a CWVWebView with a live _webState and a populated CWVBackForwardList to exist.
  2. Destroy without `shutDown` The CWVWebView is released and dealloc runs without a prior shutDown, so the old code frees the web state but leaves _backForwardList.navigationManager pointing at freed state.
  3. Use the stale back-forward list A subsequent access to _backForwardList/navigationManager dereferences the dangling pointer, causing a use-after-free.
  4. Downstream corruption Exploiting the freed navigation-manager memory toward a read/write primitive or code execution is (inferred) and not demonstrated by the diff.

Impact Assessment

An attacker gains a use-after-free in Chrome for iOS whenever a CWVWebView is torn down through dealloc without shutDown, leaving a dangling navigationManager in _backForwardList. The vendor advisory rates this critical; it does not report in-the-wild exploitation, and any escalation beyond the dangling-pointer condition is (inferred). Reachability depends on the destruction ordering of the web view rather than a special flag or gesture, but the exact trigger that forces dealloc ahead of shutDown is not shown in the diff.

Changed Functions

FunctionChangeNotes
if
ios/web_view/internal/cwv_web_view.mm
modified

Files Changed

  • ios/web_view/internal/cwv_web_view.mm

Audit Directions

  • Divergent destruction paths
    Audit classes with both an explicit shutDown/close method and a dealloc/destructor for cleanup steps present on one path but missing on the other.
  • Pointers into owned-but-separately-destroyed state
    Review properties like navigationManager that alias into a member’s storage (_webState) and must be nilled before or with that member’s teardown.
  • Observer and holder deregistration
    Check every teardown path removes registered observers and association holders (RemoveObserver, WebViewHolder::RemoveFromWebState) exactly once and unconditionally.
From 67184bab7ef9a8e301189d8a9dcd78d82393f27c Mon Sep 17 00:00:00 2001
From: Mike Dougherty <[email protected]>
Date: Wed, 22 Apr 2026 17:33:44 -0700
Subject: [PATCH] Ensure CWVWebView cleanup is consistent

`shutdown` may not be called before `dealloc`. In these cases, ensure
that `_backForwardList.navigationManager` is set to nil.

Additionally, move the dealloc and shutdown methods to be close to each
other and move shared logic to a helper method to make it more clear
that there are two destrution paths.

Fixed: 504072665
Change-Id: I352c3f75de196f7136046bcc74c908ea42d95b94
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7783652
Reviewed-by: Rohit Rao <[email protected]>
Commit-Queue: Mike Dougherty <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1619215}
---

diff --git a/ios/web_view/internal/cwv_web_view.mm b/ios/web_view/internal/cwv_web_view.mm
index 29b4ba1..8377e03 100644
--- a/ios/web_view/internal/cwv_web_view.mm
+++ b/ios/web_view/internal/cwv_web_view.mm
@@ -511,16 +511,6 @@
   _webState->GetWebViewProxy().allowsLinkPreview = allowsLinkPreview;
 }
 
-- (void)dealloc {
-  if (_webState) {
-    if (_webStateObserver) {
-      _webState->RemoveObserver(_webStateObserver.get());
-      _webStateObserver.reset();
-    }
-    WebViewHolder::RemoveFromWebState(_webState.get());
-  }
-}
-
 - (void)goBack {
   if (![self isWebStateSafeToUse]) {
     return;
@@ -1253,20 +1243,31 @@
 
 - (void)shutDown {
   if (_webState) {
-    // CWVBackForwardList is unsafe to use after shutting down.
-    _backForwardList.navigationManager = nil;
-
     // To handle the case where -[CWVWebView encodeRestorableStateWithCoder:] is
     // called after this method, precompute the session storage so it may be
     // used during encoding later.
     [_serializationHelper updateStateFromWebState:_webState.get()];
-    if (_webStateObserver) {
-      _webState->RemoveObserver(_webStateObserver.get());
-      _webStateObserver.reset();
-    }
-    WebViewHolder::RemoveFromWebState(_webState.get());
+
+    [self cleanupWebStateReference];
     _webState.reset();
   }
 }
 
+- (void)dealloc {
+  if (_webState) {
+    [self cleanupWebStateReference];
+  }
+}
+
+- (void)cleanupWebStateReference {
+  // CWVBackForwardList is unsafe to use after shutting down.
+  _backForwardList.navigationManager = nil;
+
+  if (_webStateObserver) {
+    _webState->RemoveObserver(_webStateObserver.get());
+    _webStateObserver.reset();
+  }
+  WebViewHolder::RemoveFromWebState(_webState.get());
+}
+
 @end
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in CWVBackForwardList via dangling NavigationManager

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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A Use-After-Free vulnerability exists in ios/web_view where CWVBackForwardList retains a dangling raw C++ pointer to a destroyed web::NavigationManager. This occurs when an embedding application releases a CWVWebView without calling the internal shutDown method, leaving the pointer un-nulled during standard deallocation. Accessing the list thereafter results in virtual calls on freed memory, potentially leading to Remote Code Execution.

Affected files:

  • ios/web_view/internal/cwv_web_view.mm
  • ios/web_view/internal/cwv_back_forward_list.mm
  • ios/web_view/internal/cwv_back_forward_list_internal.h

Estimated timestamp from git blame: 2018-10-24

Background

In ChromeWebView.framework (ios/web_view), the CWVBackForwardList Objective-C object provides the host application with access to the navigation history of a CWVWebView. Internally, it relies on a raw C++ pointer to fetch data, declared in ios/web_view/internal/cwv_back_forward_list_internal.h:

@property(nonatomic, nullable) const web::NavigationManager* navigationManager;

Vulnerability Details

The web::NavigationManager memory is owned by the web::WebState instance managed by the parent CWVWebView. A Use-After-Free condition arises because the standard deallocation path of CWVWebView does not clear the navigationManager pointer stored in its associated CWVBackForwardList.

While the internal -[CWVWebView shutDown] method correctly nullifies this pointer (_backForwardList.navigationManager = nil;), standard ARC deallocation via -[CWVWebView dealloc] does not. It is a documented expectation that dealloc can be called without shutDown occurring first (see comments in ios/web_view/internal/autofill/web_view_autofill_client_ios.h).

When a CWVWebView is deallocated, its std::unique_ptr<web::WebState> is destroyed, which in turn destroys the NavigationManagerImpl. If the embedding iOS application has retained a strong reference to the CWVBackForwardList object to power its history UI, the navigationManager property becomes a dangling pointer to freed memory.

Subsequent access to the list (e.g., -[CWVBackForwardList currentItem]) bypasses the nil check because the pointer is dangling, and executes a virtual C++ call: self.navigationManager->GetLastCommittedItem().

Potential Exploitation Steps

Note: These are theoretical steps based on code analysis; our tooling agent does not run exploit code.

  1. Setup: An attacker hosts a malicious page and the victim visits it via an app using ChromeWebView.framework.
  2. Popup Creation: The attacker’s script calls window.open(), prompting the embedder to create a new CWVWebView instance.
  3. UI Retention: The embedder application requests webView.backForwardList and retains it for its custom navigation UI.
  4. Trigger Closure: The attacker’s script calls window.close() on the popup. This triggers -[CWVUIDelegate webViewDidClose:].
  5. Deallocation: The embedder handles the delegate call by removing the popup from the view hierarchy and dropping its strong reference. The CWVWebView deallocates, destroying the NavigationManager while leaving the dangling pointer in the retained CWVBackForwardList.
  6. Heap Spray: The attacker’s surviving opener page runs a JavaScript loop to allocate large objects, reclaiming the freed memory slot previously held by the NavigationManagerImpl. (Note: ios/web_view app extensions are built with use_partition_alloc = false, meaning BackupRefPtr mitigation is not active).
  7. Execution: The embedder app updates its UI and accesses the currentItem of the retained back-forward list. The virtual call uses the attacker-controlled vtable, leading to Remote Code Execution (RCE) in the browser process.

Suggested Fix

The -[CWVWebView dealloc] method should be updated to explicitly nullify the navigationManager pointer on the _backForwardList, mirroring the logic found in the shutDown method.

// In ios/web_view/internal/cwv_web_view.mm
- (void)dealloc {
  _backForwardList.navigationManager = nil;
  if (_webState) {
    if (_webStateObserver) {
      _webState->RemoveObserver(_webStateObserver.get());
      _webStateObserver.reset();
    }
    WebViewHolder::RemoveFromWebState(_webState.get());
  }
}

Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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