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 FullScreen
DescriptionUse after free in FullScreen
ComponentFullScreen
Sub-componentPermissions
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.
Tracker560867085
Fix commitbc670d093a9b (chromium/src) +71/-34
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
Creditedsean geofrey
Reported2026-09-13 (per vendor advisory)
Fix landed2026-09-18 (public git)
Disclosed2026-09-29
Patch gap11 days — fix public in source before the advisory

Background

Device chooser dialog
the permission bubble Chrome shows for WebUSB/WebBluetooth/WebHID-style device pickers, which must first exit fullscreen so a page cannot spoof browser UI while owning the screen.
`ForSecurityDropFullscreen`
a content::WebContents method that exits fullscreen for security and returns a base::ScopedClosureRunner fullscreen blocker, but may synchronously spin a nested message loop or run event handlers.
`content::WeakDocumentPtr`
a weak handle to a RenderFrameHost that can be revalidated (AsRenderFrameHostIfValid) after a potentially destructive operation.
`GlobalBrowserCollection::FindBrowserWithTab`
a lookup that maps a WebContents to the BrowserWindowInterface currently hosting it as a tab.

Root Cause Analysis

In ShowDeviceChooserDialogForExtension and ShowDeviceChooserDialog, the code captured a raw contents pointer (and a browser) from the requesting RenderFrameHost, then called contents->ForSecurityDropFullscreen(...). That drop can spin a nested message loop or run event handlers synchronously, which may close the browser window, switch the active tab, or detach the frame — and in particular can free the very WebContents whose pointer was already captured. The pre-patch code only checked that the returned fullscreen_blocker had a value (i.e., that the drop did not immediately report the WebContents gone), then reused the stale contents/browser pointers to construct the ChooserBubbleUiViewDelegate, so a WebContents destroyed during the drop could still be handed to the dialog.

The fix hoists the lookups into DropFullscreenForChooserDialog, which validates the browser/active-tab state, performs the drop, then revalidates the frame through a WeakDocumentPtr and re-derives a known-live WebContents via WebContents::FromRenderFrameHost before re-confirming the browser — returning std::nullopt if any check fails. Because the pointer used afterward is re-derived from a frame that is confirmed active after the drop, a WebContents that the drop destroyed can never reach the dialog.

Key insight
The single mistake was reusing a WebContents (and browser) pointer captured before ForSecurityDropFullscreen, whose reentrancy can free that object; the fix revalidates a WeakDocumentPtr after the drop and re-derives the WebContents from the still-live frame instead of trusting the stale pointer.

Attack Path

  1. Trigger a chooser from a controllable frame A page (or extension surface) in a fullscreen tab invokes a device chooser, reaching ShowDeviceChooserDialog/ShowDeviceChooserDialogForExtension, which captures contents and calls ForSecurityDropFullscreen.
  2. Exploit reentrancy during the fullscreen drop The synchronous fullscreen exit spins a nested loop or runs handlers that close the window, switch tabs, or detach the frame, destroying the captured WebContents (inferred as the concrete trigger, since the patch describes but does not show the destroying handler).
  3. Use-after-free on the stale pointer The pre-patch code proceeds with the freed contents/browser to build and anchor the ChooserBubbleUiViewDelegate, dereferencing freed memory.
  4. Outcome The dialog is constructed against a destroyed WebContents, a use-after-free in the browser process (further escalation not shown by the patch).

Impact Assessment

An attacker who can cause a device chooser to be shown while a page owns fullscreen gains a use-after-free of a WebContents in the privileged browser process, exploitable when a fullscreen-drop-triggered event handler destroys the tab/window mid-call. The advisory classifies this as a use-after-free in FullScreen and reports no exploited-in-the-wild status; any escalation to code execution beyond the freed-object access is (inferred).

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
modified

Files Changed

  • chrome/browser/ui/views/permissions/chooser_bubble_ui.cc

