CVE-2026-13878
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/bluetooth_socket_mac.mm |
modified | |
whiledevice/bluetooth/bluetooth_socket_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_socket_mac.mmdevice/bluetooth/public/cpp/bluetooth_features.ccdevice/bluetooth/public/cpp/bluetooth_features.h
Patch
From ba7af815c81c3a2cc634c2516adbd5c5cff109a5 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Fri, 15 May 2026 13:37:41 -0700
Subject: [PATCH] bluetooth: Fix Use-After-Free in BluetoothSocketMac::Send
IOBluetooth writeAsync callbacks can fire synchronously on macOS,
causing premature request cleanup and UAFs if pending write counters are
incremented after the call.
This CL pre-calculates total chunks and initializes active_async_writes
upfront to ensure safe lifetime management across multi-chunk writes.
Bug: 499007266
Change-Id: I7bb48661e3b2ce72e3f9457c4be07b66a027b845
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7833477
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1631527}
---
diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index a303bc0..60b5df97 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -34,6 +34,7 @@
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/bluetooth_l2cap_channel_mac.h"
#include "device/bluetooth/bluetooth_rfcomm_channel_mac.h"
+#include "device/bluetooth/public/cpp/bluetooth_features.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
@@ -773,33 +774,86 @@
uint16_t mtu = channel_->GetOutgoingMTU();
auto send_buffer =
base::MakeRefCounted<net::DrainableIOBuffer>(buffer.get(), buffer_size);
- while (send_buffer->BytesRemaining() > 0) {
- int byte_count = send_buffer->BytesRemaining();
- if (byte_count > mtu)
- byte_count = mtu;
- IOReturn status =
- channel_->WriteAsync(send_buffer->data(), byte_count, request_ptr);
- if (status != kIOReturnSuccess) {
- std::stringstream error;
- error << "Failed to connect bluetooth socket ("
- << channel_->GetDeviceAddress() << "): (" << status << ")";
- // Remember the first error only
- if (request_ptr->status == kIOReturnSuccess)
- request_ptr->status = status;
- request_ptr->error_signaled = true;
- std::move(request_ptr->error_callback).Run(error.str());
- // We may have failed to issue any write operation. In that case, there
- // will be no corresponding completion callback for this particular
- // request, so we must forget about it now.
- if (request_ptr->active_async_writes == 0) {
- send_queue_.pop();
+ if (base::FeatureList::IsEnabled(
+ features::kBluetoothSocketMacPreCalculateWriteChunks)) {
+ // Pre-calculate total chunks to prevent premature cleanup if callbacks fire
+ // synchronously.
+ int num_chunks = buffer_size / mtu + (buffer_size % mtu != 0 ? 1 : 0);
+ request_ptr->active_async_writes = num_chunks;
+ int chunks_remaining = num_chunks;
+
+ while (chunks_remaining > 0) {
+ int byte_count = send_buffer->BytesRemaining();
+ if (byte_count > mtu) {
+ byte_count = mtu;
}
- return;
- }
- request_ptr->active_async_writes++;
- send_buffer->DidConsume(byte_count);
+ IOReturn status =
+ channel_->WriteAsync(send_buffer->data(), byte_count, request_ptr);
+
+ if (status != kIOReturnSuccess) {
+ std::stringstream error;
+ error << "Failed to connect bluetooth socket ("
+ << channel_->GetDeviceAddress() << "): (" << status << ")";
+ // Remember the first error only
+ if (request_ptr->status == kIOReturnSuccess) {
+ request_ptr->status = status;
+ }
+ request_ptr->error_signaled = true;
+ std::move(request_ptr->error_callback).Run(error.str());
+
+ // We failed to issue this write and all subsequent writes.
+ // Subtract the remaining unissued chunks rather than setting to 0
+ // directly, to avoid prematurely freeing the request if previous chunks
+ // succeeded asynchronously and are still in flight.
+ request_ptr->active_async_writes -= chunks_remaining;
+
+ // We may have failed to issue any write operation. In that case, there
+ // will be no corresponding completion callback for this particular
+ // request, so we must forget about it now.
+ if (request_ptr->active_async_writes == 0) {
+ send_queue_.pop();
+ }
+ return;
+ }
+
+ chunks_remaining--;
+ send_buffer->DidConsume(byte_count);
+ }
+ } else {
+ while (send_buffer->BytesRemaining() > 0) {
+ int byte_count = send_buffer->BytesRemaining();
+ if (byte_count > mtu) {
+ byte_count = mtu;
+ }
+
+ IOReturn status =
+ channel_->WriteAsync(send_buffer->data(), byte_count, request_ptr);
+
+ if (status != kIOReturnSuccess) {
+ std::stringstream error;
+ error << "Failed to connect bluetooth socket ("
+ << channel_->GetDeviceAddress() << "): (" << status << ")";
+ // Remember the first error only
+ if (request_ptr->status == kIOReturnSuccess) {
+ request_ptr->status = status;
+ }
+ request_ptr->error_signaled = true;
+ std::move(request_ptr->error_callback).Run(error.str());
+
+ // We may have failed to issue any write operation. In that case, there
+ // will be no corresponding completion callback for this particular
+ // request, so we must forget about it now.
+ if (request_ptr->active_async_writes == 0) {
+ send_queue_.pop();
+ }
+ return;
+ }
+
+ request_ptr->active_async_writes++;
+ send_buffer->DidConsume(byte_count);
+ }
}
}
diff --git a/device/bluetooth/public/cpp/bluetooth_features.cc b/device/bluetooth/public/cpp/bluetooth_features.cc
index 537b365..f99e54a5 100644
--- a/device/bluetooth/public/cpp/bluetooth_features.cc
+++ b/device/bluetooth/public/cpp/bluetooth_features.cc
@@ -11,4 +11,13 @@
BASE_FEATURE(kWebBluetoothAllowGetAvailabilityWithBfcache,
base::FEATURE_ENABLED_BY_DEFAULT);
+// When enabled, BluetoothSocketMac::Send pre-calculates the number of chunks
+// to write and initializes request_ptr->active_async_writes to this total
+// upfront. This prevents a Use-After-Free caused by premature cleanup if
+// completion callbacks fire synchronously during a multi-chunk write. Without
+// this, a synchronous callback would decrement the counter and could pop the
+// send_queue_ before all chunks have been issued.
+BASE_FEATURE(kBluetoothSocketMacPreCalculateWriteChunks,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
} // namespace features
diff --git a/device/bluetooth/public/cpp/bluetooth_features.h b/device/bluetooth/public/cpp/bluetooth_features.h
index fdae6b7..ae5e94c4 100644
--- a/device/bluetooth/public/cpp/bluetooth_features.h
+++ b/device/bluetooth/public/cpp/bluetooth_features.h
@@ -16,6 +16,9 @@
BLUETOOTH_FEATURES_EXPORT BASE_DECLARE_FEATURE(
kWebBluetoothAllowGetAvailabilityWithBfcache);
+BLUETOOTH_FEATURES_EXPORT BASE_DECLARE_FEATURE(
+ kBluetoothSocketMacPreCalculateWriteChunks);
+
} // namespace features
#endif // DEVICE_BLUETOOTH_PUBLIC_CPP_BLUETOOTH_FEATURES_H_
Original Bug Report
Potential UAF and DoS in BluetoothSocketMac::Send via synchronous IOBluetooth callbacks
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A Use-After-Free (UAF) and subsequent CHECK crash can occur in BluetoothSocketMac::Send on macOS. The issue arises because the active_async_writes counter is incremented after a potentially synchronous WriteAsync callback has already destroyed the SendRequest object, leading to a write-after-free and a predictable browser process crash (DoS).
Affected files:
device/bluetooth/bluetooth_socket_mac.mmservices/device/serial/bluetooth_serial_port_impl.cc
Estimated timestamp from git blame: 2020-07-13
Summary
A Use-After-Free (UAF) vulnerability exists in BluetoothSocketMac::Send (device/bluetooth/bluetooth_socket_mac.mm). The issue is caused by incorrect state management when the macOS IOBluetooth framework executes an asynchronous write completion delegate synchronously. While this leads to a write-after-free, Chromium’s object lifetime semantics and internal checks safely catch the resulting state corruption, turning this into a Denial of Service (DoS) (browser process crash) rather than an exploitable RCE.
Technical Details
In BluetoothSocketMac::Send, the code loops to chunk data and initiates asynchronous writes using channel_->WriteAsync. The counter tracking active writes is incremented only after the WriteAsync call returns:
// device/bluetooth/bluetooth_socket_mac.mm
778: IOReturn status =
779: channel_->WriteAsync(send_buffer->data(), byte_count, request_ptr);
...
799: request_ptr->active_async_writes++;
Under specific conditions on macOS, IOBluetooth can dispatch the completion delegate (rfcommChannelWriteComplete:refcon:status:) synchronously before returning to the caller. When this happens:
BluetoothSocketMac::OnChannelWriteCompleteexecutes whilerequest_ptr->active_async_writesis still0.- The counter is decremented to
-1. - Because the counter is less than
1, theSendRequestis moved fromsend_queue_into a localstd::unique_ptrand popped from the queue. - The
success_callbackis executed inline. OnChannelWriteCompletereturns, destroying the localstd::unique_ptrand freeing theSendRequestmemory.- Execution resumes in
Sendimmediately after theWriteAsynccall. - At line 799,
request_ptr->active_async_writes++increments a field on the now-freed object, constituting a UAF write.
If the data payload was larger than the MTU, the while loop continues to a second iteration. It calls WriteAsync again, passing the dangling request_ptr as the refcon. If this second call also completes synchronously, OnChannelWriteComplete is re-entered.
At line 811, CHECK_EQ(static_cast<SendRequest*>(refcon), request_ptr) attempts to verify the refcon against the front of the queue. Because the original request was popped during the first iteration, the queue is either empty (undefined behavior) or contains a new request pushed by a re-entrant Send during the success_callback. Because the original request was kept alive during the first success_callback by the local std::unique_ptr, its memory address cannot have been reused for the new request. Thus, the CHECK_EQ predictably fails, causing a safe browser crash (DoS).
Potential Trigger Steps
- A malicious website requests and is granted Web Serial API permissions to a Bluetooth device.
- The compromised renderer process calls
SerialPort::write()with a data payload strictly larger than the Bluetooth MTU. - The macOS Bluetooth stack processes the first chunk synchronously, triggering the UAF and the sequence described above.
- The browser process crashes with a
CHECKfailure on the second chunk’s synchronous completion.
Suggested Fix
The vulnerability can be resolved by incrementing active_async_writes before initiating the write operation, and decrementing it upon failure, ensuring the object is not prematurely destroyed by synchronous callbacks:
request_ptr->active_async_writes++;
IOReturn status =
channel_->WriteAsync(send_buffer->data(), byte_count, request_ptr);
if (status != kIOReturnSuccess) {
request_ptr->active_async_writes--;
...
Alternatively, OnChannelWriteComplete could unconditionally post its work to the current sequence task runner, strictly enforcing asynchronous completion semantics.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.