Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Enterprise
DescriptionUse after free in Enterprise
ComponentEnterprise
Bug ClassUAF
Tracker523715964
Fix commit0282e02b66da (chromium/src) +64/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
modified
WebContentsDestroyedDuringShowObserver
chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
modified
IN_PROC_BROWSER_TEST_P
chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
modified

Files Changed

  • chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
  • chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h
  • chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
From 0282e02b66da8427913a529146726fb21c186eb8 Mon Sep 17 00:00:00 2001
From: Alex Chen <[email protected]>
Date: Thu, 25 Jun 2026 10:18:46 -0700
Subject: [PATCH] Handle reentrant close of DesktopDataControlsDialog during Show

Showing a tab-modal dialog can spin a nested run loop on some platforms,
during which the dialog's WebContentsObserver overrides may fire and try
to close the dialog while ShowWebModalDialogViewsOwned() is still on the
stack. The returned widget was then written into a member of the deleted
dialog and dereferenced.

Hold the returned widget in a local until the dialog is known to still
be alive, and defer the WebContentsDestroyed() / PrimaryPageChanged()
close until the widget is owned by the dialog so destruction order is
correct.

Fixed: b:523715964
Change-Id: Ie3bb36ae8e5dc0eedeed323d5ad50e4160bb61a9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7988914
Reviewed-by: Dominique Fauteux-Chapleau <[email protected]>
Commit-Queue: Alex Chen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1652525}
---

diff --git a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
index d437733..9a3178c 100644
--- a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
+++ b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
@@ -263,9 +263,22 @@
     return;
   }
 
-  widget_ = constrained_window::ShowWebModalDialogViewsOwned(
+  // Showing a tab-modal dialog can run a nested loop on some platforms which
+  // may dispatch `WebContentsObserver` notifications for `web_contents()`, so
+  // hold the returned widget in a local until `this` is known to still be
+  // valid.
+  auto weak_this = weak_ptr_factory_.GetWeakPtr();
+  auto widget = constrained_window::ShowWebModalDialogViewsOwned(
       dialog_delegate_.get(), top_web_contents,
       views::Widget::InitParams::CLIENT_OWNS_WIDGET);
+  if (!weak_this) {
+    return;
+  }
+  widget_ = std::move(widget);
+  if (!web_contents()) {
+    CloseDialog(views::Widget::ClosedReason::kAcceptButtonClicked);
+    return;
+  }
   widget_->MakeCloseSynchronous(base::BindOnce(
       &DesktopDataControlsDialog::CloseDialog, base::Unretained(this)));
 }
@@ -302,6 +315,12 @@
   // was neither bypassed or accepted so it should close without calling
   // any callback.
   ClearCallbacks();
+  if (!widget_) {
+    // `Show()` is still creating the widget; it will close the dialog once the
+    // widget is owned by `this`.
+    Observe(nullptr);
+    return;
+  }
   CloseDialog(views::Widget::ClosedReason::kAcceptButtonClicked);
 }
 
@@ -312,6 +331,12 @@
   // that trigger on the new page, so callbacks must be cleared before closing
   // the dialog.
   ClearCallbacks();
+  if (!widget_) {
+    // `Show()` is still creating the widget; it will close the dialog once the
+    // widget is owned by `this`.
+    Observe(nullptr);
+    return;
+  }
   CloseDialog(views::Widget::ClosedReason::kAcceptButtonClicked);
 }
 
diff --git a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h
index 6d371e5a..4921bf9 100644
--- a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h
+++ b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h
@@ -9,6 +9,7 @@
 
 #include "base/functional/callback_forward.h"
 #include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
 #include "components/enterprise/data_controls/core/browser/data_controls_dialog.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_observer.h"
@@ -83,6 +84,8 @@
       scoped_ignore_input_events_;
 
   base::OnceClosure on_destructed_;
+
+  base::WeakPtrFactory<DesktopDataControlsDialog> weak_ptr_factory_{this};
 };
 
 }  // namespace data_controls
diff --git a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
index 120ff9a..ba81d5ba 100644
--- a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
+++ b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
@@ -113,6 +113,28 @@
       dialog_close_loops_;
 };
 
