diff --git a/chrome/browser/ui/webui/history_clusters/history_clusters_handler_browsertest.cc b/chrome/browser/ui/webui/history_clusters/history_clusters_handler_browsertest.cc index 5ddc81f08a243..85778f7c69c1e 100644 --- a/chrome/browser/ui/webui/history_clusters/history_clusters_handler_browsertest.cc +++ b/chrome/browser/ui/webui/history_clusters/history_clusters_handler_browsertest.cc @@ -15,6 +15,9 @@ #include "chrome/browser/prefs/incognito_mode_prefs.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_commands.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/tabs/tab_enums.h" #include "chrome/browser/ui/webui/history/history_ui.h" #include "chrome/test/base/in_process_browser_test.h" @@ -27,7 +30,9 @@ #include "content/public/browser/navigation_entry.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" +#include "ui/base/mojom/window_open_disposition.mojom.h" #include "ui/menus/simple_menu_model.h" +#include "url/url_constants.h" #include "ui/webui/resources/cr_components/history_clusters/history_clusters.mojom.h" namespace history_clusters { @@ -346,4 +351,87 @@ IN_PROC_BROWSER_TEST_F(HistoryClustersHandlerBrowserTest, EXPECT_FALSE(IsOpenInIncognitoEnabled(menu_model.get())); } +// Regression test for a use-after-free in HistoryClustersHandler. +// +// HistoryClustersHandler caches a tabs::TabInterface* (inside a std::variant +// of raw pointers, so it is not protected by MiraclePtr). When a popup window +// hosting chrome://history is converted to a tabbed browser via +// ConvertPopupToTabbedBrowser (the "Show as tab" user action), the underlying +// TabModel is destroyed by TabStripModel::DetachWebContentsAtForInsertion -> +// tabs::TabModel::DestroyAndTakeWebContents while the WebContents (and thus +// the WebUI controller and the HistoryClustersHandler) survives and is moved +// into the new browser. The cached TabInterface* is now dangling. +// +// A compromised WebUI renderer can then send the +// history_clusters.mojom.PageHandler.OpenHistoryUrl IPC, which dispatches to +// HistoryClustersHandler::OpenHistoryUrl in the browser process. That method +// calls GetBrowserWindowInterface(interface_), which performs a virtual call +// on the freed TabInterface, yielding control-flow hijack in the browser. +// +// This test simulates that exact sequence and is expected to trigger an ASAN +// heap-use-after-free in GetBrowserWindowInterface(). +class HistoryClustersHandlerPopupUafBrowserTest : public InProcessBrowserTest { + public: + HistoryClustersHandlerPopupUafBrowserTest() { + feature_list_.InitWithFeatures({history_clusters::internal::kJourneys}, {}); + } + ~HistoryClustersHandlerPopupUafBrowserTest() override = default; + + private: + base::test::ScopedFeatureList feature_list_; +}; + +IN_PROC_BROWSER_TEST_F(HistoryClustersHandlerPopupUafBrowserTest, + UseAfterFreeOnPopupToTabConversion) { + // Set the pref so the page binds the history clusters PageHandler on load. + browser()->profile()->GetPrefs()->SetInteger( + history_clusters::prefs::kLastSelectedTab, + history_clusters::prefs::TabbedPage::GROUP); + + // Step 1: Create a popup browser. This corresponds to the user (or an + // attacker via window.open from another WebUI) opening chrome://history in a + // popup-type window. + Browser* popup = Browser::Create( + Browser::CreateParams(Browser::TYPE_POPUP, browser()->profile(), + /*user_gesture=*/true)); + chrome::AddTabAt(popup, GURL(url::kAboutBlankURL), -1, true); + popup->window()->Show(); + + // Step 2: Navigate the popup to chrome://history/grouped. The page's JS + // binds history_clusters.mojom.PageHandler, which causes + // HistoryUI::BindInterface() to construct a HistoryClustersHandler that + // caches the popup tab's tabs::TabInterface* in its `interface_` field. + ASSERT_TRUE(ui_test_utils::NavigateToURL( + popup, GURL(GetChromeUIHistoryClustersURL()))); + content::WebContents* web_contents = + popup->tab_strip_model()->GetActiveWebContents(); + ASSERT_TRUE(content::WaitForLoadStop(web_contents)); + + HistoryUI* history_ui = + web_contents->GetWebUI()->GetController()->GetAs(); + ASSERT_TRUE(history_ui); + HistoryClustersHandler* handler = + history_ui->GetHistoryClustersHandlerForTesting(); + ASSERT_TRUE(handler) + << "Page did not bind history_clusters.mojom.PageHandler"; + + // Step 3: Convert the popup to a tabbed browser ("Show as tab"). This + // destroys the popup's TabModel while moving the WebContents (and the still- + // alive HistoryClustersHandler) into a new normal browser. The handler's + // cached TabInterface* is now dangling. + chrome::ConvertPopupToTabbedBrowser(popup); + + // Step 4: Simulate the compromised renderer sending the OpenHistoryUrl IPC. + // The Mojo dispatcher would invoke exactly this method on the browser-side + // handler. Under ASAN this is expected to report heap-use-after-free when + // GetBrowserWindowInterface() makes a virtual call on the freed TabModel. + auto modifiers = ui::mojom::ClickModifiers::New(); + modifiers->middle_button = false; + modifiers->alt_key = false; + modifiers->ctrl_key = false; + modifiers->meta_key = false; + modifiers->shift_key = false; + handler->OpenHistoryUrl(GURL("https://example.com/"), std::move(modifiers)); +} + } // namespace history_clusters