CVE-2026-12439
Overview
Background
- `DigitalIdentityMultiStepDialog`
- the Views-based controller object for the multi-step Digital Credentials UI that owns a
dialog_handle to the widget it creates. - `constrained_window::ShowWebModalDialogViews`
- a helper that creates and shows a web-modal dialog widget and returns the resulting
views::Widget*. - Nested message loop
- a re-entrant event loop that runs synchronously inside a call (here, spun while exiting HTML fullscreen), allowing other tasks to run and objects to be destroyed before the outer call returns.
- `base::WeakPtr` / `base::WeakPtrFactory`
- a Chromium smart-pointer pair where the factory hands out weak references that become null once the owning object is destroyed, so callers can test liveness before dereferencing.
Root Cause Analysis
In the dialog-creation path (the commit subject names DigitalIdentityMultiStepDialog::TryShow; the @@ -346 hunk header does not confirm the enclosing function name, so it is (inferred)), the pre-patch code wrote dialog_ = constrained_window::ShowWebModalDialogViews(new_dialog_delegate.release(), web_contents_.get())->GetWeakPtr(); — assigning the returned widget’s weak pointer directly into the this->dialog_ member. As the removed comment and commit message state, ShowWebModalDialogViews can spin a nested message loop (by exiting HTML fullscreen mode) that synchronously destroys this, so control returns to a statement that writes to dialog_ on a freed object, and the following SecurityDialogTracker::GetInstance()->AddSecurityDialog(dialog_.get()) also reads the freed member. The invariant violated is that this must remain live across a call that can re-enter the message loop before its members are touched again.
The fix captures weak_ptr = weak_ptr_factory_.GetWeakPtr() before the call, stores the widget in a local views::Widget* widget, and adds if (!weak_ptr) { return; } so member access resumes only when this is still alive; the new weak_ptr_factory_ member in the header backs the check. This works because the liveness test runs after the re-entrant call but strictly before any use of dialog_, converting the use-after-free into an early return.
this’s members (dialog_) immediately after a call that can synchronously destroy this via a nested message loop, with no liveness check in between. The fix interposes a base::WeakPtr liveness guard that early-returns when the object was freed during the call.Attack Path
- Reach the Digital Credentials dialog path
A page invokes the Digital Credentials flow, causing the browser to construct a
DigitalIdentityMultiStepDialogand enter the code that callsconstrained_window::ShowWebModalDialogViewsto show the modal widget. - Trigger a nested message loop that frees the dialog
The dialog show path exits HTML fullscreen mode, which spins a nested message loop; during that loop the
DigitalIdentityMultiStepDialogis synchronously destroyed (e.g. the flow is torn down), freeingthis.(inferred)— the precise event sequence that destroys the object during fullscreen exit is described by the commit message but not shown in the diff. - Use-after-free on return
When
ShowWebModalDialogViewsreturns, the pre-patch code executesdialog_ = ...->GetWeakPtr(), a write into the freed object, followed byAddSecurityDialog(dialog_.get()), a read of the freed member. - Downstream corruption / escalation
Controlling the timing and heap reuse of the freed
DigitalIdentityMultiStepDialogcould turn the write-after-free into a corruption primitive.(inferred)— the diff proves only the invalid access; it demonstrates no specific read/write primitive, memory-disclosure, or code-execution outcome.
Impact Assessment
DigitalIdentityMultiStepDialog, in the browser process where the Views UI runs. Preconditions are triggering the Digital Credentials flow and inducing the synchronous teardown while ShowWebModalDialogViews re-enters the loop. Any escalation to exploitable memory corruption or code execution is (inferred) and not demonstrated by the patch; the advisory classifies the issue only as a use-after-free in Digital Credentials and reports no in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc |
modified |
Files Changed
chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.ccchrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
Audit Directions
- Member access after re-entrant UI callsAudit any code that assigns to or reads
thismembers immediately afterShowWebModalDialogViews, fullscreen-mode transitions, or other calls that can spin a nested message loop, and require aWeakPtrliveness check in between. - Chained calls on values from blocking callsFlag patterns like
member_ = SomeShowCall(...)->Method()where the show call may synchronously destroy the object, since the trailing assignment and dereference run on a possibly-freedthis. - Dialog controllers lacking a WeakPtrFactoryReview other Views dialog/controller classes in
chrome/browser/ui/views/digital_credentials/and sibling directories that create modal dialogs but keep nobase::WeakPtrFactory, as they cannot self-check liveness across re-entrant show paths.
Patch
From 4059bc49eb1209be38510ad6fe4dba00fd8c1771 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
Date: Wed, 10 Jun 2026 04:52:09 -0700
Subject: [PATCH] [DC] Fix UAF in DigitalIdentityMultiStepDialog::TryShow
The dialog object could be synchronously destroyed during a nested
message loop spun by exiting HTML fullscreen mode in
`ShowWebModalDialogViews`. This causes a write-after-free when
`TryShow` returns and attempts to assign the returned widget to
`this->dialog_`.
This CL introduces a `base::WeakPtrFactory` to
`DigitalIdentityMultiStepDialog` and checks the liveness of `this`
using a `WeakPtr` before accessing member variables after the dialog
creation call returns.
Fixed: 519728275
Change-Id: Ifc3bf9eaa5bfa501d3e8d967df5c8252a6fdcad9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7914992
Commit-Queue: Rafał Godlewski <rgod@google.com>
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Reviewed-by: Rafał Godlewski <rgod@google.com>
Cr-Commit-Position: refs/heads/main@{#1644566}
---
diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc
index 97b80bb..52418e4 100644
--- a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc
+++ b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc
@@ -346,10 +346,18 @@
std::move(custom_body_field));
if (new_dialog_delegate) {
+ base::WeakPtr<DigitalIdentityMultiStepDialog> weak_ptr =
+ weak_ptr_factory_.GetWeakPtr();
// views::Widget takes ownership of `new_dialog_delegate`.
- dialog_ = constrained_window::ShowWebModalDialogViews(
- new_dialog_delegate.release(), web_contents_.get())
- ->GetWeakPtr();
+ views::Widget* widget = constrained_window::ShowWebModalDialogViews(
+ new_dialog_delegate.release(), web_contents_.get());
+ // `ShowWebModalDialogViews` can spin a nested message loop and
+ // synchronously destroy `this`. Check `weak_ptr` before accessing member
+ // variables.
+ if (!weak_ptr) {
+ return;
+ }
+ dialog_ = widget->GetWeakPtr();
extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
dialog_.get());
}
diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
index 2b355e6..d94980b 100644
--- a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
+++ b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
@@ -85,6 +85,8 @@
base::WeakPtr<content::WebContents> web_contents_;
base::WeakPtr<views::Widget> dialog_;
+
+ base::WeakPtrFactory<DigitalIdentityMultiStepDialog> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_VIEWS_DIGITAL_CREDENTIALS_DIGITAL_IDENTITY_MULTI_STEP_DIALOG_H_
Original Bug Report
Potential Browser-Process Use-After-Free in DigitalIdentityMultiStepDialog::TryShow
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 browser-process use-after-free vulnerability exists in DigitalIdentityMultiStepDialog::TryShow. When displaying a web-modal dialog, the hosting WebContents can be synchronously destroyed during a nested message loop spun by exiting fullscreen mode. This synchronously destroys the dialog object, leaving the method to perform unsafe write and read operations on the freed instance upon return.
Affected files:
chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.ccchrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.h
Estimated timestamp from git blame: 2024-07-23
Root Cause Analysis
A potential heap use-after-free (UAF) vulnerability exists in DigitalIdentityMultiStepDialog::TryShow due to a re-entrant destruction path during dialog creation. When displaying a web modal, the hosting WebContents can be synchronously destroyed, leading to the deletion of the dialog object. Upon return from ShowWebModalDialogViews, the method performs write and read operations on the freed this instance.
The vulnerable code is located in chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog.cc:
if (new_dialog_delegate) {
// views::Widget takes ownership of `new_dialog_delegate`.
dialog_ = constrained_window::ShowWebModalDialogViews(
new_dialog_delegate.release(), web_contents_.get())
->GetWeakPtr();
extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
dialog_.get());
}
There is no self-liveness guard after ShowWebModalDialogViews returns. The class has no base::WeakPtrFactory<DigitalIdentityMultiStepDialog>, so a self-liveness check is currently impossible without a code change. The written member dialog_ is a base::WeakPtr<views::Widget>, which is not protected by MiraclePtr.
Synchronous Destruction Path
ShowWebModalDialogViewscallsShowModalDialog, which delegates toWebContentsModalDialogManager::ShowDialogWithManager.- This invokes
BlockWebContentsInteraction(true), which callsBrowserWindowModalDialogDelegate::SetWebContentsBlocked(web_contents, true). - If the tab is currently in HTML fullscreen,
SetWebContentsBlockedcallsweb_contents->ExitFullscreen(true)to drop fullscreen. WebContentsImpl::ExitFullscreenModeinvokesdelegate_->ExitFullscreenModeForTab(this).- As documented in the
WebContentsImplsource code, this delegate call may spin a nested message loop (or wait for window server events) while adjusting the platform window state. - While this nested loop is spinning, queued tasks (such as standard tab closing or navigation commits) can run, which synchronously destroys the hosting
WebContentsand the underlyingRenderFrameHost. - The destruction of the
RenderFrameHostsynchronously deletes the associatedDocumentService(DigitalIdentityRequestImpl), which deletes theDigitalIdentityProviderDesktopand consequently theDigitalIdentityMultiStepDialog(this). - Control then returns to
TryShow, which attempts to write tothis->dialog_and read from it viadialog_.get(), resulting in a Use-After-Free.
Suggested / Potential Trigger Steps
Note: These are potential steps based on static code analysis; our tooling agent does not currently have the ability to run code or verify a live proof of concept.
- A web page in HTML fullscreen initiates a low-risk Digital Credentials API request via
navigator.credentials.getusing a gesture. - The request bypasses the security interstitial and invokes
DigitalIdentityMultiStepDialog::TryShowto display the QR code modal. TryShowcallsShowWebModalDialogViews, forcing the browser to exit fullscreen.- Exiting fullscreen spins a nested loop during which the tab is closed or navigated, synchronously deleting the
WebContentsand theDigitalIdentityMultiStepDialogobject. - When
ShowWebModalDialogViewsreturns, the method writes the return value tothis->dialog_on the freedthisallocation.
Suggested Fix
Add a base::WeakPtrFactory to the DigitalIdentityMultiStepDialog class. Capture a weak reference to this before calling ShowWebModalDialogViews, and use a self-liveness guard to return early if this has been deleted:
if (new_dialog_delegate) {
base::WeakPtr<DigitalIdentityMultiStepDialog> weak_this = weak_ptr_factory_.GetWeakPtr();
views::Widget* widget = constrained_window::ShowWebModalDialogViews(
new_dialog_delegate.release(), web_contents_.get());
if (!weak_this) {
return;
}
dialog_ = widget->GetWeakPtr();
extensions::SecurityDialogTracker::GetInstance()->AddSecurityDialog(
dialog_.get());
}
Evaluated with Chrome root at commit: 9ebf4302210513a012c901d87a2668b3aadf8cc1
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.