CVE-2026-102324
Overview
Background
- `PictureInPictureWindowManager`
- the singleton that owns the active picture-in-picture (PiP) window and can close it via
ExitPictureInPicture()(synchronous) orExitPictureInPictureSoon()(posts a task to close later). - `ExitPictureInPicture()`
- synchronously closes the PiP window and destroys associated objects, including the tab’s
TabFeatures, immediately within the calling stack frame. - `OnOcclusionStateChanged` occlusion callback
- dialog delegates (shortcut, web-app modal, web-app update-review) that, when a PiP window occludes a dialog, force the PiP window closed “to prevent spoofing.”
- Re-entrancy / use-after-free
- destroying objects synchronously from inside an observer/callback that is still executing on those very objects leaves the caller (e.g. the dialog-showing path) holding freed pointers.
Root Cause Analysis
The dialog delegates called PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture() directly from their OnOcclusionStateChanged occlusion callbacks. Because ExitPictureInPicture() synchronously tears down the PiP window and destroys the associated TabFeatures, invoking it while a dialog was being shown on that same window destroyed objects that the in-progress dialog/occlusion machinery was still using, producing re-entrancy and a use-after-free (observed as an ASan crash when trying to show the dialog). The invariant violated is that objects must not be destroyed synchronously from within a callback that is still operating on them.
The fix switches all three occlusion callbacks to the newly public static ExitPictureInPictureSoon(), which posts a task so the PiP window is closed after the current call stack unwinds, and documents ExitPictureInPicture() as unsafe to call from callbacks/observers. This works because deferring the teardown lets the dialog-show path finish and the stack unwind before TabFeatures and the window are destroyed.
ExitPictureInPictureSoon().Attack Path
- Open a PiP window over a target surface A page or web app opens a document/video PiP window positioned to occlude a browser dialog. (inferred that page content drives PiP placement.)
- Cause the PiP window to occlude a dialog
A dialog (create-desktop-shortcut, web-app modal, or web-app update-review) is shown and detects occlusion by the PiP window, firing
OnOcclusionStateChanged(occluded=true). - Synchronous teardown re-enters
Pre-patch, the callback calls
ExitPictureInPicture(), which synchronously destroys the PiP window and itsTabFeatureswhile the dialog-show / occlusion path is still on the stack. - Use-after-free
The still-executing dialog machinery dereferences the now-destroyed
TabFeatures/window objects, causing the use-after-free crash. (The concrete exploitation beyond the crash is inferred; the patch demonstrates the re-entrant free, not a controlled primitive.)
Impact Assessment
TabFeatures in the browser process, the class the advisory reports as use-after-free in PictureInPicture. The precondition is having a PiP window occlude one of the affected dialogs; escalation from the crash to controlled code execution is (inferred) and not demonstrated by the patch, which merely defers the teardown. No exploited-in-the-wild status is stated in the advisory.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc |
modified | |
ifchrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc |
modified | |
ifchrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc |
modified | |
ifchrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc |
modified | |
TEST_Fchrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc |
modified |
Files Changed
chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.ccchrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.ccchrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.ccchrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.ccchrome/browser/picture_in_picture/picture_in_picture_window_manager.hchrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc
Audit Directions
- Synchronous destruction from observers/callbacksSearch occlusion, visibility, focus, and widget-show callbacks for calls that synchronously destroy owning objects (windows,
TabFeatures, controllers) and route them through a posted-task variant instead. - Dual sync/async teardown APIsWhere a manager exposes both a synchronous destroy and an “…Soon”/deferred variant, audit every caller to confirm the synchronous form is never invoked from within a stack that is still using the destroyed objects.
- Re-entrancy during dialog/PiP setupReview PiP and dialog interaction points (spoofing-prevention forced closes) for re-entrancy where showing or setting up one UI surface triggers teardown of another still referenced by the current frame.
Patch
From afb9b3f8eaf646f34743aa0f474988fc6970ff3f Mon Sep 17 00:00:00 2001 From: Alison Gale <[email protected]> Date: Thu, 17 Sep 2026 17:08:45 -0700 Subject: [PATCH] [Dialog] Fix crash with pip and dialog manager Synchronously tearing down the pip window destroyed the tab features associated with it which caused an asan crash when trying to show a dialog on that window. Switch to exposing the async version to prevent this. Bug: 560251736,560238698,560503095,562174487,467460499 Change-Id: Ie34406e317267d819472d90aec96914b4c5a38f5 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8408228 Commit-Queue: Alison Gale <[email protected]> Reviewed-by: Benjamin Keen <[email protected]> Cr-Commit-Position: refs/heads/main@{#1700295} --- diff --git a/chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc b/chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc index f42ec3f..d62f385 100644 --- a/chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc +++ b/chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc @@ -9,6 +9,7 @@ #include "base/files/file_path.h" #include "base/strings/strcat.h" #include "base/strings/string_number_conversions.h" +#include "base/test/run_until.h" #include "base/time/time.h" #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" @@ -78,17 +79,8 @@ } bool DocumentPictureInPictureMixinTestBase::AwaitPipWindowClosedSuccessfully() { - auto* render_widget_host_view = GetRenderWidgetHostView(); - if (!render_widget_host_view) { - return true; - } - ui_test_utils::CheckWaiter( - base::BindRepeating(&content::RenderWidgetHostView::IsShowing, - base::Unretained(render_widget_host_view)), - false, base::Seconds(30)) - .Wait(); - - return (GetRenderWidgetHostView() == nullptr); + return base::test::RunUntil( + [this]() { return GetRenderWidgetHostView() == nullptr; }); } content::RenderWidgetHostView* diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h index 444c5b4..063a8f7 100644 --- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h +++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h @@ -117,8 +117,22 @@ // Closes any existing picture-in-picture windows (video or document pip). // Returns true if a picture-in-picture window was closed, and false if there // were no picture-in-picture windows to close. + // + // NOTE: This synchronously closes the window and destroys associated objects + // (such as TabFeatures). Calling this synchronously from callbacks or + // observers (e.g. during widget show or visibility changes) can cause + // re-entrancy issues and use-after-free crashes. Use + // `ExitPictureInPictureSoon()` unless you know that synchronous closure will + // not be a problem. bool ExitPictureInPicture(); + // Exits picture in picture soon, but not before this call returns. If + // picture in picture closes between now and then, that's okay. Intended as a + // helper for callbacks and observers to avoid re-entrant calls during pip + // set-up or in observer notifications (e.g. when an occluded dialog is being + // shown). + static void ExitPictureInPictureSoon(); + // Called to notify that the initiator web contents should be focused. void FocusInitiator(); @@ -352,10 +366,6 @@ // Called when the document PiP parent web contents is being destroyed. void DocumentWebContentsDestroyed(); - // Exits picture in picture soon, but not before this call returns. If - // picture in picture closes between now and then, that's okay. Intended as a - // helper class for callbacks, to avoid re-entrant calls during pip set-up. - static void ExitPictureInPictureSoon(); #if !BUILDFLAG(IS_ANDROID) // Creates the `occlusion_tracker_` if it does not already exist and should diff --git a/chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc b/chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc index 1601d983..8cfd2e6 100644 --- a/chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc +++ b/chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc @@ -103,10 +103,8 @@ } void CreateDesktopShortcutDelegate::OnOcclusionStateChanged(bool occluded) { - // If a picture-in-picture window is occluding the dialog, force it to close - // to prevent spoofing. if (occluded) { - PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture(); + PictureInPictureWindowManager::ExitPictureInPictureSoon(); } } diff --git a/chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc b/chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc index f46ab3f7..2d563af 100644 --- a/chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc +++ b/chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc @@ -55,7 +55,7 @@ // If a picture-in-picture window is occluding the dialog, force it to close // to prevent spoofing. if (occluded) { - PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture(); + PictureInPictureWindowManager::ExitPictureInPictureSoon(); } } diff --git a/chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc b/chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc index 21ad4a98..0e2efda 100644 --- a/chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc +++ b/chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc @@ -202,7 +202,7 @@ // If a picture-in-picture window is occluding the dialog, force it to close // to prevent spoofing. if (occluded) { - PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture(); + PictureInPictureWindowManager::ExitPictureInPictureSoon(); } } diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc index d1c3c50..7a9bb2d 100644 --- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc +++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc @@ -176,6 +176,41 @@ PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture()); } +TEST_F(PictureInPictureWindowManagerTest, + ExitPictureInPictureSoonClosesWindowAsynchronously) { + PictureInPictureWindowManager* picture_in_picture_window_manager = + PictureInPictureWindowManager::GetInstance(); + MockPictureInPictureWindowController controller; + picture_in_picture_window_manager->EnterPictureInPictureWithController( + &controller); + EXPECT_TRUE(picture_in_picture_window_manager->IsInPictureInPicture()); + + // Window should not be closed synchronously. + EXPECT_CALL(controller, Close(/*should_pause_video=*/false)).Times(0); + PictureInPictureWindowManager::ExitPictureInPictureSoon(); + EXPECT_TRUE(picture_in_picture_window_manager->IsInPictureInPicture()); + + // Window should close asynchronously once the posted task runs. + base::RunLoop run_loop; + EXPECT_CALL(controller, Close(/*should_pause_video=*/false)) + .WillOnce([&run_loop]() { run_loop.Quit(); }); + run_loop.Run(); + EXPECT_FALSE(picture_in_picture_window_manager->IsInPictureInPicture()); +} + +TEST_F(PictureInPictureWindowManagerTest, + ExitPictureInPictureSoonDoesNotCrashWhenThereIsNoWindow) { + EXPECT_FALSE( + PictureInPictureWindowManager::GetInstance()->IsInPictureInPicture()); + PictureInPictureWindowManager::ExitPictureInPictureSoon(); + base::RunLoop run_loop; + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, run_loop.QuitClosure()); + run_loop.Run(); + EXPECT_FALSE( + PictureInPictureWindowManager::GetInstance()->IsInPictureInPicture()); +} + TEST_F(PictureInPictureWindowManagerTest, IsInPictureInPicture) { PictureInPictureWindowManager* picture_in_picture_window_manager = PictureInPictureWindowManager::GetInstance();
Regression Test / PoC
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
index d1c3c50..7a9bb2d 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager_unittest.cc
@@ -176,6 +176,41 @@
PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture());
}
+TEST_F(PictureInPictureWindowManagerTest,
+ ExitPictureInPictureSoonClosesWindowAsynchronously) {
+ PictureInPictureWindowManager* picture_in_picture_window_manager =
+ PictureInPictureWindowManager::GetInstance();
+ MockPictureInPictureWindowController controller;
+ picture_in_picture_window_manager->EnterPictureInPictureWithController(
+ &controller);
+ EXPECT_TRUE(picture_in_picture_window_manager->IsInPictureInPicture());
+
+ // Window should not be closed synchronously.
+ EXPECT_CALL(controller, Close(/*should_pause_video=*/false)).Times(0);
+ PictureInPictureWindowManager::ExitPictureInPictureSoon();
+ EXPECT_TRUE(picture_in_picture_window_manager->IsInPictureInPicture());
+
+ // Window should close asynchronously once the posted task runs.
+ base::RunLoop run_loop;
+ EXPECT_CALL(controller, Close(/*should_pause_video=*/false))
+ .WillOnce([&run_loop]() { run_loop.Quit(); });
+ run_loop.Run();
+ EXPECT_FALSE(picture_in_picture_window_manager->IsInPictureInPicture());
+}
+
+TEST_F(PictureInPictureWindowManagerTest,
+ ExitPictureInPictureSoonDoesNotCrashWhenThereIsNoWindow) {
+ EXPECT_FALSE(
+ PictureInPictureWindowManager::GetInstance()->IsInPictureInPicture());
+ PictureInPictureWindowManager::ExitPictureInPictureSoon();
+ base::RunLoop run_loop;
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, run_loop.QuitClosure());
+ run_loop.Run();
+ EXPECT_FALSE(
+ PictureInPictureWindowManager::GetInstance()->IsInPictureInPicture());
+}
+
TEST_F(PictureInPictureWindowManagerTest, IsInPictureInPicture) {
PictureInPictureWindowManager* picture_in_picture_window_manager =
PictureInPictureWindowManager::GetInstance();