High watch CVSS 9.6 chrome UAF 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Passwords
DescriptionUse after free in Passwords
ComponentPasswords
Sub-componentDialog
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker560251736
Fix commitafb9b3f8eaf6 (chromium/src) +55/-20
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedXinyang Ge
Reported2026-09-11 (per vendor advisory)
Fix landed2026-09-18 (public git)
Disclosed2026-09-29
Patch gap11 days — fix public in source before the advisory

Background

Document Picture-in-Picture
a floating always-on-top browser window whose backing WebContents and per-tab objects (TabFeatures) are owned indirectly through PictureInPictureWindowManager.
`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.

Key insight
The core mistake was performing synchronous, destructive teardown of PiP-owned objects from inside an observer/callback that runs while those objects (and the dialog machinery) are still on the stack; deferring the teardown with ExitPictureInPictureSoon() removes the re-entrant free.

Attack Path

  1. Open a PiP window The user (or page) opens a document/video picture-in-picture window managed by PictureInPictureWindowManager.
  2. 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.
  3. Re-entrant synchronous teardown The observer calls ExitPictureInPicture(), which synchronously closes the PiP window and destroys its TabFeatures/associated objects while the show-dialog path is still executing.
  4. 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

An attacker able to induce the occluded-dialog condition against an open PiP window can cause a use-after-free in the browser process, giving at minimum a crash and potentially a controllable dangling-object condition (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

FunctionChangeNotes
if
chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc
modified
if
chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc
modified
if
chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc
modified
if
chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc
modified
TEST_F
chrome/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.cc
  • chrome/browser/ui/views/web_apps/web_app_modal_dialog_delegate.cc
  • chrome/browser/picture_in_picture/document_picture_in_picture_mixin_test_base.cc
  • chrome/browser/ui/views/shortcuts/create_desktop_shortcut_delegate.cc
  • chrome/browser/picture_in_picture/picture_in_picture_window_manager.h
  • chrome/browser/ui/views/web_apps/web_app_update_review_dialog.cc

Audit Directions

  • Synchronous destructive calls from observers/callbacks
    Audit 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 flows
    Look 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 WebContents or TabFeatures.
  • Deferred-vs-synchronous API pairs
    Where 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.
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…