Medium watch CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Sub-componentBluetooth
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker499007266
Fix commitba7af815c81c (chromium/src) +90/-24
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-05-15 (public git)
Disclosed2026-06-30
Patch gap46 days — fix public in source before the advisory

Background

`BluetoothSocketMac::Send`
the macOS implementation that transmits a caller-supplied buffer over an RFCOMM/L2CAP Bluetooth channel by splitting it into MTU-sized chunks and issuing each via channel_->WriteAsync.
`active_async_writes`
a per-request counter on request_ptr tracking how many asynchronous write operations are still in flight; when it reaches zero the request is considered complete and send_queue_.pop() frees it.
`WriteAsync`
an IOBluetooth-backed asynchronous write whose completion callback, on macOS, can fire synchronously from within the WriteAsync call itself rather than later on the run loop.
`kBluetoothSocketMacPreCalculateWriteChunks`
the new feature flag (enabled by default) gating the fixed code path that computes the total chunk count up front.

Root Cause Analysis

The pre-patch loop in BluetoothSocketMac::Send incremented request_ptr->active_async_writes after the call to channel_->WriteAsync returned. On macOS the IOBluetooth WriteAsync completion callback can execute synchronously during the WriteAsync call, and that callback decrements active_async_writes and, when it hits zero, calls send_queue_.pop() which destroys the request object. Because the increment had not yet happened when the synchronous callback ran, the counter for the just-issued chunk was observed as zero, causing premature pop() and destruction of request_ptr while the loop still intended to issue further chunks and continued to dereference the freed request_ptr (and iterate send_buffer).

The fix pre-calculates num_chunks and initializes active_async_writes = num_chunks before any WriteAsync call, so a synchronous callback can never drive the counter to zero while writes remain to be issued; on an error path it subtracts only the remaining unissued chunks rather than resetting to zero, preserving correctness for in-flight writes.

Key insight
The counter that governs the request’s lifetime was updated after the operation that could trigger its consumer, creating a window where a synchronous completion callback frees the request out from under the still-running loop; initializing the counter to the full chunk total before issuing any write closes that window.

Attack Path

  1. Trigger a multi-chunk Bluetooth send A local Bluetooth peer (or paired device) arranges a socket whose outgoing MTU is smaller than the payload, so BluetoothSocketMac::Send must loop over multiple WriteAsync chunks. (inferred)
  2. Force a synchronous completion callback Under IOBluetooth on macOS, the WriteAsync completion callback fires synchronously during the first chunk’s WriteAsync call, before active_async_writes++ executes.
  3. Premature request destruction The synchronous callback sees active_async_writes == 0, treats the request as finished, and executes send_queue_.pop(), freeing request_ptr.
  4. Use-after-free Control returns into the loop, which increments/dereferences the now-freed request_ptr and issues subsequent chunks against freed state, constituting the use-after-free. Any further exploitation (controlled reallocation of the freed request, read/write primitive) is not shown by the patch. (inferred)

Impact Assessment

An attacker able to drive Bluetooth socket writes on macOS can cause a use-after-free of the send request object in the browser/device process that hosts the Bluetooth stack, leading at minimum to memory corruption and likely a crash. The advisory classifies this as a use-after-free in Bluetooth with no exploited-in-the-wild status stated; escalation to controlled code execution is not demonstrated by the patch and is (inferred). Preconditions include an active Bluetooth socket and a payload spanning more than one MTU-sized chunk on the macOS IOBluetooth path.

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

Audit Directions

  • Post-operation counter updates
    Flag any reference-count or in-flight counter that is incremented after invoking an async operation whose callback may run synchronously and consume that counter; the update must precede the operation.
  • Synchronous-callback assumptions
    Audit platform async APIs (IOBluetooth, Grand Central Dispatch, other OS callbacks) used elsewhere in device/bluetooth and beyond for code that assumes the completion handler is always deferred to a later run-loop turn.
  • Queue pop on completion
    Review completion handlers that call pop()/destroy objects from a shared queue while a caller still holds a raw pointer into that queue, ensuring lifetime ownership survives all issuing loops.
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_
Loading diff…

Original Bug Report

reported by vm...@google.com

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