Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker499007266
Fix commitba7af815c81c (chromium/src) +90/-24
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
device/bluetooth/bluetooth_socket_mac.mm
modified
while
device/bluetooth/bluetooth_socket_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_socket_mac.mm
  • device/bluetooth/public/cpp/bluetooth_features.cc
  • device/bluetooth/public/cpp/bluetooth_features.h
From ba7af815c81c3a2cc634c2516adbd5c5cff109a5 Mon Sep 17 00:00:00 2001
From: Alvin Ji <[email protected]>
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 <[email protected]>
Commit-Queue: Alvin Ji <[email protected]>
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_
Loading diff…

Original Bug Report

reported by [email protected]

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.mm
  • services/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:

  1. BluetoothSocketMac::OnChannelWriteComplete executes while request_ptr->active_async_writes is still 0.
  2. The counter is decremented to -1.
  3. Because the counter is less than 1, the SendRequest is moved from send_queue_ into a local std::unique_ptr and popped from the queue.
  4. The success_callback is executed inline.
  5. OnChannelWriteComplete returns, destroying the local std::unique_ptr and freeing the SendRequest memory.
  6. Execution resumes in Send immediately after the WriteAsync call.
  7. 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

  1. A malicious website requests and is granted Web Serial API permissions to a Bluetooth device.
  2. The compromised renderer process calls SerialPort::write() with a data payload strictly larger than the Bluetooth MTU.
  3. The macOS Bluetooth stack processes the first chunk synchronously, triggering the UAF and the sequence described above.
  4. The browser process crashes with a CHECK failure 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.

View on issue tracker