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 <yigu@chromium.org>
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 <cbiesinger@chromium.org>
Commit-Queue: Yi Gu <yigu@chromium.org>
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();
+ }
+}