Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Digital Credentials API
DescriptionUse after free in Digital Credentials API
ComponentDigital Credentials API
Bug ClassUAF
Tracker488617440
Fix commit952d06969915 (chromium/src) +37/-32
CISA KEVNot listed
Creditedsean wong
Disclosed2026-03-18

Changed Functions

FunctionChangeNotes
if
content/browser/digital_credentials/cross_device_transaction_impl.cc
modified
TEST_P
content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
modified

Files Changed

  • content/browser/digital_credentials/cross_device_transaction_impl.cc
  • content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
From 952d06969915c8e44f9ab6008be6f14f6338901e Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <[email protected]>
Date: Thu, 12 Mar 2026 02:51:15 -0700
Subject: [PATCH] [DC] Fix crash when Bluetooth is disabled during a DC flow

When performing a Digital Credential Request, Chrome crashes if the
Bluetooth adapter is powered off while a transaction is in progress.
This happens because TransactionImpl (a BluetoothAdapter::Observer)
synchronously executes its completion callback upon receiving the
AdapterPoweredChanged(false) notification.

The completion callback synchronously destroys the
DigitalIdentityProvider and the TransactionImpl itself. Destroying an
observer while the BluetoothAdapter is still iterating over its
ObserverList causes a use-after-free or re-entrancy crash.

This CL fixes the issue by posting a task to execute the completion
callback asynchronously, which is the standard pattern for preventing
synchronous destruction in observer notifications.

This is a follow-up to https://crrev.com/c/7132078 which fixed multiple
error cases when the bluetooth permission is denied ...etc but
overlooked one scenario when the Bluetooth adapter gets turned off
during a transaction.

Fixed: 488617440
Change-Id: If78d9d3b5cb8d27b4bcaad48ba8850da2ebae0b5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7656824
Commit-Queue: Mohamed Amir Yosef <[email protected]>
Reviewed-by: Martin Kreichgauer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1598301}
---

diff --git a/content/browser/digital_credentials/cross_device_transaction_impl.cc b/content/browser/digital_credentials/cross_device_transaction_impl.cc
index 7d9e81aa..83faad0 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl.cc
@@ -127,7 +127,9 @@
   }
   if (!powered && !waiting_for_power_) {
     FIDO_LOG(EVENT) << "Lost BLE power during digital identity transaction.";
-    std::move(callback_).Run(base::unexpected(SystemError::kLostPower));
+    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+        FROM_HERE, base::BindOnce(std::move(callback_),
+                                  base::unexpected(SystemError::kLostPower)));
     return;
   }
   if (powered && waiting_for_power_) {
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 d09bb92..016e425 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
@@ -73,10 +73,24 @@
   static base::Value request() {
     base::DictValue request_value;
     request_value.Set("foo", "bar");
-    return base::Value(std::move(request));
+    return base::Value(std::move(request_value));
   }
   RequestInfo::RequestType request_type() { return GetParam(); }
 
+  void CreateTransaction() {
+    transaction_ = Transaction::New(
+        RequestInfo(request_type(), origin(), request()), qr_generator_key(),
+        network_context_factory(), event_callback_.GetRepeatingCallback(),
+        base::BindOnce(
+            &DigitalIdentityCrossDeviceTransactionTest::OnTransactionDone,
+            base::Unretained(this)));
+  }
+
+  void OnTransactionDone(base::expected<Response, Error> result) {
+    transaction_.reset();
+    callback_.SetValue(std::move(result));
+  }
+
   static std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key() {
     std::array<uint8_t, device::cablev2::kQRKeySize> key = {0};
     return key;
@@ -94,23 +108,20 @@
   base::test::TestFuture<base::expected<Response, Error>> callback_;
   base::test::TestFuture<Event> event_callback_;
   base::test::TaskEnvironment task_environment_;
+  std::unique_ptr<Transaction> transaction_;
 };
 
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NoBle) {
   bluetooth_values_for_testing_->SetLESupported(false);
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
 }
 
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NoAdapter) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(false));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   // Callback should not have been called synchronously.
   EXPECT_FALSE(callback_.IsReady());
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
@@ -122,9 +133,7 @@
       .WillRepeatedly(
           Return(device::BluetoothAdapter::PermissionStatus::kDenied));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kPermissionDenied));
 }
 
