Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker523704570
Fix commit30838188f5ff (chromium/src) +6/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-23

Files Changed

  • device/bluetooth/bluetooth_socket_mac.mm
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_);
Loading diff…

Original Bug Report

reported by rj...@google.com

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.

  1. Circular Reference Creation: In BluetoothSocketMac::AcceptConnectionRequest (device/bluetooth/bluetooth_socket_mac.mm), a new client_socket is created. A success_callback is bound using base::BindOnce (line 1001), which captures the scoped_refptr<BluetoothSocketMac> by value. This callback is stored in client_socket->connect_callbacks_. This establishes a cycle: client_socket owns connect_callbacks_, which owns the closure holding the scoped_refptr to client_socket. When the function returns, this closure holds the only reference (refcount = 1).
  2. The Race Condition: The code expects setting the channel delegate (SetSocket) to synchronously trigger the OS rfcommChannelOpenComplete: callback, which would post a task to the message loop (adding another reference). However, macOS IOBluetooth callbacks for already-open incoming channels are dispatched asynchronously. This leaves a window where the refcount remains exactly 1.
  3. Synchronous Destruction: If an attacker disconnects during this window, the OS triggers rfcommChannelClosed:, calling BluetoothSocketMac::OnChannelClosed(), which calls ReleaseChannel(). At line 1043, ReleaseChannel calls connect_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_).
  4. Use-After-Free & Double-Free: Execution returns to ReleaseChannel() immediately after the reset(), but this has now been freed. The code executes empty_queue(receive_queue_) (line 1045) on the freed object. The empty_queue helper uses std::swap with 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 to free() 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.)

  1. An attacker targets a Chrome instance on macOS where an extension is actively listening via the chrome.bluetoothSocket API.
  2. The attacker connects a malicious Bluetooth device to the advertised RFCOMM or L2CAP service.
  3. 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.
  4. 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.

View on issue tracker