CVE-2026-7897
Overview
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
GetSelectedTextresolves), 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.mmthat 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.
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
- 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’sweb::WebState. - Close or navigate away the tab
Before the asynchronous
GetSelectedText(or the edit-menuUIAction) resolves, the underlyingweb::WebStateis destroyed, e.g. by closing the tab, freeing the object the block captured. - Callback fires on stale pointer
When the async operation completes, the block runs and, pre-patch, dereferences the now-dangling
webStatewhile forwarding it totriggerExplainWithGeminiForText:webState:oraddItemWithResponse:completion:webState:, producing a use-after-free. - 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
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
| Function | Change | Notes |
|---|---|---|
ifios/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 blocksGrep iOS
.mmcode forweb::WebState*(or any raw object pointer) captured by value intobase::BindOnceblocks orUIAction/completion handlers and dereferenced after anawait-style boundary. - `__weak self` without weak model/dataFlag callbacks that already use
__weak __typeof(self) weakSelfbut still capture other long-lived C++ objects raw, sinceweakSelfalone does not protect those objects. - Tab-lifetime-crossing featuresReview newer intelligence/Gemini and edit-menu integrations that fetch per-tab data asynchronously, ensuring each uses
GetWeakPtr()plus a null check rather than a capturedWebState*.
Patch
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 ]);
Original Bug Report
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.mmios/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:
- A user visits a malicious website and long-presses to trigger the iOS edit menu.
- The mediator requests the selected text by evaluating JavaScript in the renderer.
- The malicious page overrides
Selection.prototype.toStringor intercepts the message event to return a valid text selection payload. - Simultaneously, the malicious page schedules a
window.close()call (e.g., viasetTimeout) to execute immediately after returning the payload. - The browser process receives the selection payload, and
WebSelectionTabHelperposts the Objective-C block to the main thread’s task runner. - The browser process then receives the close request, destroying the
WebStateImpl.WebStateDestroyedis called but fails to cancel the already-posted task. - The main thread executes the posted block, which attempts a virtual call on the destroyed
WebStateobject, 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.