@@ -135,14 +144,11 @@
           Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(false));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kBluetoothNotPowered));
 
-  reinterpret_cast<TransactionImpl*>(transaction.get())
+  reinterpret_cast<TransactionImpl*>(transaction_.get())
       ->AdapterPoweredChanged(nullptr, /*powered=*/true);
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kReady));
@@ -165,10 +171,7 @@
               device::BluetoothAdapter::RequestSystemPermissionCallback
                   callback) { permission_callback = std::move(callback); });
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kNeedPermission));
 
@@ -181,7 +184,7 @@
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NeedPermissionThenGranted) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(true));
   EXPECT_CALL(*mock_adapter_, GetOsPermissionStatus)
-      .WillOnce(
+      .WillRepeatedly(
           Return(device::BluetoothAdapter::PermissionStatus::kUndetermined));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(true));
 
@@ -192,10 +195,7 @@
               device::BluetoothAdapter::RequestSystemPermissionCallback
                   callback) { permission_callback = std::move(callback); });
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kNeedPermission));
 
@@ -209,20 +209,23 @@
        BleTurnedOffDuringTransaction) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(true));
   EXPECT_CALL(*mock_adapter_, GetOsPermissionStatus)
-      .WillOnce(Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
+      .WillRepeatedly(
+          Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(true));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kReady));
 
-  reinterpret_cast<TransactionImpl*>(transaction.get())
-      ->AdapterPoweredChanged(nullptr, /*powered=*/false);
+  reinterpret_cast<TransactionImpl*>(transaction_.get())
+      ->AdapterPoweredChanged(mock_adapter_.get(), /*powered=*/false);
+
+  // The callback should not have been run yet because it was posted to the task
+  // runner. This verifies the fix for the synchronous destruction crash.
+  EXPECT_FALSE(callback_.IsReady());
 
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kLostPower));
+  EXPECT_EQ(transaction_, nullptr);
 }
 
 INSTANTIATE_TEST_SUITE_P(,
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 d09bb92..016e425 100644
--- a/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
+++ b/content/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
@@ -73,10 +73,24 @@
   static base::Value request() {
     base::DictValue request_value;
     request_value.Set("foo", "bar");
-    return base::Value(std::move(request));
+    return base::Value(std::move(request_value));
   }
   RequestInfo::RequestType request_type() { return GetParam(); }
 
+  void CreateTransaction() {
+    transaction_ = Transaction::New(
+        RequestInfo(request_type(), origin(), request()), qr_generator_key(),
+        network_context_factory(), event_callback_.GetRepeatingCallback(),
+        base::BindOnce(
+            &DigitalIdentityCrossDeviceTransactionTest::OnTransactionDone,
+            base::Unretained(this)));
+  }
+
+  void OnTransactionDone(base::expected<Response, Error> result) {
+    transaction_.reset();
+    callback_.SetValue(std::move(result));
+  }
+
   static std::array<uint8_t, device::cablev2::kQRKeySize> qr_generator_key() {
     std::array<uint8_t, device::cablev2::kQRKeySize> key = {0};
     return key;
@@ -94,23 +108,20 @@
   base::test::TestFuture<base::expected<Response, Error>> callback_;
   base::test::TestFuture<Event> event_callback_;
   base::test::TaskEnvironment task_environment_;
+  std::unique_ptr<Transaction> transaction_;
 };
 
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NoBle) {
   bluetooth_values_for_testing_->SetLESupported(false);
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
 }
 
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NoAdapter) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(false));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   // Callback should not have been called synchronously.
   EXPECT_FALSE(callback_.IsReady());
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kNoBleSupport));
@@ -122,9 +133,7 @@
       .WillRepeatedly(
           Return(device::BluetoothAdapter::PermissionStatus::kDenied));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), base::DoNothing(), callback_.GetCallback());
+  CreateTransaction();
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kPermissionDenied));
 }
 
@@ -135,14 +144,11 @@
           Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(false));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kBluetoothNotPowered));
 
-  reinterpret_cast<TransactionImpl*>(transaction.get())
+  reinterpret_cast<TransactionImpl*>(transaction_.get())
       ->AdapterPoweredChanged(nullptr, /*powered=*/true);
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kReady));
@@ -165,10 +171,7 @@
               device::BluetoothAdapter::RequestSystemPermissionCallback
                   callback) { permission_callback = std::move(callback); });
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kNeedPermission));
 
