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

Overview

Critical
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Mobile
DescriptionUse after free in Mobile
ComponentMobile
Sub-componentiOS
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.
Tracker504069514
Fix commitc51f6f1236c6 (chromium/src) +12/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/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-20 (public git)
Disclosed2026-05-05
Patch gap15 days — fix public in source before the advisory

Background

`web::WebState`
the iOS WebKit-backed object that represents a single tab’s web content and whose lifetime is tied to that tab.
`base::WeakPtr`
a smart pointer that automatically becomes null when the pointed-to object is destroyed, letting a holder test validity before dereferencing.
`base::BindOnce` block / async callback
a closure captured and invoked later (here after GetSelectedText resolves), so any raw pointer it captures must still be valid at invocation time, not just at capture time.
`ExplainWithGeminiMediator`
the iOS mediator in explain_with_gemini_mediator.mm that reads the current tab’s selected text and routes it into the “Explain with Gemini” feature.

Root Cause Analysis

Three code paths in explain_with_gemini_mediator.mm captured a raw web::WebState* webState by value and then dereferenced it from inside an asynchronous closure — either the GetSelectedText completion block or a UIAction handler — that runs an arbitrary amount of time after capture. The invariant violated is that a pointer captured for later asynchronous use must remain valid until the closure executes; because the WebState (its tab) can be destroyed in the interim, the closure could dereference freed memory when passing webState on to triggerExplainWithGeminiForText:webState:, addItemWithResponse:completion:webState:, or the edit-menu action.

The fix replaces the raw capture with a base::WeakPtr<web::WebState> (weakWebState, obtained via webState->GetWeakPtr()), re-materializes it inside each block as capturedWebState = weakWebState.get(), and guards use behind capturedWebState being non-null, so a destroyed WebState yields a null pointer and the dangerous call is skipped. The guards run before the WebState is ever passed onward, closing the use-after-free window.

Key insight
The single mistake was holding a raw, lifetime-unaware web::WebState* across an asynchronous boundary; the fix swaps it for a base::WeakPtr<web::WebState> that is validity-checked inside the block before any use.

Attack Path

  1. Invoke the Gemini selection flow The user (or attacker-controlled page content) triggers the Explain-with-Gemini entry point on a tab, causing the mediator to call tabHelper->GetSelectedText(...) with a block that captures the tab’s web::WebState.
  2. Close or navigate away the tab Before the asynchronous GetSelectedText (or the edit-menu UIAction) resolves, the underlying web::WebState is destroyed, e.g. by closing the tab, freeing the object the block captured.
  3. Callback fires on stale pointer When the async operation completes, the block runs and, pre-patch, dereferences the now-dangling webState while forwarding it to triggerExplainWithGeminiForText:webState: or addItemWithResponse:completion:webState:, producing a use-after-free.
  4. Downstream corruption (inferred) Any exploitation beyond the dangling dereference (controlling the freed slot’s contents, achieving a read/write primitive) is not shown by the diff and is (inferred).

Impact Assessment

An attacker who can get a tab’s web::WebState freed during the asynchronous selected-text/edit-menu window gains a use-after-free in the iOS browser process. The vendor advisory rates this critical and describes it as a use after free in Mobile; it requires the iOS “Explain with Gemini” feature path and a timing race between tab destruction and the async callback, and any escalation to memory corruption or code execution is (inferred).

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm
modified

Files Changed

  • ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm

Audit Directions

  • Raw pointers captured into async blocks
    Grep iOS .mm code for web::WebState* (or any raw object pointer) captured by value into base::BindOnce blocks or UIAction/completion handlers and dereferenced after an await-style boundary.
  • `__weak self` without weak model/data
    Flag callbacks that already use __weak __typeof(self) weakSelf but still capture other long-lived C++ objects raw, since weakSelf alone does not protect those objects.
  • Tab-lifetime-crossing features
    Review newer intelligence/Gemini and edit-menu integrations that fetch per-tab data asynchronously, ensuring each uses GetWeakPtr() plus a null check rather than a captured WebState*.
From c51f6f1236c6575397abbd63f120fc608db09b48 Mon Sep 17 00:00:00 2001
From: Yasaman Sedaghat <yasamans@google.com>
Date: Mon, 20 Apr 2026 14:18:52 -0700
Subject: [PATCH] [ios] Fix a potential Use-After-Free (UAF) vulnerability in ExplainWithGeminiMediator on iOS.

We were capturing a raw web::WebState* by value within an asynchronous
block. If the WebState is destroyed before the block executes, we will
have a dangling pointer. With this change, we capture the
base::WeakPtr<web::WebState> and check its validity inside the block
before use.

Fixed: 504069514
Change-Id: Idbe23a2d2724cfdaebe1a7bcd85822fa7e27c245
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7778590
Reviewed-by: Adam Arcaro <adamta@google.com>
Commit-Queue: Yasaman Sedaghat <yasamans@google.com>
Cr-Commit-Position: refs/heads/main@{#1617767}
---

diff --git a/ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm b/ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm
index 79741e6..9f27e6a1 100644
--- a/ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm
+++ b/ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm
@@ -104,9 +104,11 @@
       WebSelectionTabHelper::FromWebState(webState);
   __weak __typeof(self) weakSelf = self;
   tabHelper->GetSelectedText(base::BindOnce(^(WebSelectionResponse* response) {
-    if (weakSelf && response.valid && response.selectedText.length) {
+    web::WebState* capturedWebState = weakWebState.get();
+    if (weakSelf && capturedWebState && response.valid &&
+        response.selectedText.length) {
       [weakSelf triggerExplainWithGeminiForText:response.selectedText
-                                       webState:webState];
+                                       webState:capturedWebState];
     }
   }));
 }
