CVE-2026-76040
Overview
Background
- `ConsentDialogCoordinator`
- browser-process UI code (
chrome/browser/ui/views/device_signals_consent/) that shows the Device Trust Connector (“DTC”) device-signals consent dialog. This is an enterprise feature used when policy asks the user to consent to sharing device signals. - `chrome::ShowBrowserModal`
- creates and shows a browser-modal dialog
views::Widgetfor aBrowserand returns that widget pointer. - Nested run loop
- according to the commit message, showing a modal dialog on macOS can spin a nested message loop inside the call. Arbitrary queued tasks, including teardown of the browser, profile or coordinator, can run before the call returns.
- `base::WeakPtr`
- a non-owning pointer, made by
weak_ptr_factory_.GetWeakPtr(), that becomes null when its owner is destroyed. It lets code check whetherthisstill exists after re-entrant work.
Root Cause Analysis
According to the commit subject, ConsentDialogCoordinator::Show() assigned the result of chrome::ShowBrowserModal(browser_, CreateDeviceSignalsConsentDialogModel()) directly to the member dialog_widget_. The call is evaluated first, and the store to this->dialog_widget_ happens only after it returns. On macOS, ShowBrowserModal can spin a nested run loop, and the owning Browser, Profile or the coordinator itself may be destroyed during that loop. When control came back, the assignment wrote into a freed ConsentDialogCoordinator, which is a use-after-free write in the browser process. The code assumed that this would outlive a call that can re-enter the message loop.
The fix takes a weak_this before the call, stores the returned views::Widget* in a local variable, and assigns dialog_widget_ only if weak_this is still valid. The write is skipped if the coordinator was destroyed during the show.
this could be dangling at the time of the write. Checking a WeakPtr taken before the call, and writing only if the object is still alive, removes the write-after-free.Attack Path
- Enterprise consent flow
A managed profile with the Device Trust Connector device-signals consent feature enabled by policy causes
ConsentDialogCoordinator::Show()to run. Ordinary web content cannot directly cause this. - Nested loop during show
On macOS,
chrome::ShowBrowserModalspins a nested run loop while it shows the dialog, according to the commit message. - Teardown inside the loop
During that loop, the browser window, profile or coordinator is torn down (for example, by window close, profile shutdown, or another queued task), which frees the
ConsentDialogCoordinator(inferred trigger; the commit names only the possibility). - Write to freed memory
When
ShowBrowserModalreturns, the pre-patch code writes the widget pointer into the freed object’sdialog_widget_field. If the freed memory has been reallocated, this could corrupt browser-process heap data (inferred).
Impact Assessment
ConsentDialogCoordinator in the privileged browser process. This could at worst corrupt heap memory (inferred), but the attacker has little control over the value written (a views::Widget*) and over when it happens. Reaching it needs macOS, an enterprise-managed profile where the DTC consent dialog is shown, and teardown racing with the nested run loop. The bug is not shown to be reachable from web content. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc |
modified |
Files Changed
chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc
Audit Directions
- Member writes after modal/show callsSearch for
x_ = chrome::ShowBrowserModal(...),ShowWebModal,constrained_window::Show*,views::Widget::Show,RunMenuAtand similar calls whose result is assigned straight to a member. On macOS these can spin nested loops that destroythis. - Re-entrancy through nested run loopsReview coordinators and controllers owned by
Browser,Profileor tab helpers for any use ofthisafter calls that may pump messages (modal sheets, file pickers,RunLoop().Run()), and require aWeakPtrorAutoReset-style guard. - Enterprise and consent UI lifecyclesAudit other policy-driven dialogs (device trust, enterprise sign-in, management disclosure) for coordinators whose lifetime is tied to a browser or profile that can shut down while the dialog is opening.
Patch
From e6bca4df6bc1e28a27e0dbf2921b3b5502a9310b Mon Sep 17 00:00:00 2001 From: Zonghan Xu <[email protected]> Date: Wed, 12 Aug 2026 05:20:23 -0700 Subject: [PATCH] [DTC Consent] [Fortify] Fix potential UAF in ConsentDialogCoordinator::Show() Showing a modal dialog can spin a nested run loop on macOS, during which the owning browser, profile, or coordinator may be torn down. Guard the post-show write with a weak pointer to avoid a use-after-free write if `this` is destroyed during ShowBrowserModal. Bug: 534862220 Change-Id: Ia3536e34be051089bfea7633415ed0939ef933ab Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8242507 Reviewed-by: Dominique Fauteux-Chapleau <[email protected]> Commit-Queue: Dominique Fauteux-Chapleau <[email protected]> Auto-Submit: Zonghan Xu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1677948} --- diff --git a/chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc b/chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc index 349b320..86ef5bba 100644 --- a/chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc +++ b/chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc @@ -152,8 +152,13 @@ return; } base::RecordAction(base::UserMetricsAction("DeviceSignalsConsent_Shown")); - dialog_widget_ = chrome::ShowBrowserModal( + + auto weak_this = weak_ptr_factory_.GetWeakPtr(); + views::Widget* widget = chrome::ShowBrowserModal( browser_, CreateDeviceSignalsConsentDialogModel()); + if (weak_this) { + dialog_widget_ = widget; + } } void ConsentDialogCoordinator::OnConsentDialogAccept() {
Original Bug Report
Potential UAF write in ConsentDialogCoordinator::Show due to macOS nested run 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 vulnerability exists in ConsentDialogCoordinator::Show on macOS. Calling chrome::ShowBrowserModal synchronously runs a nested run loop that can process incoming tasks or IPCs, potentially destroying the ConsentDialogCoordinator object before the function returns. This could allow a heap write-after-free when assigning the resulting widget pointer to a member of the freed coordinator.
Affected files:
chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.ccchrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.hchrome/browser/enterprise/connectors/device_trust/navigation_throttle.ccchrome/browser/enterprise/connectors/device_trust/navigation_throttle.h
Estimated timestamp from git blame: 2023-05-25
Description
A potential use-after-free (UAF) write vulnerability has been identified in ConsentDialogCoordinator::Show() on macOS. The root cause is a synchronous nested run loop spawned during the modal sheet presentation, which permits arbitrary task processing (including frame destruction) while ConsentDialogCoordinator::Show() is suspended on the call stack.
Root Cause Analysis
In chrome/browser/ui/views/device_signals_consent/consent_dialog_coordinator.cc, the Show() method is defined as follows:
void ConsentDialogCoordinator::Show() {
if (dialog_widget_ && !dialog_widget_->IsClosed()) {
...
return;
}
base::RecordAction(base::UserMetricsAction("DeviceSignalsConsent_Shown"));
dialog_widget_ = chrome::ShowBrowserModal(
browser_, CreateDeviceSignalsConsentDialogModel());
}
When chrome::ShowBrowserModal is called:
- It delegates to
BrowserModalHelper::Show, which creates aviews::BubbleDialogModelHostwithModalType::kWindowand callswidget->Show()(components/constrained_window/constrained_window_views.cc:215). - On macOS, this triggers the in-process Cocoa window bridge (
NativeWidgetNSWindowBridge::ShowAsModalSheet), which synchronously invokes-[NSWindow beginSheet:completionHandler:]on the UI thread’s stack (components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm:2232). beginSheet:enters a synchronous nested run loop to perform the sheet’s slide-down presentation animation.
During this nested run loop, the UI thread continues to pump events, allowing processing of arbitrary Mojo IPCs or timers. If a renderer-initiated navigation or window-close event occurs for the deferred frame:
- The pending
NavigationRequestis aborted and synchronously destroyed. - The owned
DeviceTrustNavigationThrottleis deleted (chrome/browser/enterprise/connectors/device_trust/navigation_throttle.cc:129). - The throttle deletes its owned
consent_requester_(std::unique_ptr<ConsentRequester>), invoking~ConsentDialogCoordinator(). - Since
dialog_widget_is stillnullptr(as the assignment has not yet executed), the destructor’s safety checks do not perform any widget cleanup, and theConsentDialogCoordinatorheap allocation is freed.
Once the AppKit nested loop yields and chrome::ShowBrowserModal returns the Widget* pointer, execution resumes at the call site. The code then attempts to write the pointer to this->dialog_widget_. Since this has already been deleted, this results in a write-after-free (and a potential reference count decrement inside raw_ptr::operator= if the slot has been reallocated to attacker-influenced data).
Potential Trigger Steps
Note: These are suggested potential steps, as our testing tools do not currently have the capability to execute code.
- Establish an enterprise-managed macOS environment where Device Trust consent collection is enabled (
ShouldCollectConsent()returnstrue). - Navigate a main frame to an attacker-controlled page, which spawns a popup (
w = window.open(...)). - Have the user perform a gesture in the popup to navigate to a monitored Device Trust URL, triggering the deferred
DeviceTrustNavigationThrottleand posting the task to show the consent dialog. - Within the popup origin, use a timed script (e.g.,
setTimeout) to close the popup or navigate it toabout:blankwithin the ~200ms slide-down animation window. - The browser processes the navigation cancel/abort IPC, destroying the deferred
NavigationRequest, the throttle, and the coordinator object. - The nested loop returns, triggering the UAF write to
this->dialog_widget_on the freed coordinator’s address.
Suggested Fix
To prevent this issue, we should use a base::WeakPtr to guard the assignment to dialog_widget_ after chrome::ShowBrowserModal returns, ensuring the coordinator object is still alive:
void ConsentDialogCoordinator::Show() {
if (dialog_widget_ && !dialog_widget_->IsClosed()) {
if (!dialog_widget_->IsVisible()) {
dialog_widget_->Show();
}
return;
}
base::RecordAction(base::UserMetricsAction("DeviceSignalsConsent_Shown"));
auto weak_this = weak_ptr_factory_.GetWeakPtr();
views::Widget* widget = chrome::ShowBrowserModal(
browser_, CreateDeviceSignalsConsentDialogModel());
if (weak_this) {
dialog_widget_ = widget;
}
}
Evaluated with Chrome root at commit: b5b015ea5f690560237d1f0cff1405844cd12b8d
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.