Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in DigitalCredentials
DescriptionUse after free in DigitalCredentials
ComponentDigitalCredentials
Sub-componentDC
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker557206809
Fix commit1c8d7dfe35d4 (chromium/src) +79/-1
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
Creditedsean geofrey
Reported2026-09-04 (per vendor advisory)
Fix landed2026-09-08 (public git)
Disclosed2026-09-15
Patch gap7 days — fix public in source before the advisory

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 DigitalIdentityProviderDesktop invoked by the device::cablev2 transport 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 this was 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.

Key insight
The single mistake is continuing to touch instance members after a re-entrant UI call (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

  1. Initiate a cross-device Digital Credentials request A page starts a Digital Credentials (DigitalCredentials) request that triggers the desktop hybrid flow, causing DigitalIdentityProviderDesktop to show the QR dialog and begin listening for device::cablev2 events.
  2. Deliver a cable event A phone advertisement or connection drives the transport to invoke OnCableEvent with kBLEAdvertReceived or kPhoneConnected, which calls ShowConnectingToPhoneDialog().
  3. 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 DigitalIdentityProviderDesktop before ShowConnectingToPhoneDialog() returns (the regression test forces this via ProviderDestroyerOnChildViewAddedObserver on OnChildViewAdded).
  4. Use-after-free on return Control returns into OnCableEvent and, without the guard, dereferences the freed cable_connecting_dialog_timer_ member and the freed weak_ptr_factory_, reading and writing reclaimed heap memory.
  5. 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

An attacker who can drive the desktop Digital Credentials cross-device flow gains a use-after-free of a 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

FunctionChangeNotes
switch
chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
modified
if
chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
modified
ProviderDestroyerOnChildViewAddedObserver
chrome/browser/ui/views/digital_credentials/digital_identity_multi_step_dialog_browsertest.cc
modified
if
chrome/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.cc
  • chrome/browser/digital_credentials/digital_identity_provider_desktop.h
  • chrome/browser/digital_credentials/digital_identity_provider_desktop.cc

Audit Directions

  • Member access after re-entrant UI calls
    Audit any handler that calls a dialog/view-mutating method (like ShowConnectingToPhoneDialog) and then touches this members; view mutation can synchronously run observers that free the owner, so a WeakPtr check must sit between the call and the access.
  • `GetWeakPtr()` called late
    Flag code that binds weak_ptr_factory_.GetWeakPtr() (or any member) after a potentially-destructive call rather than capturing the WeakPtr up front; the factory itself is freed with the object.
  • Event/observer callbacks on owned providers
    Review other device::cablev2 and views::ViewObserver callbacks 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.
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();
+  }
+}
Loading diff…

Regression Test / PoC

shipped with the fix
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();
+  }
+}
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.