@@ -181,7 +184,7 @@
 TEST_P(DigitalIdentityCrossDeviceTransactionTest, NeedPermissionThenGranted) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(true));
   EXPECT_CALL(*mock_adapter_, GetOsPermissionStatus)
-      .WillOnce(
+      .WillRepeatedly(
           Return(device::BluetoothAdapter::PermissionStatus::kUndetermined));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(true));
 
@@ -192,10 +195,7 @@
               device::BluetoothAdapter::RequestSystemPermissionCallback
                   callback) { permission_callback = std::move(callback); });
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kNeedPermission));
 
@@ -209,20 +209,23 @@
        BleTurnedOffDuringTransaction) {
   EXPECT_CALL(*mock_adapter_, IsPresent).WillRepeatedly(Return(true));
   EXPECT_CALL(*mock_adapter_, GetOsPermissionStatus)
-      .WillOnce(Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
+      .WillRepeatedly(
+          Return(device::BluetoothAdapter::PermissionStatus::kAllowed));
   EXPECT_CALL(*mock_adapter_, IsPowered).WillRepeatedly(Return(true));
 
-  std::unique_ptr<Transaction> transaction = Transaction::New(
-      RequestInfo(request_type(), origin(), request()), qr_generator_key(),
-      network_context_factory(), event_callback_.GetRepeatingCallback(),
-      callback_.GetCallback());
+  CreateTransaction();
 
   EXPECT_EQ(event_callback_.Take(), Event(SystemEvent::kReady));
 
-  reinterpret_cast<TransactionImpl*>(transaction.get())
-      ->AdapterPoweredChanged(nullptr, /*powered=*/false);
+  reinterpret_cast<TransactionImpl*>(transaction_.get())
+      ->AdapterPoweredChanged(mock_adapter_.get(), /*powered=*/false);
+
+  // The callback should not have been run yet because it was posted to the task
+  // runner. This verifies the fix for the synchronous destruction crash.
+  EXPECT_FALSE(callback_.IsReady());
 
   EXPECT_THAT(callback_.Take(), ContainsError(SystemError::kLostPower));
+  EXPECT_EQ(transaction_, nullptr);
 }
 
 INSTANTIATE_TEST_SUITE_P(,
Loading diff…

Original Bug Report

reported by [email protected]

Browser Process Heap-Use-After-Free in Digital Credentials API (Renderer → Browser Memory Corruption)


Report description

Browser Process Heap-Use-After-Free in Digital Credentials API (Renderer → Browser Memory Corruption)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

content/browser/webid/digital_credentials/cross_device_transaction_impl.cc


The problem

Please describe the technical details of the vulnerability

Summary

A Heap Use-After-Free vulnerability exists in the Browser Process within the Digital Credentials implementation.

A regression changed error callback execution from asynchronous to synchronous. This allows a TransactionImpl object to be destroyed while a background worker thread is still executing inside its context.

This results in:

  • Cross-thread lifetime violation
  • Use-after-free write in Browser Process memory
  • Deterministic heap corruption
  • Renderer → Browser security boundary violation

The crash is reproducible and confirmed in ASAN-instrumented Chromium builds.


Affected Component

content/browser/webid/digital_credentials/

Specifically:

cross_device_transaction_impl.cc

Tested Version:

141.0.7369.0 (Windows x64)

Vulnerability Type

  • Heap Use-After-Free
  • Cross-thread object lifetime violation
  • Browser Process memory corruption

Root Cause Analysis

In cross_device_transaction_impl.cc, error handling previously dispatched callbacks asynchronously via PostTask.

The regression changed this to synchronous execution:

// Vulnerable pattern
std::move(callback_).Run(base::unexpected(error));

Lifetime Sequence

  1. callback_ is owned by DigitalIdentityRequestImpl
  2. Synchronous .Run() immediately triggers parent cleanup
  3. Cleanup calls provider_.reset()
  4. provider_ owns TransactionImpl
  5. TransactionImpl is destroyed
  6. Background worker thread is still executing inside TransactionImpl
  7. Worker resumes execution on freed memory

This creates a classic cross-thread Use-After-Free condition in the Browser Process.


Technical Evidence

ASAN Crash

this is the current crash report "
KEY_VALUES_STRING: 1

    Key  : Analysis.CPU.mSec
    Value: 1484

    Key  : Analysis.Elapsed.mSec
    Value: 7383

    Key  : Analysis.IO.Other.Mb
    Value: 0

    Key  : Analysis.IO.Read.Mb
    Value: 1

    Key  : Analysis.IO.Write.Mb
    Value: 0

    Key  : Analysis.Init.CPU.mSec
    Value: 1218

    Key  : Analysis.Init.Elapsed.mSec
    Value: 6072

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 2001

    Key  : Analysis.Version.DbgEng
    Value: 10.0.29507.1001

    Key  : Analysis.Version.Description
    Value: 10.2511.5.1 amd64fre

    Key  : Analysis.Version.Ext
    Value: 1.2511.5.1

    Key  : Failure.Bucket
    Value: APPLICATION_FAULT_AVRF_517a7ed_chrome_elf.dll!Unknown

    Key  : Failure.Exception.Code
    Value: 0x517a7ed

    Key  : Failure.Exception.IP.Address
    Value: 0x7ffdd1f5de62

    Key  : Failure.Exception.IP.Module
    Value: chrome_elf

    Key  : Failure.Exception.IP.Offset
    Value: 0x1ade62

    Key  : Failure.Hash
    Value: {83d3484b-61a5-7688-5a33-ec9bb1799d38}

    Key  : Failure.ProblemClass.Primary
    Value: APPLICATION_FAULT

    Key  : Faulting.IP.Type
    Value: Paged

    Key  : Timeline.Process.Start.DeltaSec
    Value: 250

    Key  : WER.Process.Version
    Value: 141.0.7369.0


FILE_IN_CAB:  3479e8e4-5242-4370-96e4-baa39fd3a88f.dmp

NTGLOBALFLAG:  2000000

APPLICATION_VERIFIER_LOADED: 1

CONTEXT:  (.ecxr)
rax=000002193723fdbc rbx=00000095be5ff340 rcx=00000095be5fede0
rdx=0000000000000004 rsi=0000000000000000 rdi=00000012b7cbfdb8
rip=00007ffdd1f5de62 rsp=00000095be5feda0 rbp=00000095be5ff370
 r8=0000000000000096  r9=00000000000000a0 r10=00007ffe7a400000
r11=00007ffe7a4ed775 r12=000002067f580000 r13=00000095be5fedc0
r14=00000095be5fede0 r15=000002193723fdbc
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=0000  ds=0000  es=0000  fs=0053  gs=002b             efl=00000202
chrome_elf!GetHandleVerifier+0xdd672:
00007ffd`d1f5de62 4c89f1          mov     rcx,r14
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffdd1f5de62 (chrome_elf!GetHandleVerifier+0x00000000000dd672)
   ExceptionCode: 0517a7ed
  ExceptionFlags: 00000000
NumberParameters: 0

PROCESS_NAME:  chrome.exe

ERROR_CODE: (NTSTATUS) 0x517a7ed - <Unable to get error code text>

EXCEPTION_CODE_STR:  517a7ed

STACK_TEXT:  
00000095`be5feda0 00007ffd`57be5cbc     : 00000000`45e0360e 00000095`be5ff560 00000095`be5ff5b0 00000000`00000001 : chrome_elf!GetHandleVerifier+0xdd672
00000095`be5ff3c0 00007ffd`5797a7e0     : 00000095`be5ff640 00001214`7f954c80 00000000`00000000 00007ffd`57924382 : chrome!GetHandleVerifier+0x3655dc
00000095`be5ff600 00007ffd`5797d5c4     : 00000000`00000000 00000095`be5ff980 00000095`be5ffa10 00000000`00000000 : chrome!GetHandleVerifier+0xfa100
00000095`be5ff720 00007ffd`5797a5c4     : 00000095`be5ff900 00007ffd`446b34b2 00000095`be5ffb60 00000000`0000000b : chrome!GetHandleVerifier+0xfcee4
00000095`be5ffa70 00007ffd`5797a219     : 00000000`00000000 00000095`be5ffb60 00000095`be5ffbe0 00000206`7f580000 : chrome!GetHandleVerifier+0xf9ee4
00000095`be5ffae0 00007ffd`5796c160     : 00000000`00000000 00000000`00000000 00000000`00000246 00007ffd`57964e8f : chrome!GetHandleVerifier+0xf9b39
00000095`be5ffc40 00007ffd`578a55b4     : 00000000`00000000 00000000`00000000 00000000`00000002 00000000`00000000 : chrome!GetHandleVerifier+0xeba80
00000095`be5ffd20 00007ffd`ccdbb18d     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : chrome!GetHandleVerifier+0x24ed4
00000095`be5ffe30 00007ffe`7b53e8d7     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : clang_rt_asan_dynamic_x86_64!_asan_wrap_CreateThread+0x14d
00000095`be5ffe70 00007ffe`7d2ac40c     : 00000000`00000000 00000000`00000000 000004f0`fffffb30 000004d0`fffffb30 : KERNEL32!BaseThreadInitThunk+0x17
00000095`be5ffea0 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2c


STACK_COMMAND: ~3s; .ecxr ; kb

IP_IN_PAGED_CODE: 
chrome_elf!GetHandleVerifier+dd672
00007ffd`d1f5de62 4c89f1          mov     rcx,r14

SYMBOL_NAME:  chrome_elf+1ade62

MODULE_NAME: chrome_elf

IMAGE_NAME:  chrome_elf.dll

FAILURE_BUCKET_ID:  APPLICATION_FAULT_AVRF_517a7ed_chrome_elf.dll!Unknown

OSPLATFORM_TYPE:  x64

OSNAME:  Windows 10

IMAGE_VERSION:  141.0.7369.0

FAILURE_ID_HASH:  {83d3484b-61a5-7688-5a33-ec9bb1799d38}

Followup:     MachineOwner
---------"

Exception:

ExceptionCode: 0x517a7ed (ASAN security trap)
Process: chrome.exe (Browser Process)
Thread: Background worker thread

Register state at crash:

rax = 000002193723fdbc
r15 = 000002193723fdbc

Previously observed faulting instruction:

mov qword ptr [r13+r15], rax

This indicates:

  • Attacker-influenced value written
  • Write target derived from freed object memory
  • ASAN confirms the region was already freed and poisoned

Subsequent crash observed in:

chrome_elf!GetHandleVerifier
clang_rt_asan_dynamic_x86_64

This is consistent with allocator metadata corruption caused by a UAF write.


Security Boundary Impact

  • Triggered from renderer context
  • Corrupts memory in privileged Browser Process
  • Violates renderer sandbox boundary

This qualifies as:

Renderer → Browser Process memory corruption.

Browser-process UAF vulnerabilities are considered high severity because they affect privileged code execution context.


Reproduction Steps

  1. I used win32-release_x64-media_asan-win32-release_x64-1504065.
  2. Launch chromium from chromium folder with this flag chrome.exe --enable-features=FedCmDigitalIdentity
  3. Load the attached poc.html.
  4. Ensure Bluetooth is enabled.
  5. Trigger the Digital Credentials request.
  6. Immediately disable Bluetooth.
  7. Observe crash in Browser Process (not renderer).

The crash is reproducible and deterministic under ASAN.


Exploitability Discussion

The vulnerability provides:

  • Use-after-free write primitive
  • Occurs in privileged Browser Process
  • Reachable from web content

Suggested Fix

Restore asynchronous dispatch of the error callback:

base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
    FROM_HERE,
    base::BindOnce(std::move(callback_), base::unexpected(error))
);

This ensures:

  • TransactionImpl destructor runs after stack unwinds
  • Worker thread cannot resume inside freed object
  • Object lifetime semantics are preserved

Impact analysis

Impact

  • Memory Corruption: Yes
  • Use-After-Free: Yes
  • Browser Process: Yes
  • Renderer → Browser Boundary: Yes
  • Remote Triggerable: Yes

This represents high-severity memory corruption in a privileged process.


The cause

What version of Chrome have you found the security issue in?

chromium 141.0.7369.0, chrome Version 145.0.7632.117 (Official Build) (64-bit)

Yes, it is related to a crash.

Choose the type of vulnerability

Memory Corruption

How would you like to be publicly acknowledged for your report?

sean wong

View on issue tracker