Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker505190999
Fix commitcc8c9154cfd6 (chromium/src) +21/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • device/bluetooth/bluetooth_l2cap_channel_mac.mm
  • device/bluetooth/bluetooth_rfcomm_channel_mac.mm
  • device/bluetooth/bluetooth_socket_mac.mm
From cc8c9154cfd6ab0183148852f53a8daad1f3eaa0 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <[email protected]>
Date: Fri, 24 Apr 2026 14:41:50 -0700
Subject: [PATCH] bluetooth: Fix Use-After-Free in BluetoothSocketMac connection handling.

Fix a Use-After-Free vulnerability in BluetoothSocketMac on macOS
caused by the synchronous execution of delegate callbacks originating
from the native IOBluetooth framework.

When initiating L2CAP or RFCOMM connections, macOS can complete the
operation synchronously. This immediately triggers OnChannelOpenComplete
inline, running the connection callback. If the client concurrently
closed the socket, the callback drops the final scoped_refptr,
synchronously destroying the BluetoothSocketMac instance. Upon stack
unwinding, execution attempts to assign the new channel to a freed
`this` pointer, resulting in memory corruption.

This CL resolves the race condition by:
1. Deferring socket callback execution in OnChannelOpenComplete
    using base::SingleThreadTaskRunner::PostTask, safely decoupling
    the OS event from the Chrome execution stack.
2. Wrapping the socket in base::WrapRefCounted inside the bind payload
    to guarantee the object survives in the task queue.
3. Upgrading the DCHECK to a defensive early return if the socket
    state changes before the task is processed.

Bug: 505190999
Change-Id: If3d786a1aa0967f6e8ed00fdb7706350bf073719
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7787138
Commit-Queue: Rob Pitkin <[email protected]>
Reviewed-by: Matt Reynolds <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1620484}
---

diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index aafa433..f1c5b39 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -7,7 +7,10 @@
 #include <memory>
 
 #include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "device/bluetooth/bluetooth_classic_device_mac.h"
 #include "device/bluetooth/bluetooth_socket_mac.h"
 
@@ -184,8 +187,12 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
-  socket()->OnChannelOpenComplete(
-      BluetoothClassicDeviceMac::GetDeviceAddress([channel device]), status);
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
+                                base::WrapRefCounted(socket()),
+                                BluetoothClassicDeviceMac::GetDeviceAddress(
+                                    [channel device]),
+                                status));
 }
 
 void BluetoothL2capChannelMac::OnChannelClosed(
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
index ebc6684..635a30f4 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
@@ -7,7 +7,10 @@
 #include <memory>
 
 #include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "device/bluetooth/bluetooth_classic_device_mac.h"
 #include "device/bluetooth/bluetooth_socket_mac.h"
 
@@ -184,8 +187,12 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
-  socket()->OnChannelOpenComplete(
-      BluetoothClassicDeviceMac::GetDeviceAddress([channel getDevice]), status);
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
+                                base::WrapRefCounted(socket()),
+                                BluetoothClassicDeviceMac::GetDeviceAddress(
+                                    [channel getDevice]),
+                                status));
 }
 
 void BluetoothRfcommChannelMac::OnChannelClosed(
diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index e5901d2..a303bc0 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -632,7 +632,9 @@
     const std::string& device_address,
     IOReturn status) {
   DCHECK(thread_checker_.CalledOnValidThread());
-  DCHECK(is_connecting());
+  if (!is_connecting()) {
+    return;
+  }
 
   DVLOG(1) << device_address << " " << uuid_.canonical_value()
            << ": channel open complete.";
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in BluetoothSocketMac L2CAP connection handling

Flapjack, 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 Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in BluetoothSocketMac::OnSDPQueryComplete on macOS when establishing L2CAP connections. If the macOS Bluetooth framework completes the connection synchronously and the socket is concurrently closed by the client, the BluetoothSocketMac object can be destroyed while executing on the stack. When the stack unwinds, a member variable assignment writes to freed memory, which could be leveraged for arbitrary code execution in the Browser process.

Affected files:

  • device/bluetooth/bluetooth_socket_mac.mm
  • device/bluetooth/bluetooth_l2cap_channel_mac.mm

Estimated timestamp from git blame: 2021-04-05

Description

A potential Use-After-Free vulnerability exists in the macOS Bluetooth socket implementation for L2CAP channels. The issue occurs because BluetoothSocketMac does not adequately protect its lifetime against synchronous delegate callbacks from the native macOS IOBluetooth framework.

During an L2CAP connection, BluetoothSocketMac::OnSDPQueryComplete calls BluetoothL2capChannelMac::OpenAsync. This method delegates to the macOS API [device openL2CAPChannelAsync:withPSM:delegate:]. In certain states (e.g., if the connection is cached), this API can complete synchronously, immediately invoking the delegate’s l2capChannelOpenComplete:status: method before OpenAsync returns.

This triggers a synchronous call to BluetoothSocketMac::OnChannelOpenComplete, which executes the pending connection success_callback. If an attacker (e.g., via a malicious Chrome Extension) has concurrently queued a close operation on the socket, this callback will fail to look up the socket and drop the final scoped_refptr keeping the BluetoothSocketMac alive. The object is deleted synchronously. As the stack unwinds back to OnSDPQueryComplete, execution resumes on the destroyed this pointer:

// device/bluetooth/bluetooth_socket_mac.mm
void BluetoothSocketMac::OnSDPQueryComplete(...) {
  // ...
  } else {
    DCHECK_NE(l2cap_psm, kInvalidL2capPsm);
    channel_ = 
        BluetoothL2capChannelMac::OpenAsync(this, device, l2cap_psm, &status);
  }
  // ... timer_.Start(...) accesses freed `this` 
}

