CVE-2026-11262
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ReentrancyCheckchrome/browser/ui/tabs/tab_strip_model.cc |
modified | |
TabStripModelCloseWebContentsOnChangeObserverchrome/browser/ui/tabs/tab_strip_model_browsertest.cc |
modified | |
TabStripModelTestTabGroupEntryPointsEnabledchrome/browser/ui/tabs/tab_strip_model_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/tabs/tab_strip_model.ccchrome/browser/ui/tabs/tab_strip_model_browsertest.cc
Patch
From 0985a479f32c2194a7632473c8d9c741bb388220 Mon Sep 17 00:00:00 2001
From: Charles Meng <charlesmeng@chromium.org>
Date: Fri, 10 Apr 2026 14:51:11 -0700
Subject: [PATCH] Fix UAF in tab strip model
Add a reentrancy check to CloseWebContentsAt so that it cannot be called
during a move operation (which would free the tab pointer, causing UAF).
Change UpdateTabInSplitImpl to use the private impl CloseTabs instead
of CloseWebContentsAt so that there is no reentrancy check.
Fixed: 499386363
Change-Id: Id2ad4abd1505259e0dcac07b0adebcafc3f50d87
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7745674
Commit-Queue: Charles Meng <charlesmeng@chromium.org>
Reviewed-by: Eshwar Stalin <estalin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1613117}
---
diff --git a/chrome/browser/ui/tabs/tab_strip_model.cc b/chrome/browser/ui/tabs/tab_strip_model.cc
index 7db62be3..8b9e8faa 100644
--- a/chrome/browser/ui/tabs/tab_strip_model.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model.cc
@@ -129,13 +129,17 @@
class ReentrancyCheck {
public:
explicit ReentrancyCheck(bool* guard_flag) : guard_flag_(guard_flag) {
- CHECK_CURRENTLY_ON(content::BrowserThread::UI);
- CHECK(!*guard_flag_);
+ ValidateNotReentrant(guard_flag_);
*guard_flag_ = true;
}
~ReentrancyCheck() { *guard_flag_ = false; }
+ static void ValidateNotReentrant(bool* guard_flag) {
+ CHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ CHECK(!*guard_flag);
+ }
+
private:
const raw_ptr<bool> guard_flag_;
};
@@ -1315,6 +1319,8 @@
}
void TabStripModel::CloseWebContentsAt(int index, uint32_t close_types) {
+ ReentrancyCheck::ValidateNotReentrant(&reentrancy_guard_);
+
CHECK(ContainsIndex(index));
CloseTabs({GetWebContentsAt(index)}, close_types);
}
@@ -4250,8 +4256,7 @@
const int split_index = GetIndexOfTab(split_tab);
MoveTabToIndexImpl(update_index, split_index, split_tab->GetGroup(),
split_tab->IsPinned(), initial_split_active);
- CloseWebContentsAt(GetIndexOfTab(split_tab),
- TabCloseTypes::CLOSE_USER_GESTURE);
+ CloseTabs({split_tab->GetContents()}, TabCloseTypes::CLOSE_USER_GESTURE);
} else {
tabs::TabInterface* update_tab = GetTabAtIndex(update_index);
std::optional<tab_groups::TabGroupId> initial_split_group =
diff --git a/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc b/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
index 2d3e10b..dce8eae 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
@@ -478,6 +478,34 @@
GetTabStripStateString(tab_strip_model));
}
+IN_PROC_BROWSER_TEST_F(TabStripModelBrowserTest,
+ TestCloseTabDuringMoveOperation) {
+ TabStripModel* const tab_strip_model = browser()->tab_strip_model();
+ ASSERT_NO_FATAL_FAILURE(
+ PrepareTabstripForSelectionTest(tab_strip_model, 2, 0, {0}));
+ ASSERT_EQ(2, tab_strip_model->count());
+
+ class TabStripModelCloseWebContentsOnChangeObserver
+ : public TabStripModelObserver {
+ public:
+ void OnTabStripModelChanged(
+ TabStripModel* tab_strip_model,
+ const TabStripModelChange& change,
+ const TabStripSelectionChange& selection) override {
+ if (change.type() == TabStripModelChange::Type::kMoved) {
+ tab_strip_model->CloseWebContentsAt(1,
+ TabCloseTypes::CLOSE_USER_GESTURE);
+ }
+ }
+ };
+ TabStripModelCloseWebContentsOnChangeObserver close_tab_observer;
+ tab_strip_model->AddObserver(&close_tab_observer);
+
+ EXPECT_DEATH_IF_SUPPORTED(tab_strip_model->MoveWebContentsAt(0, 1, false),
+ "Check failed");
+ tab_strip_model->RemoveObserver(&close_tab_observer);
+}
+
class TabStripModelTestTabGroupEntryPointsEnabled
: public TabStripModelBrowserTest {
public:
Regression Test / PoC
diff --git a/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc b/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
index 2d3e10b..dce8eae 100644
--- a/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
+++ b/chrome/browser/ui/tabs/tab_strip_model_browsertest.cc
@@ -478,6 +478,34 @@
GetTabStripStateString(tab_strip_model));
}
+IN_PROC_BROWSER_TEST_F(TabStripModelBrowserTest,
+ TestCloseTabDuringMoveOperation) {
+ TabStripModel* const tab_strip_model = browser()->tab_strip_model();
+ ASSERT_NO_FATAL_FAILURE(
+ PrepareTabstripForSelectionTest(tab_strip_model, 2, 0, {0}));
+ ASSERT_EQ(2, tab_strip_model->count());
+
+ class TabStripModelCloseWebContentsOnChangeObserver
+ : public TabStripModelObserver {
+ public:
+ void OnTabStripModelChanged(
+ TabStripModel* tab_strip_model,
+ const TabStripModelChange& change,
+ const TabStripSelectionChange& selection) override {
+ if (change.type() == TabStripModelChange::Type::kMoved) {
+ tab_strip_model->CloseWebContentsAt(1,
+ TabCloseTypes::CLOSE_USER_GESTURE);
+ }
+ }
+ };
+ TabStripModelCloseWebContentsOnChangeObserver close_tab_observer;
+ tab_strip_model->AddObserver(&close_tab_observer);
+
+ EXPECT_DEATH_IF_SUPPORTED(tab_strip_model->MoveWebContentsAt(0, 1, false),
+ "Check failed");
+ tab_strip_model->RemoveObserver(&close_tab_observer);
+}
+
class TabStripModelTestTabGroupEntryPointsEnabled
: public TabStripModelBrowserTest {
public:
Original Bug Report
Potential Use-After-Free in TabStripModel::MoveTabToIndexImpl due to missing re-entrancy guard
Flapjack, 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 security team.
Overview: A potential Use-After-Free vulnerability exists in TabStripModel because CloseWebContentsAt and CloseTabs lack re-entrancy guards. This allows a synchronous observer to destroy a tab while it is being moved, bypassing MiraclePtr protection and leading to a use-after-free when the move operation resumes.
Affected files:
chrome/browser/ui/tabs/tab_strip_model.ccchrome/browser/ui/tabs/tab_strip_model.h
Estimated timestamp from git blame: 2025-12-30
Summary
A potential Use-After-Free (UAF) vulnerability has been identified in TabStripModel within the browser process. The issue arises from the omission of ReentrancyCheck guards in the TabStripModel::CloseWebContentsAt and TabStripModel::CloseTabs methods. This omission permits synchronous observers to initiate tab closures while another model operation, such as moving a tab, is already underway.
Specifically, during a tab move operation, TabStripModel::MoveTabToIndexImpl caches a raw pointer to the tab, synchronously notifies observers, and then later dereferences the raw pointer. If an observer closes the tab during the notification, the tab is destroyed, leading to a UAF upon return. Furthermore, the local raw_ptr that temporarily protects the tab during the notification is destroyed before the vulnerable dereference, bypassing MiraclePtr (BRP) protections.
Technical Details
- Missing Guard: Most state-modifying methods in
TabStripModel(e.g.,MoveWebContentsAt,InsertWebContentsAt) instantiate aReentrancyCheck reentrancy_check(&reentrancy_guard_)to prevent nested modifications. However,CloseWebContentsAtandCloseTabsdo not. - Vulnerable Flow: When a tab is moved,
MoveWebContentsAtcorrectly sets the re-entrancy guard and callsMoveTabToIndexImpl. - Raw Pointer Usage:
MoveTabToIndexImplstores a raw pointer:tabs::TabInterface* const tab = GetTabAtIndex(initial_index);. - Observer Notification: It then calls
SendMoveNotificationForTab(initial_index, final_index, tab, selection);. - Temporary BRP Protection: Inside
SendMoveNotificationForTab, the tab is placed into aTabStripModelChange::Movestruct, which stores it as araw_ptr<tabs::TabInterface>. This temporarily increments the BackupRefPtr (BRP) reference count. - Re-entrant Close: Observers are notified synchronously. If an attacker-controlled or influenced observer (e.g., via an extension) calls
CloseWebContentsAtfor the moving tab, the call proceeds becauseCloseWebContentsAtlacks aReentrancyCheck. The tab is subsequently detached and deleted. - Protection Scope Ends: When the notification completes,
SendMoveNotificationForTabreturns, and its localTabStripModelChangeobject is destroyed. This decrements the BRP reference count to zero, freeing the memory for reallocation. - The UAF: Execution resumes in
MoveTabToIndexImpl, which immediately callsif (initial_pinned_state != tab->IsPinned()). The virtual callIsPinned()dereferences the now-freed and unquarantined rawtabpointer. If an attacker has sprayed the heap in the interim, this results in Remote Code Execution (RCE) in the browser process.
Note: These are potential steps, as our tooling does not currently run the code to produce a working proof-of-concept.
Suggested Fix
- Add Re-entrancy Guards: Instantiate
ReentrancyCheck reentrancy_check(&reentrancy_guard_);inTabStripModel::CloseWebContentsAtandTabStripModel::CloseTabsto prevent nested tab closures. - Defense in Depth: In
MoveTabToIndexImpl(and similar methods likeInsertTabAtIndexImpl), usebase::raw_ptr<tabs::TabInterface> tab = GetTabAtIndex(...)instead of a raw pointer. This ensures that BRP quarantines the memory for the entire duration of the function’s execution, turning any potential UAFs from complex re-entrancy bugs into safe crashes.
Evaluated with Chrome root at commit: 09ec9e7cc4d24823d20b6d37cf3d282734f6bf0f
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.