Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Digital Credentials
DescriptionUse after free in Digital Credentials
ComponentDigital Credentials
Bug ClassUAF
Tracker458082926
Fix commit31b757cf79d9 (chromium/src) +40/-30
CISA KEVNot listed
CreditedChrome
Disclosed2025-12-02

Changed Functions

FunctionChangeNotes
switch
chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
modified
if
content/browser/digital_credentials/cross_device_transaction_impl.cc
modified

Files Changed

  • chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
  • content/browser/digital_credentials/cross_device_transaction_impl.cc
  • content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
From 31b757cf79d9c69f9b5451c8892b0159ddbb0d35 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <[email protected]>
Date: Thu, 06 Nov 2025 11:05:00 -0800
Subject: [PATCH] [DC] Fix a heap-use-after-free crash in the digital credentials cross-device flow.

The crash occurred because a DigitalIdentityProviderDesktop object could
be destroyed while one of its methods was still on the stack. This
happened when TransactionImpl's constructor synchronously invoked its
completion callback on an error path (e.g., no Bluetooth adapter). This
callback would cause DigitalIdentityRequestImpl to destroy the provider,
leading to the UAF.

The fix is to post the completion callback to the task runner in these
error paths, making the call asynchronous and breaking the re-entrant
destruction.

Additionally, this CL:
- Refactors DigitalIdentityProviderDesktop::OnFinished for robustness against similar re-entrancy issues.
- Adds a unit test to verify the async callback behavior in cross_device_transaction_impl_unittest.cc.

Fixed: 458082926
Test: Added content_unittests
Change-Id: I463ed5e5c056d020b33e0f25d666ac0b925f1636
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7127620
Commit-Queue: Mohamed Amir Yosef <[email protected]>
Reviewed-by: Adem Derinel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1541335}
---

diff --git a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
index d84f860b..1a5ed042 100644
--- a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
+++ b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
@@ -235,33 +235,34 @@
     return;
   }
 
+  RequestStatusForMetrics status;
   std::visit(
-      absl::Overload{
-          [this](SystemError error) {
-            EndRequestWithError(RequestStatusForMetrics::kErrorOther);
-          },
-          [this](ProtocolError error) {
-            EndRequestWithError(RequestStatusForMetrics::kErrorOther);
-          },
-          [this](RemoteError error) {
-            switch (error) {
-              case RemoteError::kNoCredential:
-                EndRequestWithError(
-                    RequestStatusForMetrics::kErrorNoCredential);
-                break;
-              case RemoteError::kUserCanceled:
-                EndRequestWithError(
-                    RequestStatusForMetrics::kErrorUserDeclined);
-                break;
-              case RemoteError::kDeviceAborted:
-                EndRequestWithError(RequestStatusForMetrics::kErrorAborted);
-                break;
-              case RemoteError::kOther:
-                EndRequestWithError(RequestStatusForMetrics::kErrorOther);
-                break;
-            }
-          }},
+      absl::Overload{[&status](SystemError error) {
+                       status = RequestStatusForMetrics::kErrorOther;
+                     },
+                     [&status](ProtocolError error) {
+                       status = RequestStatusForMetrics::kErrorOther;
+                     },
+                     [&status](RemoteError error) {
+                       switch (error) {
+                         case RemoteError::kNoCredential:
+                           status = RequestStatusForMetrics::kErrorNoCredential;
+                           break;
+                         case RemoteError::kUserCanceled:
+                           status = RequestStatusForMetrics::kErrorUserDeclined;
+                           break;
+                         case RemoteError::kDeviceAborted:
+                           status = RequestStatusForMetrics::kErrorAborted;
+                           break;
+                         case RemoteError::kOther:
+                           status = RequestStatusForMetrics::kErrorOther;
+                           break;
+                       }
+                     }},
       result.error());
+  EndRequestWithError(status);
+  // NOTE: `EndRequestWithError` may delete `this`, so it must be the last
+  // thing called in this method.
 }
 
 DigitalIdentityMultiStepDialog*