Audit Directions

  • Pointers captured before a fullscreen drop or other reentrant call
    Audit every ForSecurityDropFullscreen caller (the CL is part of a 4-part audit: //content, //chrome mechanical callers, this device chooser, and the cast dialog) for reuse of WebContents/browser pointers captured before the drop.
  • Nested message loops and synchronous event dispatch
    Flag any browser-process code that spins a nested loop or runs page/event handlers and then continues to use objects looked up beforehand without revalidation.
  • Weak-handle revalidation pattern
    Confirm that code following a potentially destructive UI operation re-derives WebContents from a revalidated WeakDocumentPtr/RenderFrameHost (as ShareServiceImpl::RunShareOperation does) rather than trusting a “blocker has value” check as proof of liveness.
From bc670d093a9bebef6871ac71880087ccee619859 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <[email protected]>
Date: Fri, 18 Sep 2026 14:56:27 -0700
Subject: [PATCH] permissions: Harden the device chooser dialog against fullscreen drops (3/4)

Showing a device chooser dialog drops fullscreen first, so that the
dialog cannot be used to spoof UI while the page owns the screen. That
drop can spin a nested message loop or run event handlers synchronously,
which may close the browser window, switch the active tab, or detach the
frame that asked for the chooser.

The browser and tab lookups are hoisted into a
DropFullscreenForChooserDialog() helper that validates the state, drops
fullscreen, and then validates it again, returning the results in a
ChooserDialogHost. The WebContents is re-derived from the revalidated
frame rather than reusing the pointer captured before the drop, so the
dialog can never be handed a WebContents that the drop destroyed. This
mirrors what ShareServiceImpl::RunShareOperation() already does.

Part 3 of a four-part audit of ForSecurityDropFullscreen callers:
  1/4 //content
  2/4 //chrome mechanical caller hardening
  3/4 device chooser bubble (this CL)
  4/4 cast dialog

TAG=agy
CONV=295b160f-d3d8-4d61-8821-75fd7227c588

Bug: 560867085
Change-Id: I7b687bd96832d17223561e72e7f5b9b588c24859
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8426065
Reviewed-by: Elias Klim <[email protected]>
Commit-Queue: Jordan Bayles <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1701110}
---
diff --git a/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc b/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
index bbfbf513..31ff36d2 100644
--- a/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
+++ b/chrome/browser/ui/views/permissions/chooser_bubble_ui.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/ui/views/title_origin_label.h"
 #include "components/permissions/chooser_controller.h"
 #include "components/tabs/public/tab_interface.h"
+#include "content/public/browser/weak_document_ptr.h"
 #include "extensions/buildflags/buildflags.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/mojom/dialog_button.mojom.h"
