CVE-2026-84350
Overview
Background
- `TabGroup`
- the browser-side object modeling a group of tabs (its id, visual data, collapsed state); it can be destroyed synchronously when its last relevant tab leaves or it is auto-closed.
- `ToggleTabGroupCollapsedState`
- the tab-strip controller method that collapses/expands a group; while collapsing a focused group it may switch the active tab or add a tab.
- `kTabGroupsFocusing` / `tab_groups_focusing_auto_close`
- the feature under which focusing behavior can automatically close (and synchronously destroy) a group as a side effect of activating a different tab.
- `base::WeakPtr` / `ContainsTabGroup`
- a weak pointer that becomes null when its target is destroyed, and the
group_model()query that reports whether aTabGroupstill exists; both are used to detect that the group was freed mid-operation.
Root Cause Analysis
In both BrowserTabStripController::ToggleTabGroupCollapsedState and VerticalTabStripController::ToggleTabGroupCollapsedState, the code held a raw TabGroup* group pointer, then, while collapsing a focused group, called into the TabStripModel to switch the active tab (ActivateTabAt) or add a tab. Under the kTabGroupsFocusing auto-close feature, that activation can trigger the group’s automatic closure, which synchronously destroys the TabGroup, leaving group dangling. The functions then continued to use group — passing it to tabstrip_->ToggleTabGroup(group, ...) / model_->ChangeTabGroupVisuals(...) and reading group->id()/visual data — a use-after-free, because no check confirmed the group still existed after the reentrant model call.
The fix adds a liveness check that runs before those uses: BrowserTabStripController returns early if !model_->group_model()->ContainsTabGroup(group), and VerticalTabStripController captures base::WeakPtr<const TabGroup> weak_group = group->AsWeakPtr() before the activation and returns early if !weak_group afterward. TabGroup gains an AsWeakPtr() accessor and a mutable base::WeakPtrFactory<TabGroup> weak_ptr_factory_ to support the weak check, and the regression test CollapseFocusedGroupTriggersUseAfterFree confirms the group is gone after the toggle.
TabGroup* was reused after a reentrant TabStripModel call that, under the focusing auto-close feature, could synchronously free the group; the fix verifies the group’s continued existence (via ContainsTabGroup or a WeakPtr) and returns early before any post-activation use.Attack Path
- Set up a focused group
A single tab is placed in a new group and that group is made the focused group with the auto-close feature (
tab_groups_focusing_auto_close) enabled, as the regression test does withAddToNewGroup/SetFocusedGroup. - Collapse the focused group
ToggleTabGroupCollapsedStateis invoked (e.g. by a mouse action on the group header), and because the group is focused it proceeds to switch the active tab / add a tab via theTabStripModel. - Synchronous group destruction
The activation triggers the focusing auto-close path, which synchronously closes and destroys the
TabGroup, leaving the localgrouppointer dangling. - Use-after-free
The controller continues and dereferences the freed
group(passing it toToggleTabGroup/ChangeTabGroupVisualsand reading its id/visual data), a use-after-free in the browser process; further exploitation into a controlled read/write or code execution is(inferred)and not shown by the patch.
Impact Assessment
TabGroup, yielding memory corruption. Preconditions are the kTabGroupsFocusing auto-close feature being enabled and a focused group being collapsed while its activation auto-closes it. The patch demonstrates only the UAF; escalation to a read/write primitive or code execution is (inferred) and not established here.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc |
modified | |
VerticalTabStripControllerFocusingAutoCloseBrowserTestchrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.ccchrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cccomponents/tabs/public/tab_group.hchrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
Audit Directions
- Raw pointer reused after a reentrant model callAudit tab-strip/controller code that holds a
TabGroup*, tab, or view pointer across aTabStripModelmutation (activate/add/close) that can synchronously destroy the pointee. - Feature-gated synchronous destructionReview paths where a new feature (like focusing auto-close) adds a synchronous “close/destroy” side effect to previously side-effect-free operations, and confirm all pre-existing callers revalidate their objects afterward.
- Missing liveness checks after callbacks/activationLook for continued use of objects after
ActivateTabAt-style calls without aWeakPtr/Contains*guard preceding the next dereference.
Patch
From 926b0949a8dafd79619ef2b13bdff02e57ac289d Mon Sep 17 00:00:00 2001
From: David Pennington <dpenning@google.com>
Date: Thu, 25 Jun 2026 15:25:21 -0700
Subject: [PATCH] Fix BrowserTabStripController UAF on focused-group auto-close
Because the focus feature can close a group, attempting to use the
ptr after the closure can result in UAF. we can either requery the
group from the tabstripmodel or we can reget the ptr, i chose to
use a weakptr to requery the validity of the ptr so that we dont have
to do another lookup.
Also added a regression test for VerticalTabStripController UAF
Bug: 513713427
Change-Id: I4b0d0fa580681e063a2ee33d5511e781ea68a740
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7998801
Reviewed-by: Charles Meng <charlesmeng@chromium.org>
Commit-Queue: David Pennington <dpenning@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1652763}
---
diff --git a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
index a95ae65..c1b3e9b 100644
--- a/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
+++ b/chrome/browser/ui/views/tabs/browser_tab_strip_controller.cc
@@ -462,6 +462,13 @@
}
}
+ // Under the kTabGroupsFocusingAutoClose feature, switching the active tab or
+ // adding a new tab can cause the group to be automatically closed and
+ // synchronously destroyed. We must check that the group still exists.
+ if (!model_->group_model()->ContainsTabGroup(group)) {
+ return;
+ }
+
if (origin != ToggleTabGroupCollapsedStateOrigin::kMenuAction ||
should_toggle_group) {
tabstrip_->ToggleTabGroup(group, !is_currently_collapsed, origin);
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc
index d753488..05ae574 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc
@@ -6,6 +6,7 @@
#include <variant>
+#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
@@ -287,6 +288,11 @@
bool is_currently_collapsed = group->visual_data()->is_collapsed();
bool should_toggle_group = true;
+ // We use a WeakPtr because switching the active tab or adding
+ // a new tab during a collapse operation can trigger the automatic
+ // closure of the group, which synchronously destroys the TabGroup.
+ base::WeakPtr<const TabGroup> weak_group = group->AsWeakPtr();
+
tabs::TabInterface* active_tab = model_->GetActiveTab();
if (!is_currently_collapsed && active_tab && !drag_handler_->IsDragging()) {
if (active_tab->GetGroup() == group->id()) {
@@ -296,6 +302,7 @@
// be created.
const std::optional<int> next_active =
model_->GetNextExpandedActiveTab(group->id());
+
if (next_active.has_value()) {
model_->ActivateTabAt(
next_active.value(),
@@ -321,6 +328,10 @@
}
}
+ if (!weak_group) {
+ return;
+ }
+
if (origin != ToggleTabGroupCollapsedStateOrigin::kMenuAction ||
should_toggle_group) {
model_->ChangeTabGroupVisuals(
diff --git a/components/tabs/public/tab_group.h b/components/tabs/public/tab_group.h
index c6c9d323..1b55e2ce 100644
--- a/components/tabs/public/tab_group.h
+++ b/components/tabs/public/tab_group.h
@@ -11,6 +11,7 @@
#include "base/callback_list.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/tab_collection.h"
@@ -50,6 +51,11 @@
};
virtual ~TabGroup();
+ base::WeakPtr<TabGroup> AsWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); }
+ base::WeakPtr<const TabGroup> AsWeakPtr() const {
+ return weak_ptr_factory_.GetWeakPtr();
+ }
+
const tab_groups::TabGroupId& id() const { return id_; }
const tab_groups::TabGroupVisualData* visual_data() const {
return visual_data_.get();
@@ -137,6 +143,8 @@
bool is_closing_ = false;
bool is_customized_ = false;
+
+ mutable base::WeakPtrFactory<TabGroup> weak_ptr_factory_{this};
};
#endif // COMPONENTS_TABS_PUBLIC_TAB_GROUP_H_
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
index c436ae2..7d91292f 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
@@ -487,4 +487,37 @@
EXPECT_FALSE(tab0->GetGroup().has_value());
}
+class VerticalTabStripControllerFocusingAutoCloseBrowserTest
+ : public VerticalTabsBrowserTestMixin<InProcessBrowserTest> {
+ public:
+ const std::vector<base::test::FeatureRefAndParams> GetEnabledFeatures()
+ override {
+ return {
+ {features::kTabGroupsFocusing,
+ {{"tab_groups_focusing_auto_close", "true"}}},
+ {tabs::kVerticalTabs, {}},
+ };
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(VerticalTabStripControllerFocusingAutoCloseBrowserTest,
+ CollapseFocusedGroupTriggersUseAfterFree) {
+ AppendTab();
+
+ TabStripModel* model = browser()->tab_strip_model();
+ ASSERT_EQ(2, model->count());
+
+ tab_groups::TabGroupId group_id = model->AddToNewGroup({0});
+ TabGroup* group = model->group_model()->GetTabGroup(group_id);
+
+ model->SetFocusedGroup(group_id);
+ ASSERT_EQ(0, model->active_index());
+ ASSERT_EQ(group_id, model->GetFocusedGroup());
+
+ vertical_tab_strip_controller()->ToggleTabGroupCollapsedState(
+ group, ToggleTabGroupCollapsedStateOrigin::kMouse);
+
+ EXPECT_FALSE(model->group_model()->ContainsTabGroup(group_id));
+}
+
} // namespace
Regression Test / PoC
diff --git a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
index c436ae2..7d91292f 100644
--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
+++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller_browsertest.cc
@@ -487,4 +487,37 @@
EXPECT_FALSE(tab0->GetGroup().has_value());
}
+class VerticalTabStripControllerFocusingAutoCloseBrowserTest
+ : public VerticalTabsBrowserTestMixin<InProcessBrowserTest> {
+ public:
+ const std::vector<base::test::FeatureRefAndParams> GetEnabledFeatures()
+ override {
+ return {
+ {features::kTabGroupsFocusing,
+ {{"tab_groups_focusing_auto_close", "true"}}},
+ {tabs::kVerticalTabs, {}},
+ };
+ }
+};
+
+IN_PROC_BROWSER_TEST_F(VerticalTabStripControllerFocusingAutoCloseBrowserTest,
+ CollapseFocusedGroupTriggersUseAfterFree) {
+ AppendTab();
+
+ TabStripModel* model = browser()->tab_strip_model();
+ ASSERT_EQ(2, model->count());
+
+ tab_groups::TabGroupId group_id = model->AddToNewGroup({0});
+ TabGroup* group = model->group_model()->GetTabGroup(group_id);
+
+ model->SetFocusedGroup(group_id);
+ ASSERT_EQ(0, model->active_index());
+ ASSERT_EQ(group_id, model->GetFocusedGroup());
+
+ vertical_tab_strip_controller()->ToggleTabGroupCollapsedState(
+ group, ToggleTabGroupCollapsedStateOrigin::kMouse);
+
+ EXPECT_FALSE(model->group_model()->ContainsTabGroup(group_id));
+}
+
} // namespace