CVE-2026-17886
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/enterprise/data_controls/desktop_data_controls_dialog.cc |
modified | |
WebContentsDestroyedDuringShowObserverchrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Pchrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc |
modified |
Files Changed
chrome/browser/enterprise/data_controls/desktop_data_controls_dialog.ccchrome/browser/enterprise/data_controls/desktop_data_controls_dialog.hchrome/browser/enterprise/data_controls/desktop_data_controls_dialog_browsertest.cc
Patch
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.
Regression Test / PoC
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.
Original Bug Report
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.ccchrome/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)));
- Reentrant Nested Loop:
ShowWebModalDialogViewsOwnedtriggersBrowserWindowModalDialogDelegate::SetWebContentsBlocked, which forces the tab to drop HTML fullscreen viaweb_contents->ExitFullscreen(true)to present the modal dialog safely. - 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. - 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 notifiesDesktopDataControlsDialog::WebContentsDestroyed(). This synchronously invokesCloseDialog()and executesdelete this;. - Use-After-Free: At the time of self-deletion,
this->widget_is stillnullptrbecause the assignment ofconstrained_window::ShowWebModalDialogViewsOwnedhas not completed yet. Consequently, theviews::Widgetremains alive. Once the nested loop exits and the stack unwinds back toShow(), it move-assigns the returnedstd::unique_ptr<views::Widget>tothis->widget_(a UAF write into deallocated memory). It then reads the corrupted member to callwidget_->MakeCloseSynchronous(...)(a UAF read) and binds the already deallocatedthispointer 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.
- Configure an active Enterprise/MDM profile with a
DataControlsRulespolicy that yields aWarnorBlockclipboard action on the attacker’s origin. - On macOS, open a new window or tab via
window.open(). - Obtain user activation to request HTML fullscreen via
element.requestFullscreen(). - Queue a tab-close action using
setTimeout(() => { window.close(); }, 0). - Immediately invoke a clipboard read or paste action via
navigator.clipboard.readText(). - This synchronously invokes
DesktopDataControlsDialog::Show(). When the window transitions out of fullscreen, the queued tab-close runs, destroying the dialog. WhenShow()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