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 <agale@chromium.org>
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 <agale@chromium.org>
Reviewed-by: Benjamin Keen <bkeen@google.com>
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();