CVE-2026-8521
Overview
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
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.