CVE-2026-13035
Overview
Files Changed
device/bluetooth/bluetooth_socket_mac.mm
Patch
From 30838188f5ff65357672bcd00dfaff982ca7ca25 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Wed, 17 Jun 2026 17:37:38 -0700
Subject: [PATCH] Fix Use-After-Free in BluetoothSocketMac::ReleaseChannel
Prevent synchronous destruction of BluetoothSocketMac when releasing the
channel by moving connect and receive callbacks to local variables. This
ensures the socket remains alive until ReleaseChannel finishes
execution, avoiding use-after-free when accessing other member
variables.
Change-Id: I872d682b6a4d0887b8f93e118db54e51f57321f5
Bug: 523704570
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7948137
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1648713}
---
diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index 315f1ddf..abdc9a1 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -1038,9 +1038,12 @@
DCHECK(thread_checker_.CalledOnValidThread());
channel_.reset();
- // Closing the channel above prevents the callback delegate from being called
- // so it is now safe to release all callback state.
- connect_callbacks_.reset();
+ // Move connect_callbacks_ to a local variable to prevent synchronous
+ // destruction of 'this' if it holds the last reference to the socket.
+ // This keeps 'this' alive until the end of this method.
+ std::unique_ptr<ConnectCallbacks> temp_connect =
+ std::move(connect_callbacks_);
+
receive_callbacks_.reset();
empty_queue(receive_queue_);
empty_queue(send_queue_);
Original Bug Report
Potential Use-After-Free and Double-Free in BluetoothSocketMac::ReleaseChannel
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A Use-After-Free vulnerability exists in BluetoothSocketMac::ReleaseChannel on macOS when handling incoming Bluetooth connections. If an attacker disconnects during connection setup, a circular reference is broken, causing synchronous object destruction while methods are still executing on it. This leads to a deterministic double-free and heap freelist corruption in the Browser process.
Affected files:
device/bluetooth/bluetooth_socket_mac.mm
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential Use-After-Free (UAF) and Double-Free vulnerability exists in BluetoothSocketMac on macOS. It can be triggered when an incoming Bluetooth connection is closed before the connection setup is fully complete. This leads to a situation where the BluetoothSocketMac instance is destroyed while its ReleaseChannel method is still executing, resulting in memory corruption within the highly-privileged Browser process.
Root Cause Analysis
The vulnerability stems from a circular reference created during incoming connection handling, combined with the synchronous destruction of the socket object.
- Circular Reference Creation: In
BluetoothSocketMac::AcceptConnectionRequest(device/bluetooth/bluetooth_socket_mac.mm), a newclient_socketis created. Asuccess_callbackis bound usingbase::BindOnce(line 1001), which captures thescoped_refptr<BluetoothSocketMac>by value. This callback is stored inclient_socket->connect_callbacks_. This establishes a cycle:client_socketownsconnect_callbacks_, which owns the closure holding thescoped_refptrtoclient_socket. When the function returns, this closure holds the only reference (refcount = 1). - The Race Condition: The code expects setting the channel delegate (
SetSocket) to synchronously trigger the OSrfcommChannelOpenComplete:callback, which would post a task to the message loop (adding another reference). However, macOSIOBluetoothcallbacks for already-open incoming channels are dispatched asynchronously. This leaves a window where the refcount remains exactly 1. - Synchronous Destruction: If an attacker disconnects during this window, the OS triggers
rfcommChannelClosed:, callingBluetoothSocketMac::OnChannelClosed(), which callsReleaseChannel(). At line 1043,ReleaseChannelcallsconnect_callbacks_.reset(). This destroys the closure, dropping the refcount to 0. The~BluetoothSocketMac()destructor immediately runs, freeing the object and its internal queues (receive_queue_,send_queue_). - Use-After-Free & Double-Free: Execution returns to
ReleaseChannel()immediately after thereset(), butthishas now been freed. The code executesempty_queue(receive_queue_)(line 1045) on the freed object. Theempty_queuehelper usesstd::swapwith a local stack variable. This swap copies the already-freed buffer pointer into the local variable, and writes the local variable’s null state into the freed object memory (corrupting the PartitionAlloc freelist). When the local variable goes out of scope, it attempts tofree()the queue’s internal structures a second time, causing a deterministic double-free.
Potential Exploitation Steps
(Note: These are suggested steps based on static analysis; a working Proof of Concept has not yet been executed by our tooling.)
- An attacker targets a Chrome instance on macOS where an extension is actively listening via the
chrome.bluetoothSocketAPI. - The attacker connects a malicious Bluetooth device to the advertised RFCOMM or L2CAP service.
- Immediately upon establishing the connection, the attacker sends a small payload (to ensure
receive_queue_allocates memory) and then forcibly disconnects the channel before the OS has time to dispatch the open-complete callback on the run loop. - The disconnect triggers the UAF and Double-Free. By grooming the heap, the attacker could leverage the freelist corruption or the double-free to achieve arbitrary Remote Code Execution (RCE) in the unsandboxed Browser process.
Suggested Fix
To fix this issue, ReleaseChannel() must ensure that this is kept alive until the method finishes executing, or it must avoid triggering the destructor synchronously. The connect_callbacks_ should be moved into a local variable before destruction, a pattern already correctly used in BluetoothSocketMac::OnChannelOpeningTimeout() (line 712).
void BluetoothSocketMac::ReleaseChannel() {
DCHECK(thread_checker_.CalledOnValidThread());
channel_.reset();
// Move callbacks to local variables to prevent synchronous destruction of
// 'this' if they hold the last reference to the socket.
std::unique_ptr<ConnectCallbacks> temp_connect = std::move(connect_callbacks_);
std::unique_ptr<ReceiveCallbacks> temp_receive = std::move(receive_callbacks_);
empty_queue(receive_queue_);
empty_queue(send_queue_);
// temp_connect and temp_receive are destroyed here, safely dropping refcounts
// after we are done accessing member variables.
}
Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.