diff --git a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc index 26a6dabd68f1c..f25c09f70cd4e 100644 --- a/chrome/browser/ui/tabs/tab_strip_model_unittest.cc +++ b/chrome/browser/ui/tabs/tab_strip_model_unittest.cc @@ -7274,3 +7274,56 @@ TEST_P(TabStripModelTest, ReinsertTabGroupCollectionVerifyListSelectionModel) { EXPECT_EQ("active=2 anchor=2 selection=2 4", tabstrip()->selection_model().GetListSelectionModel().ToString()); } + +// ----------------------------------------------------------------------------- +// POC: Use-After-Free in MoveTabToIndexImpl via reentrant CloseWebContentsAt. +// +// CloseWebContentsAt() / CloseTabs() lack a ReentrancyCheck guard, so an +// observer that synchronously closes the moving tab during the kMoved +// notification destroys the TabModel while MoveTabToIndexImpl() still holds a +// raw `tabs::TabInterface*` to it. After SendMoveNotificationForTab() returns, +// MoveTabToIndexImpl() dereferences the freed pointer via `tab->IsPinned()`. +namespace { + +class CloseOnMoveObserver : public TabStripModelObserver { + public: + void OnTabStripModelChanged( + TabStripModel* model, + const TabStripModelChange& change, + const TabStripSelectionChange& selection) override { + if (fired_ || change.type() != TabStripModelChange::kMoved) { + return; + } + fired_ = true; + const TabStripModelChange::Move* move = change.GetMove(); + // Close the tab that is currently being moved. CloseWebContentsAt() has no + // ReentrancyCheck, so this proceeds and synchronously destroys the + // underlying TabModel inside SendDetachWebContentsNotifications(). + model->CloseWebContentsAt(move->to_index, + TabCloseTypes::CLOSE_USER_GESTURE); + } + + private: + bool fired_ = false; +}; + +} // namespace + +TEST_P(TabStripModelTest, ReentrantCloseDuringMoveUAF) { + // Three tabs so that closing one doesn't trip the "closing all" path. + tabstrip()->AppendWebContents(CreateWebContents(), /*foreground=*/true); + tabstrip()->AppendWebContents(CreateWebContents(), /*foreground=*/false); + tabstrip()->AppendWebContents(CreateWebContents(), /*foreground=*/false); + ASSERT_EQ(3, tabstrip()->count()); + + CloseOnMoveObserver evil_observer; + tabstrip()->AddObserver(&evil_observer); + + // Move tab 0 -> 2. During the kMoved notification, |evil_observer| closes + // the moving tab. After the notification returns, MoveTabToIndexImpl() calls + // `tab->IsPinned()` on a freed object => heap-use-after-free under ASAN. + tabstrip()->MoveWebContentsAt(/*index=*/0, /*to_position=*/2, + /*select_after_move=*/false); + + tabstrip()->RemoveObserver(&evil_observer); +}