CVE-2026-9964
Overview
Files Changed
device/bluetooth/bluetooth_l2cap_channel_mac.mmdevice/bluetooth/bluetooth_rfcomm_channel_mac.mmdevice/bluetooth/bluetooth_socket_mac.mm
Patch
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.";
Original Bug Report
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.mmdevice/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.
- A malicious Chrome extension with
bluetoothpermissions callschrome.bluetoothSocket.create(). - The extension initiates
chrome.bluetoothSocket.connect()targeting a cached or immediately available L2CAP service on a paired macOS Bluetooth device. - 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. - When the SDP query completes,
BluetoothSocketMac::OnSDPQueryCompleteruns, dropping its internal strong references (timers and listeners) to itself. - It then calls
OpenAsync, which callsopenL2CAPChannelAsync. The OS completes this synchronously, firing the completion delegate. BluetoothSocketMac::OnChannelOpenCompleteruns and executes the extension’s connection callback.- The callback (
BluetoothSocketAbstractConnectFunction::OnConnect) fails to find the socket (since it was closed in step 3), drops the finalscoped_refptr, andBluetoothSocketMacis deleted. - Execution returns to
OnSDPQueryComplete, which assigns the new channel to the freedchannel_member, triggering heap corruption.
Suggested Fix
There are two primary ways to fix this:
- Lifetime Protection: Hold a strong reference to
thisinsideBluetoothSocketMac::OnSDPQueryCompleteto guarantee the object survives the potential synchronous callback:scoped_refptr<BluetoothSocketMac> self(this); // ... call OpenAsync ... - Callback PostTask: Alternatively, ensure that
BluetoothL2capChannelMac::OnChannelOpenCompletealways dispatches the result to the socket asynchronously viabase::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.