diff --git a/chrome/browser/printing/print_browsertest.cc b/chrome/browser/printing/print_browsertest.cc index eaeb5a79db5d8..3d7c66740789f 100644 --- a/chrome/browser/printing/print_browsertest.cc +++ b/chrome/browser/printing/print_browsertest.cc @@ -44,6 +44,7 @@ #include "chrome/browser/profiles/profile.h" #include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h" #include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" @@ -64,6 +65,7 @@ #include "content/public/browser/render_process_host.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" +#include "content/public/browser/web_contents_delegate.h" #include "content/public/test/back_forward_cache_util.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" @@ -1775,6 +1777,94 @@ IN_PROC_BROWSER_TEST_F(PrintBrowserTest, WindowDotPrint) { print_preview_observer.WaitUntilPreviewIsReady(); } +namespace { + +// Simulates the documented content/ behavior where +// WebContentsDelegate::ExitFullscreenModeForTab() may synchronously destroy +// the WebContents (web_contents_impl.cc: "This may spin the message loop and +// destroy this object", crbug.com/1506535, crbug.com/498752242). On Windows +// this happens in production when SetWindowLong/SetWindowPos dispatches a +// re-entrant window-destruction message during fullscreen exit. +class DestroyingFullscreenDelegate : public content::WebContentsDelegate { + public: + DestroyingFullscreenDelegate( + std::unique_ptr* owned_contents, + base::OnceClosure on_destroyed) + : owned_contents_(owned_contents), + on_destroyed_(std::move(on_destroyed)) {} + + bool IsFullscreenForTabOrPending( + const content::WebContents* web_contents) override { + return true; + } + + void ExitFullscreenModeForTab(content::WebContents* web_contents) override { + // Synchronously destroy the WebContents, which destroys its + // WebContentsUserData-owned PrintViewManager while + // PrintViewManager::OnScriptedPrintPreviewCallback() is still on the + // stack. + web_contents->SetDelegate(nullptr); + owned_contents_->reset(); + if (on_destroyed_) { + std::move(on_destroyed_).Run(); + } + } + + private: + raw_ptr> owned_contents_; + base::OnceClosure on_destroyed_; +}; + +} // namespace + +// Regression test for a browser-process UAF: OnScriptedPrintPreviewCallback() +// calls web_contents()->ExitFullscreen() and then continues to dereference +// `this` (lines reading print_preview_rfh_, web_contents(), and the virtual +// PrintPreviewAllowedForTesting()) without a WeakPtr re-check. Because +// PrintViewManager is WebContentsUserData, destruction of the WebContents +// inside ExitFullscreenModeForTab() frees `this`. On ASAN builds this test +// produces heap-use-after-free if the bug is present. +IN_PROC_BROWSER_TEST_F(PrintBrowserTest, + ScriptedPrintPreviewExitFullscreenDestroysWebContents) { + // Load any page so window.print() has a renderer to run in. Use the + // embedded test server so the document has a committed http origin (needed + // by GetFullscreenUserData()->last_exits() in ExitFullscreenMode()). + ASSERT_TRUE(ui_test_utils::NavigateToURL( + browser(), embedded_test_server()->GetURL("/printing/test1.html"))); + + // Add a second tab so the browser stays open after detaching the first. + chrome::NewTab(browser()); + ASSERT_EQ(2, browser()->tab_strip_model()->count()); + + // Take ownership of the first tab's WebContents so it can be destroyed + // synchronously inside ExitFullscreenModeForTab(). + std::unique_ptr owned_contents = + browser()->tab_strip_model()->DetachWebContentsAtForInsertion(0); + ASSERT_TRUE(owned_contents); + content::WebContents* contents = owned_contents.get(); + ASSERT_TRUE(PrintViewManager::FromWebContents(contents)); + + base::RunLoop run_loop; + DestroyingFullscreenDelegate delegate(&owned_contents, + run_loop.QuitClosure()); + contents->SetDelegate(&delegate); + ASSERT_TRUE(contents->IsFullscreen()); + + // window.print() sends SetupScriptedPrintPreview followed by + // ShowScriptedPrintPreview to the browser. On non-ChromeOS the latter + // synchronously runs OnScriptedPrintPreviewCallback on the Mojo dispatch + // stack, which calls web_contents()->ExitFullscreen(true) and then keeps + // using `this`. The custom delegate synchronously destroys the WebContents + // inside ExitFullscreenModeForTab(), exercising the missing-WeakPtr-guard. + content::ExecuteScriptAsync(contents->GetPrimaryMainFrame(), + "window.print();"); + run_loop.Run(); + + // If we reach here without ASAN reporting heap-use-after-free, the bug is + // fixed (PrintViewManager re-checked its WeakPtr after ExitFullscreen()). + EXPECT_FALSE(owned_contents); +} + IN_PROC_BROWSER_TEST_F(PrintBrowserTest, PdfWindowDotPrint) { // Do not add any other printers; will default to "Save as PDF".