CVE-2026-13035
Overview
Background
- `BluetoothSocketMac`
- the macOS RFCOMM/L2CAP Bluetooth socket object, reference-counted, that owns an IOBluetooth channel plus its connect/receive callback state.
- `ReleaseChannel`
- the method that tears down the socket’s channel and callback/queue state; it runs on the socket’s checked thread.
- `ConnectCallbacks` / `connect_callbacks_`
- a
std::unique_ptr-held bundle of callbacks whose destruction can, transitively, drop the last reference-counted reference to the owningBluetoothSocketMac. - Refcount-driven self-destruction
- releasing a
scoped_refptr/callback that captured the last reference tothiscan synchronously run the object’s destructor mid-method.
Root Cause Analysis
ReleaseChannel reset channel_, then called connect_callbacks_.reset() before touching receive_callbacks_, receive_queue_, and send_queue_. Destroying connect_callbacks_ could release the last reference to the BluetoothSocketMac (the connect callbacks or bound state transitively held a reference to this), causing this to be destroyed synchronously inside reset(). Execution would then return into ReleaseChannel and continue accessing member variables (receive_callbacks_, the queues) on already-freed this, a use-after-free. The invariant violated is that this must remain live for the duration of ReleaseChannel.
The fix moves connect_callbacks_ into a local std::unique_ptr<ConnectCallbacks> temp_connect instead of destroying it in place, so any last reference is not dropped until temp_connect is destroyed at method scope exit, keeping this alive through the remaining member accesses.
connect_callbacks_ in the middle of ReleaseChannel when that destruction could drop the last reference to this; the fix defers the destruction to end-of-scope by transferring ownership into a local variable, guaranteeing this outlives the rest of the method.Attack Path
- Reach channel release
An attacker-influenced Bluetooth socket lifecycle event (disconnect/close/error) causes
ReleaseChannelto run on the socket thread. - Sole reference in callbacks
At that point
connect_callbacks_(or state it holds) is the last holder of the reference-countedBluetoothSocketMac(inferred as the specific ownership chain). - Synchronous self-destruction
connect_callbacks_.reset()runs the callbacks’ destructor, which drops the final reference and synchronously destroysthis. - Use-after-free
Control returns into
ReleaseChannel, which then accessesreceive_callbacks_,receive_queue_, andsend_queue_on the freedBluetoothSocketMac, reading/writing freed memory. - Corrupt reclaimed memory If the freed socket storage has been reallocated, these member accesses operate on attacker-influenced memory, potentially leading to memory corruption (inferred).
Impact Assessment
ReleaseChannel with the reference topology described. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.Files Changed
device/bluetooth/bluetooth_socket_mac.mm
Audit Directions
- Refcounted self-destruction mid-methodAudit methods that
reset()/clear a smart pointer or callback that may transitively own the last reference tothis, then continue touching members afterward; move such releases to end of scope or take a localscoped_refptrguard. - Callback bundles holding `this`Review objects whose stored callbacks capture
this(or a self reference) and whose teardown paths destroy those callbacks before finishing cleanup. - Ordering in teardown/`Release`/`Close` methodsLook across the codebase for teardown routines that free callback/queue state in an order where an early free can invalidate
thisbefore later statements execute.
Patch
From 30838188f5ff65357672bcd00dfaff982ca7ca25 Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> 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 <[email protected]> Reviewed-by: Matt Reynolds <[email protected]> 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.