CVE-2026-102304
Overview
Background
- Document Picture-in-Picture
- a floating always-on-top browser window whose backing
WebContentsand per-tab objects (TabFeatures) are owned indirectly throughPictureInPictureWindowManager. - `PictureInPictureWindowManager::ExitPictureInPicture()`
- a method that synchronously closes any open PiP window and tears down the objects associated with it.
- Occlusion observer
- a delegate callback (e.g.
OnOcclusionStateChanged) fired while a dialog is being shown, notifying it that another window (such as a PiP window) is occluding the dialog. - `PictureInPictureWindowManager::ExitPictureInPictureSoon()`
- a helper that posts the exit work to run after the current call returns, avoiding re-entrant teardown.
Root Cause Analysis
Several dialog delegates (CreateDesktopShortcutDelegate::OnOcclusionStateChanged and the occlusion handlers in web_app_modal_dialog_delegate.cc and web_app_update_review_dialog.cc) reacted to a PiP window occluding a dialog by calling PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture() synchronously from within the observer notification. Because ExitPictureInPicture() synchronously closes the window and destroys the associated TabFeatures and other objects, invoking it from inside the dialog-manager/occlusion notification path re-entered and destroyed state that the surrounding show-dialog flow was still using, producing a use-after-free (ASan crash) when the dialog was subsequently shown on that window.
The fix replaces the synchronous calls with the newly-exposed static ExitPictureInPictureSoon(), which posts the exit as a task that runs only after the current call stack unwinds, breaking the re-entrancy. The header change promotes ExitPictureInPictureSoon() from a private helper to a public static method and documents that ExitPictureInPicture() must not be called from callbacks or observers.
ExitPictureInPictureSoon() removes the re-entrant free.Attack Path
- Open a PiP window
The user (or page) opens a document/video picture-in-picture window managed by
PictureInPictureWindowManager. - Trigger a dialog occluded by PiP A dialog is shown (e.g. a web-app modal, update-review, or create-desktop-shortcut dialog) while the PiP window occludes it, firing the delegate’s occlusion observer.
- Re-entrant synchronous teardown
The observer calls
ExitPictureInPicture(), which synchronously closes the PiP window and destroys itsTabFeatures/associated objects while the show-dialog path is still executing. - Use-after-free
The dialog-show code then accesses the just-destroyed window/tab state, dereferencing freed memory (observed as an ASan crash). Any escalation beyond the crash is
(inferred).
Impact Assessment
(inferred). Preconditions include an active PiP window and a dialog surface that runs the occlusion observer; the advisory classifies this as a use-after-free and does not state exploited-in-the-wild status.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 destructive calls from observers/callbacksAudit every call site of
ExitPictureInPicture()and similar synchronous teardown APIs invoked from observer notifications, visibility/focus/occlusion callbacks, or widget-lifecycle events, and prefer the deferred variant. - Re-entrant object destruction during UI show flowsLook for dialog/bubble/window show paths that can destroy the objects they are mid-way through operating on, especially where closing one surface tears down a
WebContentsorTabFeatures. - Deferred-vs-synchronous API pairsWhere an API exposes both a synchronous and a “soon”/posted variant, verify callers on re-entrant paths use the posted one and that documentation flags the synchronous version as unsafe from callbacks.
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();