High watch CVSS 8.3 chrome UAF 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in PictureInPicture
DescriptionUse after free in PictureInPicture
ComponentPictureInPicture
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.
Tracker562174487
Fix commitafb9b3f8eaf6 (chromium/src) +55/-20
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedBlockian Creator of Kritt and Open-Kritt
Reported2026-09-15 (per vendor advisory)
Fix landed2026-09-18 (public git)
Disclosed2026-09-29
Patch gap11 days — fix public in source before the advisory

Background

`PictureInPictureWindowManager`
the singleton that owns the active picture-in-picture (PiP) window and can close it via ExitPictureInPicture() (synchronous) or ExitPictureInPictureSoon() (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.

Key insight
The single mistake was performing a synchronous, destructive PiP teardown from inside an occlusion observer that was still using the objects being destroyed; the fix defers the teardown by routing those callers through the asynchronous ExitPictureInPictureSoon().

Attack Path

  1. 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.)
  2. 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).
  3. Synchronous teardown re-enters Pre-patch, the callback calls ExitPictureInPicture(), which synchronously destroys the PiP window and its TabFeatures while the dialog-show / occlusion path is still on the stack.
  4. 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

An attacker able to trigger the occlusion-forced PiP close while a dialog is being shown can cause a use-after-free of the PiP window’s 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

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 destruction from observers/callbacks
    Search 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 APIs
    Where 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 setup
    Review 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.
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…