@@ -128,10 +130,11 @@
 
   __weak __typeof(self) weakSelf = self;
   tabHelper->GetSelectedText(base::BindOnce(^(WebSelectionResponse* response) {
-    if (weakSelf) {
+    web::WebState* capturedWebState = weakWebState.get();
+    if (weakSelf && capturedWebState) {
       [weakSelf addItemWithResponse:response
                          completion:completion
-                           webState:webState];
+                           webState:capturedWebState];
       return;
     }
     completion(@[]);
@@ -157,8 +160,12 @@
   }
 
   __weak __typeof(self) weakSelf = self;
+  base::WeakPtr<web::WebState> weakWebState = webState->GetWeakPtr();
   UIAction* action = [self actionWithHandler:^(UIAction* a) {
-    [weakSelf triggerExplainWithGeminiForText:text webState:webState];
+    web::WebState* capturedWebState = weakWebState.get();
+    if (weakSelf && capturedWebState) {
+      [weakSelf triggerExplainWithGeminiForText:text webState:capturedWebState];
+    }
   }];
   RecordGeminiEntryPointAvailable(gemini::EntryPoint::EditMenu);
   completion(@[ action ]);
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in ExplainWithGeminiMediator via async block capture

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 potential Use-After-Free (UAF) exists in ExplainWithGeminiMediator due to capturing a raw web::WebState* by value within an asynchronous Objective-C block. If the WebState is destroyed (e.g., via window.close()) before the block executes, the dangling pointer is dereferenced during a virtual method call, leading to potential remote code execution in the browser process.

Affected files:

  • ios/chrome/browser/intelligence/explain_with_gemini/coordinator/explain_with_gemini_mediator.mm
  • ios/chrome/browser/web_selection/model/web_selection_tab_helper.mm

Estimated timestamp from git blame: 2026-02-18

Vulnerability Details

A potential Use-After-Free (UAF) vulnerability exists in ExplainWithGeminiMediator on iOS. The issue arises when the mediator extracts a raw web::WebState* from a base::WeakPtr<web::WebState> and captures this raw pointer by value inside asynchronous Objective-C blocks.

In ExplainWithGeminiMediator.mm, methods like addItemForWebState:withCompletion: and fetchSelectionForWebState: pass a completion block to WebSelectionTabHelper::GetSelectedText. This block is executed asynchronously via base::SequencedTaskRunner::GetCurrentDefault()->PostTask after receiving a text selection payload via IPC from the renderer.

While WebSelectionTabHelper implements WebStateDestroyed to clear callbacks when a tab is closed, it only does so if the callback hasn’t already been posted to the task runner (if (!final_callback_) { return; }).

If a WebState is closed after the valid text selection response has been received and posted to the task queue, but before that queued task runs, the WebStateImpl is destroyed. When the block eventually executes, it invokes canPerformExplainWithGeminiInWebState:, which dereferences the dangling pointer by calling the virtual method webState->GetBrowserState().

Crucially, MiraclePtr (BackupRefPtr) does not protect raw C++ pointers captured by value inside Objective-C blocks or C++ lambdas, as the automated rewriter skips these compiler-generated fields. Therefore, the pointer remains completely unprotected.

Potential Attack Scenario

These are suggested steps an attacker could follow to trigger this vulnerability:

  1. A user visits a malicious website and long-presses to trigger the iOS edit menu.
  2. The mediator requests the selected text by evaluating JavaScript in the renderer.
  3. The malicious page overrides Selection.prototype.toString or intercepts the message event to return a valid text selection payload.
  4. Simultaneously, the malicious page schedules a window.close() call (e.g., via setTimeout) to execute immediately after returning the payload.
  5. The browser process receives the selection payload, and WebSelectionTabHelper posts the Objective-C block to the main thread’s task runner.
  6. The browser process then receives the close request, destroying the WebStateImpl. WebStateDestroyed is called but fails to cancel the already-posted task.
  7. The main thread executes the posted block, which attempts a virtual call on the destroyed WebState object, resulting in a UAF in the browser process.

Suggested Fix

Do not capture the raw web::WebState* inside the Objective-C blocks. Instead, capture the base::WeakPtr<web::WebState> and check its validity inside the block before use.

  base::WeakPtr<web::WebState> weakWebState = webState->GetWeakPtr();
  __weak __typeof(self) weakSelf = self;
  tabHelper->GetSelectedText(base::BindOnce(^(WebSelectionResponse* response) {
    web::WebState* capturedWebState = weakWebState.get();
    if (weakSelf && capturedWebState) {
      [weakSelf addItemWithResponse:response
                         completion:completion
                           webState:capturedWebState];
    } else {
      completion(@[]);
    }
  }));

This pattern should be applied to all blocks capturing the WebState in ExplainWithGeminiMediator.mm.

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