CVE-2026-4456
Overview
Background
- Digital Credentials cross-device transaction
- the browser-side
TransactionImplthat drives a cross-device Digital Credentials request over Bluetooth Low Energy and reports its outcome through a one-shot completion callback (callback_). - `BluetoothAdapter::Observer`
- an interface whose methods, such as
AdapterPoweredChanged(), are invoked byBluetoothAdapterwhile it iterates itsObserverList; per the commit message,TransactionImplis such an observer. - Synchronous completion re-entrancy
- running a completion callback inline from an observer notification lets the callback’s owner destroy the notifying observer before the notifier’s iteration has unwound.
- `PostTask`
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask()defers a closure to a later turn of the current thread’s task loop, after the current call stack has returned.
Root Cause Analysis
In content/browser/digital_credentials/cross_device_transaction_impl.cc, the branch if (!powered && !waiting_for_power_) inside TransactionImpl::AdapterPoweredChanged() (the hunk header carries no function name; the commit message and test identify it) handled loss of Bluetooth power by calling std::move(callback_).Run(base::unexpected(SystemError::kLostPower)) synchronously. According to the commit message, that completion callback synchronously destroys the DigitalIdentityProvider and the TransactionImpl itself, while BluetoothAdapter is still iterating its ObserverList to deliver AdapterPoweredChanged(false), producing a use-after-free (or re-entrancy crash). The violated invariant is that an observer must not be destroyed from within its own notification.
The fix posts the callback with PostTask(FROM_HERE, base::BindOnce(std::move(callback_), ...)), so the TransactionImpl is torn down only after the adapter’s notification loop and AdapterPoweredChanged() have returned. The commit notes this is a follow-up to an earlier change that fixed other synchronous-error paths but missed the “adapter turned off mid-transaction” case.
BluetoothAdapter observer notification; deferring it with PostTask removes the re-entrant self-destruction.Attack Path
- Start a Digital Credentials request
a web page invokes the Digital Credentials API in a way that selects the cross-device (Bluetooth) flow, creating a
TransactionImplthat registers as aBluetoothAdapterobserver in the browser process. - Transaction reaches the ready state
Bluetooth is present, permitted, and powered, so the transaction proceeds (the test waits for
SystemEvent::kReady). - Bluetooth adapter powers off
the adapter is turned off while the transaction is in progress, and
BluetoothAdapternotifies observers viaAdapterPoweredChanged(adapter, false). - Synchronous teardown during iteration
the
!powered && !waiting_for_power_branch runscallback_inline, whose owner destroys theDigitalIdentityProviderandTransactionImplwhile the adapter is still iterating its observer list, so the adapter’s subsequent access touches freed memory.
Impact Assessment
content/browser), which is unsandboxed; any memory-corruption primitive beyond a crash is (inferred). Reaching it requires a page to start a cross-device Digital Credentials transaction and the Bluetooth adapter to lose power during that transaction (e.g. the user or OS turning Bluetooth off), a timing/user-action condition that web content does not control as far as the diff shows; it also requires a platform where the Bluetooth-based cross-device flow is used. The vendor rates this as high severity; the advisory provided does not state in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/digital_credentials/cross_device_transaction_impl.cc |
modified | |
TEST_Pcontent/browser/digital_credentials/cross_device_transaction_impl_unittest.cc |
modified |
Files Changed
content/browser/digital_credentials/cross_device_transaction_impl.cccontent/browser/digital_credentials/cross_device_transaction_impl_unittest.cc
Audit Directions
- Completion callbacks run from observer methodsaudit other
BluetoothAdapter::Observer(and genericbase::ObserverList) implementations incontent/browser/digital_credentialsanddevice/fidoforstd::move(callback).Run(...)insideAdapterPresentChanged,AdapterPoweredChanged, orDeviceChanged-style notifications. - Error paths added after the factthe commit is a follow-up to an earlier fix for other error cases; check every remaining
SystemErrorpath inTransactionImpl(and its siblings) for synchronous callback invocation that can destroythis. - Tests that hide self-destructionlook for unit tests that hold the object independently of the completion callback (as the old tests did with a local
std::unique_ptrandbase::DoNothing()), which cannot catch callbacks that delete their caller.
Patch
From 952d06969915c8e44f9ab6008be6f14f6338901e Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
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 <mamir@chromium.org>
Reviewed-by: Martin Kreichgauer <martinkr@google.com>
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(,
Regression Test / PoC
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(,
Original Bug Report
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
callback_is owned byDigitalIdentityRequestImpl- Synchronous
.Run()immediately triggers parent cleanup - Cleanup calls
provider_.reset() provider_ownsTransactionImplTransactionImplis destroyed- Background worker thread is still executing inside
TransactionImpl - 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
- I used win32-release_x64-media_asan-win32-release_x64-1504065.
- Launch chromium from chromium folder with this flag
chrome.exe --enable-features=FedCmDigitalIdentity - Load the attached
poc.html. - Ensure Bluetooth is enabled.
- Trigger the Digital Credentials request.
- Immediately disable Bluetooth.
- 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:
TransactionImpldestructor 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)
Is the security issue related to a crash?
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