CVE-2026-8521
Overview
Background
- `TabGroup`
- a model object (iOS) representing a group of tabs, whose lifetime is managed by the tab/web-state model, not by the UI coordinators that display it.
- `base::WeakPtr`
- a smart pointer that automatically becomes null when the referenced object is destroyed, letting holders detect a dangling reference instead of dereferencing freed memory.
- `RecentActivityCoordinator`
- the iOS coordinator that, on dismissing its presented view controller, runs a completion block that asks the
TabGroupsCommandshandler to open the group edition UI. - Dismissal completion block
- an Objective-C block captured and run asynchronously after a presented view controller is dismissed, so values it captures may outlive the moment they were captured.
Root Cause Analysis
showTabGroupEditionForGroup: accepted a raw const TabGroup*. In RecentActivityCoordinator, the raw group pointer was captured by value inside the asynchronous dismissViewControllerAnimated:completion: block and used only after the dismissal animation completed. Between capture and execution the underlying TabGroup could be destroyed, so the completion block passed a dangling pointer into showTabGroupEditionForGroup:, which then forwarded it to CreateTabGroupCoordinator — a use-after-free.
The fix changes the TabGroupsCommands protocol method to take a base::WeakPtr<const TabGroup>; RecentActivityCoordinator now captures group->GetWeakPtr() so a destroyed group is observed as null. showTabGroupEditionForGroup: replaces the unconditional CHECK(tabGroup) with an early if (!tabGroup) { return; } so a now-freed group is handled gracefully rather than crashing or dereferencing, and only calls tabGroup.get() after that guard. Callers (TabGridCoordinator, TabGroupViewController) are updated to pass weak pointers instead of raw pointers.
TabGroup* in an async completion block that could run after the group was freed; the fix carries the group as a base::WeakPtr and bails out early when it has become null.Attack Path
- Open the recent-activity edition flow
Use the Tab Groups UI so
RecentActivityCoordinatorbegins dismissing its view controller with a completion block that will open group edition forgroup. - Capture a raw group pointer
The pre-patch block captures the raw
TabGroup*by value, binding it for later execution after the dismissal animation. - Destroy the group during dismissal
Cause the referenced
TabGroupto be destroyed (e.g., the group is closed/removed) before the asynchronous completion block runs. - Dangling dereference
When the completion block fires it calls
showTabGroupEditionForGroup:with the freed pointer, which the oldCHECK(tabGroup)passes and forwards intoCreateTabGroupCoordinator, a use-after-free on the freedTabGroup.
Impact Assessment
TabGroup object in the browser (app) process on iOS, with impact beyond the crash (e.g., controlled reuse of freed memory) (inferred) and not demonstrated by the diff. The vendor advisory rates this critical and classifies it as a use-after-free in Tab Groups; it is iOS-specific and requires exercising the recent-activity/tab-group UI such that a group is freed during the dismissal window. No exploited-in-the-wild status is asserted beyond what the advisory provides.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mm |
modified |
Files Changed
ios/chrome/browser/shared/public/commands/tab_groups_commands.hios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mmios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_grid_coordinator.mmios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mmios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/tab_group_view_controller.mm
Audit Directions
- Raw pointers in async blocksAudit Objective-C completion/dismissal blocks that capture raw C++ model pointers (
TabGroup*,WebState*, etc.) for use after an animation or deferred callback; preferbase::WeakPtrcaptures. - Command handler signaturesReview
*Commandsprotocol methods that take non-owning raw pointers to model objects, since handlers are often invoked asynchronously relative to the caller. - CHECK-on-non-null as a lifetime guardFlag
CHECK(ptr)used where the real risk is the pointer being valid-but-dangling; a non-null CHECK does not detect a freed object the way aWeakPtrnull check does.
Patch
From 4c89d773467407c2802dc3f9b2af4c683bacbee6 Mon Sep 17 00:00:00 2001
From: Aliona DANGLA <alionadangla@google.com>
Date: Tue, 21 Apr 2026 10:07:14 -0700
Subject: [PATCH] [iOS] Fix use-after-free in RecentActivityCoordinator
Update TabGroupsCommands to use base::WeakPtr for
showTabGroupEditionForGroup: to fix a UAF in RecentActivityCoordinator
dismissal completion block.
Fixed: 504106200
Change-Id: Id6dce9aa3f3b35c6fe352051494f17d33c19eb02
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7781310
Reviewed-by: Eric Noyau <noyau@chromium.org>
Auto-Submit: Aliona Dangla <alionadangla@chromium.org>
Commit-Queue: Aliona Dangla <alionadangla@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618277}
---
diff --git a/ios/chrome/browser/shared/public/commands/tab_groups_commands.h b/ios/chrome/browser/shared/public/commands/tab_groups_commands.h
index 7210a89d..0c038de 100644
--- a/ios/chrome/browser/shared/public/commands/tab_groups_commands.h
+++ b/ios/chrome/browser/shared/public/commands/tab_groups_commands.h
@@ -34,7 +34,7 @@
- (void)hideTabGroupCreationAnimated:(BOOL)animated;
// Shows tab group edition view.
-- (void)showTabGroupEditionForGroup:(const TabGroup*)tabGroup;
+- (void)showTabGroupEditionForGroup:(base::WeakPtr<const TabGroup>)tabGroup;
// Show the current active tab.
- (void)showActiveTab;
diff --git a/ios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mm b/ios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mm
index 45e4fc85..92de67b 100644
--- a/ios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mm
+++ b/ios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mm
@@ -345,10 +345,12 @@
_tabGroupCreator = nil;
}
-- (void)showTabGroupEditionForGroup:(const TabGroup*)tabGroup {
+- (void)showTabGroupEditionForGroup:(base::WeakPtr<const TabGroup>)tabGroup {
CHECK(!_tabGroupCreator) << "There is an attempt to edit a tab group when a "
"creation process is still running.";
- CHECK(tabGroup) << "To edit a tab group you should pass a group.";
+ if (!tabGroup) {
+ return;
+ }
UIViewController* backgroundView = _tabGroupCoordinator
? _tabGroupCoordinator.viewController
@@ -356,7 +358,7 @@
_tabGroupCreator = [[CreateTabGroupCoordinator alloc]
initTabGroupEditionWithBaseViewController:backgroundView
browser:self.browser
- tabGroup:tabGroup];
+ tabGroup:tabGroup.get()];
_tabGroupCreator.delegate = self;
[_tabGroupCreator start];
}
diff --git a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_grid_coordinator.mm b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_grid_coordinator.mm
index 6f4b795..4db752c 100644
--- a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_grid_coordinator.mm
+++ b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_grid_coordinator.mm
@@ -1603,7 +1603,7 @@
} else {
coordinator = _regularGridCoordinator;
}
- [coordinator showTabGroupEditionForGroup:group.get()];
+ [coordinator showTabGroupEditionForGroup:group];
}
- (void)closeTabWithIdentifier:(web::WebStateID)identifier
diff --git a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mm b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mm
index 38afb7c0..8aeed7bd 100644
--- a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mm
+++ b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mm
@@ -135,10 +135,12 @@
}
id<TabGroupsCommands> tabGroupsHandler = HandlerForProtocol(
self.browser->GetCommandDispatcher(), TabGroupsCommands);
+ base::WeakPtr<const TabGroup> weakGroup = group->GetWeakPtr();
[_viewController.presentingViewController
dismissViewControllerAnimated:YES
completion:^{
- [tabGroupsHandler showTabGroupEditionForGroup:group];
+ [tabGroupsHandler
+ showTabGroupEditionForGroup:weakGroup];
}];
}
diff --git a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/tab_group_view_controller.mm b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/tab_group_view_controller.mm
index 0486af2a..9a812988 100644
--- a/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/tab_group_view_controller.mm
+++ b/ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/tab_group_view_controller.mm
@@ -779,7 +779,7 @@
// Displays the menu to rename and change the color of the currently displayed
// group.
- (void)displayEditionMenu {
- [_handler showTabGroupEditionForGroup:_tabGroup];
+ [_handler showTabGroupEditionForGroup:_tabGroup->GetWeakPtr()];
}
// Returns the tab group menu.
Original Bug Report
Potential UAF in RecentActivityCoordinator due to raw pointer capture bypassing BRP
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: RecentActivityCoordinator captures a raw const TabGroup* pointer in an asynchronous UIKit dismissal block. If a remote collaborator deletes the shared tab group during the dismissal animation, the memory is freed and unquarantined by BackupRefPtr. When the animation completes, the block executes with a dangling pointer, potentially leading to arbitrary memory increment and remote code execution in the browser process.
Affected files:
ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mmios/chrome/browser/shared/public/commands/tab_groups_commands.hios/chrome/browser/tab_switcher/tab_grid/base_grid/coordinator/base_grid_coordinator.mmios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/create_tab_group_mediator.mmios/chrome/browser/tab_switcher/ui_bundled/tab_group_item.mmios/chrome/browser/shared/model/web_state_list/web_state_list.mm
Estimated timestamp from git blame: 2025-04-02
Summary
A potential use-after-free (UAF) vulnerability exists in the Chrome for iOS browser process. In RecentActivityCoordinator, a raw const TabGroup* pointer is captured by value inside an Objective-C completion block for a view controller dismissal. If the tab group is destroyed (e.g., via a remote sync event from a collaborator) before the animation completes, the pointer becomes dangling. Because the capture uses a bare C pointer rather than a raw_ptr or base::WeakPtr, BackupRefPtr (BRP) does not track the reference, allowing the memory to be unquarantined and reallocated before the block executes.
Technical Details
In ios/chrome/browser/tab_switcher/ui_bundled/tab_grid/tab_groups/recent_activity_coordinator.mm, the method -showTabGroupEditForGroup: handles user interaction with a recent activity item. It dismisses the current view controller and defers opening the tab group editor until the animation completes:
- (void)showTabGroupEditForGroup:(const TabGroup*)group {
// ...
[_viewController.presentingViewController
dismissViewControllerAnimated:YES
completion:^{
[tabGroupsHandler showTabGroupEditionForGroup:group];
}];
}
Here, group is captured by value as a raw C pointer.
Suggested Exploitation Path
Note: These are potential steps based on static analysis.
- Attacker Action: An attacker modifies a shared tab group (e.g., changes its color), generating a sync event that appears in the victim’s “Recent Activity” list.
- Victim Interaction: The victim opens the Recent Activity sheet and taps the attacker’s edit. This calls
showTabGroupEditForGroup:, initiating a ~300ms UIKit dismissal animation. The completion block capturing the raw pointer is queued. - Race Condition: During this ~300ms window, the attacker deletes the shared tab group, which syncs to the victim’s device.
- Object Destruction: The sync event triggers
WebStateList::DeleteGroupIfEmpty(), executinggroups_.erase(iter). This destroys thestd::unique_ptr<TabGroup>, synchronously callingdeleteon theTabGroupinstance and invalidating itsWeakPtrFactory. - BRP Bypass: The deletion notifies
TabGroupMediator, which stops theTabGroupCoordinator. The coordinator destroys its internalraw_ptr<const TabGroup> _tabGroup. Because the Objective-C block only holds a bare C pointer, PartitionAlloc’s BRP reference count reaches zero. The memory is immediately unquarantined and returned to the free list. - Reallocation: The attacker uses other sync events to allocate objects of the same size, controlling the bytes at the dangling pointer’s address.
- Dangling Pointer Dereference: The UIKit animation finishes, and the completion block executes. The dangling pointer is passed to
CreateTabGroupMediator, which initializes aTabGroupItemand callstabGroup->GetWeakPtr().
Because the attacker controls the reallocated memory, they control the WeakReference::Flag address used by GetWeakPtr(), gaining an atomic memory increment primitive (AddRef()) in the unsandboxed browser process. This is a common primitive used to achieve Remote Code Execution (RCE).
Proposed Fix
The block should capture a base::WeakPtr<const TabGroup> instead of a raw pointer. If the group is deleted during the animation, the weak pointer will safely evaluate to null.
- (void)showTabGroupEditForGroup:(const TabGroup*)group {
// ...
base::WeakPtr<const TabGroup> weakGroup = group->GetWeakPtr();
[_viewController.presentingViewController
dismissViewControllerAnimated:YES
completion:^{
if (weakGroup) {
[tabGroupsHandler showTabGroupEditionForGroup:weakGroup.get()];
}
}];
}
(Note that the TabGroupsCommands protocol may also need updating to accept a base::WeakPtr or handle null gracefully).
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.