The assignment to channel_ writes a std::unique_ptr into freed heap memory, and triggers delete on the old, attacker-controllable pointer value. This can be exploited to achieve arbitrary code execution in the Browser process.

Potential Steps to Trigger

Note: These are suggested steps based on static code analysis; our tooling agent does not currently have the ability to run code to verify them with a live Proof of Concept.

  1. A malicious Chrome extension with bluetooth permissions calls chrome.bluetoothSocket.create().
  2. The extension initiates chrome.bluetoothSocket.connect() targeting a cached or immediately available L2CAP service on a paired macOS Bluetooth device.
  3. While the SDP query is in flight asynchronously, the extension calls chrome.bluetoothSocket.close(). This synchronously removes the socket from the Extension API’s internal resource manager.
  4. When the SDP query completes, BluetoothSocketMac::OnSDPQueryComplete runs, dropping its internal strong references (timers and listeners) to itself.
  5. It then calls OpenAsync, which calls openL2CAPChannelAsync. The OS completes this synchronously, firing the completion delegate.
  6. BluetoothSocketMac::OnChannelOpenComplete runs and executes the extension’s connection callback.
  7. The callback (BluetoothSocketAbstractConnectFunction::OnConnect) fails to find the socket (since it was closed in step 3), drops the final scoped_refptr, and BluetoothSocketMac is deleted.
  8. Execution returns to OnSDPQueryComplete, which assigns the new channel to the freed channel_ member, triggering heap corruption.

Suggested Fix

There are two primary ways to fix this:

  1. Lifetime Protection: Hold a strong reference to this inside BluetoothSocketMac::OnSDPQueryComplete to guarantee the object survives the potential synchronous callback:
    scoped_refptr<BluetoothSocketMac> self(this);
    // ... call OpenAsync ...
    
  2. Callback PostTask: Alternatively, ensure that BluetoothL2capChannelMac::OnChannelOpenComplete always dispatches the result to the socket asynchronously via base::SingleThreadTaskRunner::PostTask, preventing the re-entrant synchronous destruction.

Notably, the RFCOMM implementation (BluetoothRfcommChannelMac) avoids this specific exploit path because its delegate (BluetoothRfcommChannelDelegate) contains a CHECK(_rfcommChannel) which crashes the process in the synchronous scenario before the UAF can be triggered. Adding a similar CHECK to BluetoothL2capChannelDelegate would convert this into a safe DoS crash, but properly fixing the lifetime or deferring the callback is the preferred approach.

Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a


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