CVE-2026-91729
Overview
Background
- `DigitalIdentityProviderDesktop`
- the browser-process object that drives the desktop UI for a Digital Credentials request, including the cross-device (hybrid/caBLE) flow that pairs with a phone.
- `OnCableEvent`
- a callback on
DigitalIdentityProviderDesktopinvoked by thedevice::cablev2transport as connection state changes (e.g.kBLEAdvertReceived,kPhoneConnected,kReady). - `ShowConnectingToPhoneDialog`
- a method that updates the multi-step dialog’s contents view to show connecting progress; mutating that view can synchronously run view observers and other callbacks that tear down the request.
- `base::WeakPtr`
- a non-owning pointer that becomes null once its referent is destroyed, letting a caller detect that
thiswas freed during a re-entrant call.
Root Cause Analysis
In DigitalIdentityProviderDesktop::OnCableEvent, the kPhoneConnected and kBLEAdvertReceived cases called ShowConnectingToPhoneDialog() and then, in the same invocation, dereferenced the member cable_connecting_dialog_timer_ (checking IsRunning() and calling Start()). The invariant the code assumed — that this remains alive across ShowConnectingToPhoneDialog() — does not hold: updating the dialog’s contents view can synchronously notify observers (views::ViewObserver::OnChildViewAdded) whose handlers can destroy the DigitalIdentityProviderDesktop instance before control returns. Once the object is freed, the subsequent access to cable_connecting_dialog_timer_ reads and writes freed memory, and binding weak_ptr_factory_.GetWeakPtr() also touches the destroyed factory — a use-after-free in the browser process.
The fix captures weak_this = weak_ptr_factory_.GetWeakPtr() at function entry, and after ShowConnectingToPhoneDialog() returns it checks if (!weak_this) return;, bailing out before any freed member is touched; the already-captured weak_this is then reused for the timer callback binding instead of calling GetWeakPtr() again on a possibly-dead factory.
ShowConnectingToPhoneDialog()) that can synchronously free this; the fix adds a WeakPtr liveness check (if (!weak_this) return;) between that call and the member access so the function exits cleanly when the object has been destroyed.Attack Path
- Initiate a cross-device Digital Credentials request
A page starts a Digital Credentials (
DigitalCredentials) request that triggers the desktop hybrid flow, causingDigitalIdentityProviderDesktopto show the QR dialog and begin listening fordevice::cablev2events. - Deliver a cable event
A phone advertisement or connection drives the transport to invoke
OnCableEventwithkBLEAdvertReceivedorkPhoneConnected, which callsShowConnectingToPhoneDialog(). - Destroy the provider during the dialog update
Mutating the dialog’s contents view synchronously runs a view observer/callback that tears down the request and destroys the
DigitalIdentityProviderDesktopbeforeShowConnectingToPhoneDialog()returns (the regression test forces this viaProviderDestroyerOnChildViewAddedObserveronOnChildViewAdded). - Use-after-free on return
Control returns into
OnCableEventand, without the guard, dereferences the freedcable_connecting_dialog_timer_member and the freedweak_ptr_factory_, reading and writing reclaimed heap memory. - Escalation
Controlling the contents of the reallocated memory could turn the freed-object access into a stronger primitive
(inferred); the patch itself demonstrates only the UAF, not a concrete read/write or code-execution sink.
Impact Assessment
DigitalIdentityProviderDesktop in the browser process, reachable when the provider is destroyed synchronously during ShowConnectingToPhoneDialog(). The preconditions are that a Digital Credentials request reaches the hybrid dialog stage and a cable event (kBLEAdvertReceived/kPhoneConnected) arrives while some path destroys the provider mid-update; the timing-dependent destruction makes this non-trivial to trigger. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.6. The advisory provides no exploited-in-the-wild or severity claim to report.Changed Functions
| Function | Change | Notes |
|---|---|---|
switchchrome/browser/digital_credentials/digital_identity_provider_desktop.cc |
modified | |
ifchrome/browser/digital_credentials/digital_identity_provider_desktop.cc |
modified | |
ProviderDestroyerOnChildViewAddedObserverchrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc |
modified | |
ifchrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc |
modified |
Files Changed
chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.ccchrome/browser/digital_credentials/digital_identity_provider_desktop.hchrome/browser/digital_credentials/digital_identity_provider_desktop.cc
Audit Directions
- Member access after re-entrant UI callsAudit any handler that calls a dialog/view-mutating method (like
ShowConnectingToPhoneDialog) and then touchesthismembers; view mutation can synchronously run observers that free the owner, so aWeakPtrcheck must sit between the call and the access. - `GetWeakPtr()` called lateFlag code that binds
weak_ptr_factory_.GetWeakPtr()(or any member) after a potentially-destructive call rather than capturing theWeakPtrup front; the factory itself is freed with the object. - Event/observer callbacks on owned providersReview other
device::cablev2andviews::ViewObservercallbacks in the Digital Credentials and WebAuthn hybrid flows for the same destroy-during-notification re-entrancy where a synchronous observer can outlive-invalidate the callee.
Patch
From 1c8d7dfe35d41046fa4b629ab1d1286f6e6d14f2 Mon Sep 17 00:00:00 2001 From: Yi Gu <[email protected]> Date: Tue, 08 Sep 2026 10:34:03 -0700 Subject: [PATCH] [DC] Fix UAF in in DigitalIdentityProviderDesktop::OnCableEvent Fixed: 557206809 Change-Id: If7528c8202aa2b65d83ea08ad55d84b9b7ec9448 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8370383 Reviewed-by: Christian Biesinger <[email protected]> Commit-Queue: Yi Gu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1693894} --- diff --git a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc index 867cd5e60..e66320ba 100644 --- a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc +++ b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc @@ -202,16 +202,21 @@ void DigitalIdentityProviderDesktop::OnCableEvent( device::cablev2::Event event) { + base::WeakPtr<DigitalIdentityProviderDesktop> weak_this = + weak_ptr_factory_.GetWeakPtr(); switch (event) { case device::cablev2::Event::kPhoneConnected: case device::cablev2::Event::kBLEAdvertReceived: ShowConnectingToPhoneDialog(); + if (!weak_this) { + return; + } if (!cable_connecting_dialog_timer_.IsRunning()) { cable_connecting_dialog_timer_.Start( FROM_HERE, base::Milliseconds(2500), base::BindOnce( &DigitalIdentityProviderDesktop::OnCableConnectingTimerComplete, - weak_ptr_factory_.GetWeakPtr())); + weak_this)); } break; case device::cablev2::Event::kReady: diff --git a/chrome/browser/digital_credentials/digital_identity_provider_desktop.h b/chrome/browser/digital_credentials/digital_identity_provider_desktop.h index 2be6166..a492236 100644 --- a/chrome/browser/digital_credentials/digital_identity_provider_desktop.h +++ b/chrome/browser/digital_credentials/digital_identity_provider_desktop.h @@ -5,6 +5,7 @@ #ifndef CHROME_BROWSER_DIGITAL_CREDENTIALS_DIGITAL_IDENTITY_PROVIDER_DESKTOP_H_ #define CHROME_BROWSER_DIGITAL_CREDENTIALS_DIGITAL_IDENTITY_PROVIDER_DESKTOP_H_ +#include "base/gtest_prod_util.h" #include "base/memory/weak_ptr.h" #include "base/timer/timer.h" #include "content/public/browser/cross_device_request_info.h" @@ -82,6 +83,9 @@ content::DigitalIdentityProvider::RequestStatusForMetrics); private: + FRIEND_TEST_ALL_PREFIXES(DigitalIdentityMultiStepDialogBrowserTest, + OnCableEventOwnerDestroyedDuringDialogUpdate); + // Called whenever some significant event occurs during the transaction. void OnEvent(const std::string& qr_url, RequestInfo::RequestType request_type, diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc index 20e78ad..47f500f 100644 --- a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc +++ b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc @@ -13,11 +13,14 @@ #include "content/public/browser/web_contents.h" #include "content/public/common/content_features.h" #include "content/public/test/browser_test.h" +#include "device/fido/cable/v2_constants.h" #include "ui/base/mojom/dialog_button.mojom.h" #include "ui/views/bubble/bubble_dialog_delegate_view.h" #include "ui/views/controls/label.h" +#include "ui/views/controls/scroll_view.h" #include "ui/views/test/widget_test.h" #include "ui/views/view.h" +#include "ui/views/view_observer.h" #include "ui/views/widget/widget.h" #include "ui/views/widget/widget_observer.h" @@ -280,6 +283,24 @@ base::OnceClosure destruction_callback_; }; +class ProviderDestroyerOnChildViewAddedObserver : public views::ViewObserver { + public: + explicit ProviderDestroyerOnChildViewAddedObserver( + base::OnceClosure destruction_callback) + : destruction_callback_(std::move(destruction_callback)) {} + + void OnChildViewAdded(views::View* observed_view, + views::View* child) override { + observed_view->RemoveObserver(this); + if (destruction_callback_) { + std::move(destruction_callback_).Run(); + } + } + + private: + base::OnceClosure destruction_callback_; +}; + } // namespace // Regression test for UAF in @@ -335,3 +356,51 @@ views::test::WidgetDestroyedWaiter(weak_widget.get()).Wait(); } } + +// Regression test for UAF in +// DigitalIdentityProviderDesktop::OnCableEvent when the provider is +// synchronously destroyed during ShowConnectingToPhoneDialog. +IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest, + OnCableEventOwnerDestroyedDuringDialogUpdate) { + auto provider = std::make_unique<TestDigitalIdentityProviderDesktop>(); + + base::RunLoop run_loop; + // Show the dialog via the real ShowQrCodeDialog flow. + provider->SetUpAndShowQrDialog(GetActiveWebContents(), + run_loop.QuitClosure()); + + views::Widget* widget = nullptr; + { + DigitalIdentityMultiStepDialog* dialog = provider->GetDialog(); + DigitalIdentityMultiStepDialog::TestApi dialog_test_api(dialog); + widget = dialog_test_api.GetWidget(); + } + ASSERT_TRUE(widget); + base::WeakPtr<views::Widget> weak_widget = widget->GetWeakPtr(); + + views::View* contents_view = static_cast<views::ScrollView*>( + widget->widget_delegate()->GetContentsView()) + ->contents(); + ASSERT_TRUE(contents_view); + + // Set up the observer to synchronously destroy the provider when + // ShowConnectingToPhoneDialog updates the contents view. + ProviderDestroyerOnChildViewAddedObserver observer(base::BindOnce( + [](std::unique_ptr<TestDigitalIdentityProviderDesktop>* provider) { + provider->reset(); + }, + base::Unretained(&provider))); + contents_view->AddObserver(&observer); + + // Trigger OnCableEvent. Without the fix, this causes a UAF as + // ShowConnectingToPhoneDialog destroys `provider`, and subsequent member + // access in OnCableEvent reads freed memory. + provider->OnCableEvent(device::cablev2::Event::kPhoneConnected); + + EXPECT_FALSE(provider); + + // Clean up if the widget is still alive. + if (weak_widget) { + views::test::WidgetDestroyedWaiter(weak_widget.get()).Wait(); + } +}
Regression Test / PoC
diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc
index 20e78ad..47f500f 100644
--- a/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc
+++ b/chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc
@@ -13,11 +13,14 @@
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
+#include "device/fido/cable/v2_constants.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/label.h"
+#include "ui/views/controls/scroll_view.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
+#include "ui/views/view_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
@@ -280,6 +283,24 @@
base::OnceClosure destruction_callback_;
};
+class ProviderDestroyerOnChildViewAddedObserver : public views::ViewObserver {
+ public:
+ explicit ProviderDestroyerOnChildViewAddedObserver(
+ base::OnceClosure destruction_callback)
+ : destruction_callback_(std::move(destruction_callback)) {}
+
+ void OnChildViewAdded(views::View* observed_view,
+ views::View* child) override {
+ observed_view->RemoveObserver(this);
+ if (destruction_callback_) {
+ std::move(destruction_callback_).Run();
+ }
+ }
+
+ private:
+ base::OnceClosure destruction_callback_;
+};
+
} // namespace
// Regression test for UAF in
@@ -335,3 +356,51 @@
views::test::WidgetDestroyedWaiter(weak_widget.get()).Wait();
}
}
+
+// Regression test for UAF in
+// DigitalIdentityProviderDesktop::OnCableEvent when the provider is
+// synchronously destroyed during ShowConnectingToPhoneDialog.
+IN_PROC_BROWSER_TEST_F(DigitalIdentityMultiStepDialogBrowserTest,
+ OnCableEventOwnerDestroyedDuringDialogUpdate) {
+ auto provider = std::make_unique<TestDigitalIdentityProviderDesktop>();
+
+ base::RunLoop run_loop;
+ // Show the dialog via the real ShowQrCodeDialog flow.
+ provider->SetUpAndShowQrDialog(GetActiveWebContents(),
+ run_loop.QuitClosure());
+
+ views::Widget* widget = nullptr;
+ {
+ DigitalIdentityMultiStepDialog* dialog = provider->GetDialog();
+ DigitalIdentityMultiStepDialog::TestApi dialog_test_api(dialog);
+ widget = dialog_test_api.GetWidget();
+ }
+ ASSERT_TRUE(widget);
+ base::WeakPtr<views::Widget> weak_widget = widget->GetWeakPtr();
+
+ views::View* contents_view = static_cast<views::ScrollView*>(
+ widget->widget_delegate()->GetContentsView())
+ ->contents();
+ ASSERT_TRUE(contents_view);
+
+ // Set up the observer to synchronously destroy the provider when
+ // ShowConnectingToPhoneDialog updates the contents view.
+ ProviderDestroyerOnChildViewAddedObserver observer(base::BindOnce(
+ [](std::unique_ptr<TestDigitalIdentityProviderDesktop>* provider) {
+ provider->reset();
+ },
+ base::Unretained(&provider)));
+ contents_view->AddObserver(&observer);
+
+ // Trigger OnCableEvent. Without the fix, this causes a UAF as
+ // ShowConnectingToPhoneDialog destroys `provider`, and subsequent member
+ // access in OnCableEvent reads freed memory.
+ provider->OnCableEvent(device::cablev2::Event::kPhoneConnected);
+
+ EXPECT_FALSE(provider);
+
+ // Clean up if the widget is still alive.
+ if (weak_widget) {
+ views::test::WidgetDestroyedWaiter(weak_widget.get()).Wait();
+ }
+}