diff --git a/content/browser/digital_credentials/cross_device_transaction_impl.cc b/content/browser/digital_credentials/cross_device_transaction_impl.cc
index a09a12e..6298771c 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl.cc
@@ -156,7 +156,10 @@
 
   if (!adapter_->IsPresent()) {
     FIDO_LOG(EVENT) << "No BLE adapter.";
-    std::move(callback_).Run(base::unexpected(SystemError::kNoBleSupport));
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE,
+        base::BindOnce(std::move(callback_),
+                       base::unexpected(SystemError::kNoBleSupport)));
     return;
   }
 
@@ -171,8 +174,10 @@
       return;
     case device::BluetoothAdapter::PermissionStatus::kDenied:
       FIDO_LOG(EVENT) << "BLE permission denied.";
-      std::move(callback_).Run(
-          base::unexpected(SystemError::kPermissionDenied));
+      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+          FROM_HERE,
+          base::BindOnce(std::move(callback_),
+                         base::unexpected(SystemError::kPermissionDenied)));
       return;
     case device::BluetoothAdapter::PermissionStatus::kAllowed:
       break;
@@ -197,8 +202,10 @@
   if (status == device::BluetoothAdapter::PermissionStatus::kDenied) {
     FIDO_LOG(EVENT) << "BLE permission denied.";
     if (callback_) {
-      std::move(callback_).Run(
-          base::unexpected(SystemError::kPermissionDenied));
+      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+          FROM_HERE,
+          base::BindOnce(std::move(callback_),
+                         base::unexpected(SystemError::kPermissionDenied)));
     }
     return;
   }
diff --git a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
index 27dadeb..d9d1c7a 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
@@ -111,6 +111,8 @@
   std::unique_ptr<Transaction> transaction = Transaction::New(
       RequestInfo(request_type(), origin(), request()), qr_generator_key(),
       network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  // Callback should not have been called synchronously.
+  EXPECT_FALSE(callback_.IsReady());
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
index 27dadeb..d9d1c7a 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
@@ -111,6 +111,8 @@
   std::unique_ptr<Transaction> transaction = Transaction::New(
       RequestInfo(request_type(), origin(), request()), qr_generator_key(),
       network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  // Callback should not have been called synchronously.
+  EXPECT_FALSE(callback_.IsReady());
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
 }
Loading diff…

Original Bug Report

reported by [email protected]

mojo_js_in_process_fuzzer: Heap-use-after-free in DigitalIdentityProviderDesktop::Create

Detailed Report: https://clusterfuzz.com/testcase?key=5682786211921920

Fuzzing Engine: libFuzzer Fuzz Target: mojo_js_in_process_fuzzer Job Type: libfuzzer_chrome_asan Platform Id: linux

Crash Type: Heap-use-after-free READ 8 Crash Address: 0x78ac87048068 Crash State: DigitalIdentityProviderDesktop::Create content::DigitalIdentityRequestImpl::Create blink::mojom::DigitalIdentityRequestStubDispatch::AcceptWithResponder

Sanitizer: address (ASAN)

Recommended Security Severity: Critical

Crash Revision: https://clusterfuzz.com/revisions?job=libfuzzer_chrome_asan&revision=1533338

Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5682786211921920

Issue filed automatically.

See https://chromium.googlesource.com/chromium/src/+/master/testing/libfuzzer/reproducing.md for instructions on reproducing this bug locally.

************************* UNREPRODUCIBLE ************************* Note: This crash might not be reproducible with the provided testcase. That said, for the past 14 days, we’ve been seeing this crash frequently.

It may be possible to reproduce by trying the following options:

  • Run testcase multiple times for a longer duration.
  • Run fuzzing without testcase argument to hit the same crash signature.

If it still does not reproduce, try a speculative fix based on the crash stacktrace and verify if it works by looking at the crash statistics in the report. We will auto-close the bug if the crash is not seen for 14 days.


View on issue tracker