@@ -226,42 +227,88 @@
 
 namespace {
 
+// The browser hosting a chooser dialog and the tab contents the dialog
+// belongs to, along with the blocker that keeps fullscreen from being
+// re-entered while the dialog is up. All three are known to be live at the
+// point the struct is returned.
+struct ChooserDialogHost {
+  raw_ptr<BrowserWindowInterface> browser;
+  raw_ptr<content::WebContents> contents;
+  base::ScopedClosureRunner fullscreen_blocker;
+};
+
+// Verifies that `contents` is the active tab of a live browser, drops
+// fullscreen for security, and then verifies it again.
+//
+// Dropping fullscreen can spin a nested message loop or run event handlers
+// synchronously, which may close the browser window, switch the active tab, or
+// destroy the frame in `weak_document`. Everything looked up beforehand is
+// therefore re-queried afterwards. Returns `std::nullopt` if the dialog should
+// no longer be shown.
+std::optional<ChooserDialogHost> DropFullscreenForChooserDialog(
+    content::WebContents* contents,
+    const content::WeakDocumentPtr& weak_document) {
+  auto* browser =
+      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(contents);
+  if (!browser || !browser->GetActiveTabInterface() ||
+      browser->GetActiveTabInterface()->GetContents() != contents) {
+    return std::nullopt;
+  }
+
+  std::optional<base::ScopedClosureRunner> fullscreen_blocker =
+      contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
+  content::RenderFrameHost* rfh = weak_document.AsRenderFrameHostIfValid();
+  if (!fullscreen_blocker.has_value() || !rfh || !rfh->IsActive()) {
+    return std::nullopt;
+  }
+
+  // `contents` was captured before the drop and may have been freed by it, so
+  // stop using it and re-derive a known-live WebContents from the frame that
+  // was just revalidated.
+  contents = content::WebContents::FromRenderFrameHost(rfh);
+  if (!contents) {
+    return std::nullopt;
+  }
+
+  browser =
+      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(contents);
+  if (!browser || !browser->GetActiveTabInterface() ||
+      browser->GetActiveTabInterface()->GetContents() != contents) {
+    return std::nullopt;
+  }
+
+  return ChooserDialogHost{browser, contents, std::move(*fullscreen_blocker)};
+}
+
 #if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
 base::OnceClosure ShowDeviceChooserDialogForExtension(
     content::RenderFrameHost* owner,
     const extensions::Extension* extension,
     std::unique_ptr<permissions::ChooserController> controller) {
+  content::WeakDocumentPtr weak_document = owner->GetWeakDocumentPtr();
   auto* contents = content::WebContents::FromRenderFrameHost(owner);
-  auto* browser =
-      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(contents);
-  if (!browser) {
+  std::optional<ChooserDialogHost> host =
+      DropFullscreenForChooserDialog(contents, weak_document);
+  if (!host) {
     return base::DoNothing();
   }
 
-  if (!browser->GetActiveTabInterface() ||
-      browser->GetActiveTabInterface()->GetContents() != contents) {
+  auto* browser_view = BrowserView::GetBrowserViewForBrowser(host->browser);
+  if (!browser_view) {
     return base::DoNothing();
   }
 
   // `GetExtensionsContainerViews` may return `nullptr`, for instance in
   // extension popup windows.
-  auto* extensions_toolbar = BrowserView::GetBrowserViewForBrowser(browser)
-                                 ->toolbar_button_provider()
-                                 ->GetExtensionsContainerViews();
+  auto* extensions_toolbar =
+      browser_view->toolbar_button_provider()->GetExtensionsContainerViews();
   if (!extensions_toolbar) {
     return base::DoNothing();
   }
 
-  std::optional<base::ScopedClosureRunner> fullscreen_blocker =
-      contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
-  if (!fullscreen_blocker.has_value()) {
-    // The WebContents ha been destroyed, bail out.
-    return base::DoNothing();
-  }
-
   auto bubble = std::make_unique<ChooserBubbleUiViewDelegate>(
-      browser, contents, std::move(controller),
-      std::move(fullscreen_blocker).value());
+      host->browser, host->contents, std::move(controller),
+      std::move(host->fullscreen_blocker));
   base::OnceClosure close_closure = bubble->MakeCloseClosure();
   extensions_toolbar->ShowWidgetForExtension(
       views::BubbleDialogDelegateView::CreateBubble(std::move(bubble)),
@@ -275,6 +322,7 @@
 base::OnceClosure ShowDeviceChooserDialog(
     content::RenderFrameHost* owner,
     std::unique_ptr<permissions::ChooserController> controller) {
+  content::WeakDocumentPtr weak_document = owner->GetWeakDocumentPtr();
   auto* contents = content::WebContents::FromRenderFrameHost(owner);
 
 #if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
@@ -300,27 +348,16 @@
   }
 #endif  // BUILDFLAG(ENABLE_EXTENSIONS_CORE)
 
-  auto* browser =
-      GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(contents);
-  if (!browser) {
+  std::optional<ChooserDialogHost> host =
+      DropFullscreenForChooserDialog(contents, weak_document);
+  if (!host) {
     return base::DoNothing();
   }
-
-  if (!browser->GetActiveTabInterface() ||
-      browser->GetActiveTabInterface()->GetContents() != contents) {
-    return base::DoNothing();
-  }
-
-  std::optional<base::ScopedClosureRunner> fullscreen_blocker =
-      contents->ForSecurityDropFullscreen(display::kInvalidDisplayId);
-  if (!fullscreen_blocker.has_value()) {
-    // The WebContents ha been destroyed, bail out.
-    return base::DoNothing();
-  }
+  BrowserWindowInterface* browser = host->browser;
 
   auto bubble = std::make_unique<ChooserBubbleUiViewDelegate>(
-      browser, contents, std::move(controller),
-      std::move(fullscreen_blocker).value());
+      browser, host->contents, std::move(controller),
+      std::move(host->fullscreen_blocker));
 
   bubble->UpdateAnchor(browser);
 
Loading diff…