+// Observer that simulates the observed WebContents going away while the modal
+// dialog widget is still being created inside Show(). This mirrors what can
+// happen on platforms where showing a tab-modal dialog spins a nested run
+// loop.
+class WebContentsDestroyedDuringShowObserver
+    : public DesktopDataControlsDialog::TestObserver {
+ public:
+  void OnWidgetInitialized(DesktopDataControlsDialog* dialog,
+                           views::DialogDelegate* dialog_delegate) override {
+    dialog->WebContentsDestroyed();
+  }
+
+  void OnDestructed(DesktopDataControlsDialog* dialog) override {
+    ++destructed_count_;
+  }
+
+  size_t destructed_count() const { return destructed_count_; }
+
+ private:
+  size_t destructed_count_ = 0;
+};
+
 }  // namespace
 
 IN_PROC_BROWSER_TEST_P(DesktopDataControlsDialogUiTest, DefaultUi) {
@@ -140,6 +162,19 @@
   CloseDialogsAndWait();
 }
 
+IN_PROC_BROWSER_TEST_F(InProcessBrowserTest,
+                       WebContentsDestroyedWhileShowingWidget) {
+  WebContentsDestroyedDuringShowObserver observer;
+
+  DesktopDataControlsDialogFactory::GetInstance()->ShowDialogIfNeeded(
+      browser()->tab_strip_model()->GetActiveWebContents(),
+      DataControlsDialog::Type::kClipboardCopyBlock);
+
+  // The dialog should have been closed and destroyed synchronously once Show()
+  // detected that its WebContents went away during widget creation.
+  EXPECT_EQ(observer.destructed_count(), 1u);
+}
+
 IN_PROC_BROWSER_TEST_F(DesktopDataControlsDialogTest,
                        ShowDialogMultipleTimes_DifferentTypes) {
   // Distinct dialogs should be created for different types.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
index 120ff9a..ba81d5ba 100644
--- a/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
+++ b/chrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
@@ -113,6 +113,28 @@
       dialog_close_loops_;
 };
 
+// Observer that simulates the observed WebContents going away while the modal
+// dialog widget is still being created inside Show(). This mirrors what can
+// happen on platforms where showing a tab-modal dialog spins a nested run
+// loop.
+class WebContentsDestroyedDuringShowObserver
+    : public DesktopDataControlsDialog::TestObserver {
+ public:
+  void OnWidgetInitialized(DesktopDataControlsDialog* dialog,
+                           views::DialogDelegate* dialog_delegate) override {
+    dialog->WebContentsDestroyed();
+  }
+
+  void OnDestructed(DesktopDataControlsDialog* dialog) override {
+    ++destructed_count_;
+  }
+
+  size_t destructed_count() const { return destructed_count_; }
+
+ private:
+  size_t destructed_count_ = 0;
+};
+
 }  // namespace
 
 IN_PROC_BROWSER_TEST_P(DesktopDataControlsDialogUiTest, DefaultUi) {
@@ -140,6 +162,19 @@
   CloseDialogsAndWait();
 }
 
+IN_PROC_BROWSER_TEST_F(InProcessBrowserTest,
+                       WebContentsDestroyedWhileShowingWidget) {
+  WebContentsDestroyedDuringShowObserver observer;
+
+  DesktopDataControlsDialogFactory::GetInstance()->ShowDialogIfNeeded(
+      browser()->tab_strip_model()->GetActiveWebContents(),
+      DataControlsDialog::Type::kClipboardCopyBlock);
+
+  // The dialog should have been closed and destroyed synchronously once Show()
+  // detected that its WebContents went away during widget creation.
+  EXPECT_EQ(observer.destructed_count(), 1u);
+}
+
 IN_PROC_BROWSER_TEST_F(DesktopDataControlsDialogTest,
                        ShowDialogMultipleTimes_DifferentTypes) {
   // Distinct dialogs should be created for different types.
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in DesktopDataControlsDialog::Show on macOS due to Nested Loop

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential use-after-free (write and read) vulnerability exists in DesktopDataControlsDialog::Show on macOS. Exiting HTML fullscreen mode during dialog creation can spin a nested run loop, allowing queued tasks to run and synchronously destroy the dialog before the Show method resumes execution. This results in an unprotected UAF when writing and reading from freed member variables.

Affected files:

  • chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc
  • chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h

Estimated timestamp from git blame: 2025-05-27

Technical Analysis

In DesktopDataControlsDialog::Show (located in chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc), the browser attempts to display a modal dialog:

widget_ = constrained_window::ShowWebModalDialogViewsOwned(
    dialog_delegate_.get(), top_web_contents,
    views::Widget::InitParams::CLIENT_OWNS_WIDGET);
widget_->MakeCloseSynchronous(base::BindOnce(
    &DesktopDataControlsDialog::CloseDialog, base::Unretained(this)));
  1. Reentrant Nested Loop: ShowWebModalDialogViewsOwned triggers BrowserWindowModalDialogDelegate::SetWebContentsBlocked, which forces the tab to drop HTML fullscreen via web_contents->ExitFullscreen(true) to present the modal dialog safely.
  2. Mac-specific Loop Spinning: On macOS, exiting fullscreen mode calls down to AppKit (-[NSWindow toggleFullScreen:]), which spins a nested Cocoa run loop to run window transition animations.
  3. Synchronous Self-Deletion: While the nested run loop is spinning, queued tasks (such as a tab close or navigation) can run synchronously. If a tab close runs, it destroys the underlying WebContents, which notifies DesktopDataControlsDialog::WebContentsDestroyed(). This synchronously invokes CloseDialog() and executes delete this;.
  4. Use-After-Free: At the time of self-deletion, this->widget_ is still nullptr because the assignment of constrained_window::ShowWebModalDialogViewsOwned has not completed yet. Consequently, the views::Widget remains alive. Once the nested loop exits and the stack unwinds back to Show(), it move-assigns the returned std::unique_ptr<views::Widget> to this->widget_ (a UAF write into deallocated memory). It then reads the corrupted member to call widget_->MakeCloseSynchronous(...) (a UAF read) and binds the already deallocated this pointer to the close callback.

Because widget_ is a std::unique_ptr and this is accessed directly from the stack, MiraclePtr does not protect against this vulnerability.

Suggested Steps to Trigger the Potential Vulnerability

Note: Since our tooling agent does not have the ability to run code, these are potential steps derived from static analysis.

  1. Configure an active Enterprise/MDM profile with a DataControlsRules policy that yields a Warn or Block clipboard action on the attacker’s origin.
  2. On macOS, open a new window or tab via window.open().
  3. Obtain user activation to request HTML fullscreen via element.requestFullscreen().
  4. Queue a tab-close action using setTimeout(() => { window.close(); }, 0).
  5. Immediately invoke a clipboard read or paste action via navigator.clipboard.readText().
  6. This synchronously invokes DesktopDataControlsDialog::Show(). When the window transitions out of fullscreen, the queued tab-close runs, destroying the dialog. When Show() resumes, it encounters the UAF.

Suggested Fix

Introduce a base::WeakPtrFactory<DesktopDataControlsDialog> in DesktopDataControlsDialog to verify self-liveness before assigning and dereferencing the widget:

In chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.h:

private:
  ...
  base::WeakPtrFactory<DesktopDataControlsDialog> weak_ptr_factory_{this};

In chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc:

void DesktopDataControlsDialog::Show(base::OnceClosure on_destructed) {
  ...
  base::WeakPtr<DesktopDataControlsDialog> weak_this = weak_ptr_factory_.GetWeakPtr();
  auto widget = constrained_window::ShowWebModalDialogViewsOwned(
      dialog_delegate_.get(), top_web_contents,
      views::Widget::InitParams::CLIENT_OWNS_WIDGET);
  
  if (!weak_this) {
    return;
  }

  widget_ = std::move(widget);
  widget_->MakeCloseSynchronous(base::BindOnce(
      &DesktopDataControlsDialog::CloseDialog, base::Unretained(this)));
}

Evaluated with Chrome root at commit: b9a7cc5bde906eb50612d7bc368108eea0476cfb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

Raised in root component due to access or custom field issues on 1208119

View on issue tracker