Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker523557855
Fix commit725513ffa178 (chromium/src) +24/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
delegate_
device/bluetooth/bluetooth_l2cap_channel_mac.mm
modified
if
device/bluetooth/bluetooth_l2cap_channel_mac.mm
modified
delegate_
device/bluetooth/bluetooth_rfcomm_channel_mac.mm
modified
if
device/bluetooth/bluetooth_rfcomm_channel_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_l2cap_channel_mac.h
  • device/bluetooth/bluetooth_l2cap_channel_mac.mm
  • device/bluetooth/bluetooth_rfcomm_channel_mac.h
  • device/bluetooth/bluetooth_rfcomm_channel_mac.mm
From 725513ffa178c24467ee4b1bcb78d29784b5c68b Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Wed, 15 Jul 2026 12:03:01 -0700
Subject: [PATCH] [macOS][Bluetooth] Only close channel in destructor if opened

When an outgoing connection fails, the delegate's self-retain
mechanism was bypassed if we didn't arm it, but if we did arm it
unconditionally, we would leak the delegate because closeChannel
was always called in the destructor.

This CL updates the destructors of BluetoothL2capChannelMac and
BluetoothRfcommChannelMac to only call closeChannel if the channel
was successfully opened. We also revert to only arming _strongSelf
on successful open.

TAG=agy
CONV=739fb0cc-cf81-4f3d-a995-a113aa8120ff

Bug: 523557855
Change-Id: I410c8daa5eb671867f5cf92adf3e447a057d7468
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7947649
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1662771}
---

diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.h b/device/bluetooth/bluetooth_l2cap_channel_mac.h
index f42d4c6..d0280b3 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.h
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.h
@@ -62,6 +62,9 @@
 
   // The delegate for the native channel.
   BluetoothL2capChannelDelegate* __strong delegate_;
+
+  // True if the channel is currently open.
+  bool is_opened_;
 };
 
 }  // namespace device
diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index 58436795..463e83bb 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -116,8 +116,7 @@
 BluetoothL2capChannelMac::BluetoothL2capChannelMac(
     BluetoothSocketMac* socket,
     IOBluetoothL2CAPChannel* channel)
-    : channel_(channel),
-      delegate_(nil) {
+    : channel_(channel), delegate_(nil), is_opened_(channel != nil) {
   SetSocket(socket);
 }
 
@@ -128,7 +127,9 @@
   // for events that occur after our destruction.
   [delegate_ resetOwner];
   [channel_ setDelegate:nil];
-  [channel_ closeChannel];
+  if (is_opened_) {
+    [channel_ closeChannel];
+  }
   // `delegate_`'s self-retain (`_strongSelf`) is only armed after a successful
   // open. If we are destroyed during a pending or failed open, keep the
   // delegate alive across one main-run-loop turn so any already-enqueued
@@ -203,6 +204,10 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
+  if (status == kIOReturnSuccess) {
+    is_opened_ = true;
+  }
+
   base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
                                 base::WrapRefCounted(socket()),
@@ -215,6 +220,7 @@
     IOBluetoothL2CAPChannel* channel) {
   DCHECK_EQ(channel_, channel);
   channel_ = nil;
+  is_opened_ = false;
   [delegate_ resetOwner];
   delegate_ = nil;
   socket()->OnChannelClosed();
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.h b/device/bluetooth/bluetooth_rfcomm_channel_mac.h
index 3c9fbf5b..15b83d9 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.h
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.h
@@ -63,6 +63,9 @@
 
   // The delegate for the native channel.
   BluetoothRfcommChannelDelegate* __strong delegate_;
+
+  // True if the channel is currently open.
+  bool is_opened_;
 };
 
 }  // namespace device
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
index 2354bad..7b893c78a 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
@@ -116,8 +116,7 @@
 BluetoothRfcommChannelMac::BluetoothRfcommChannelMac(
     BluetoothSocketMac* socket,
     IOBluetoothRFCOMMChannel* channel)
-    : channel_(channel),
-      delegate_(nil) {
+    : channel_(channel), delegate_(nil), is_opened_(channel != nil) {
   SetSocket(socket);
 }
 
@@ -128,7 +127,9 @@
   // for events that occur after our destruction.
   [delegate_ resetOwner];
   [channel_ setDelegate:nil];
-  [channel_ closeChannel];
+  if (is_opened_) {
+    [channel_ closeChannel];
+  }
   // `delegate_`'s self-retain (`_strongSelf`) is only armed after a successful
   // open. If we are destroyed during a pending or failed open, keep the
   // delegate alive across one main-run-loop turn so any already-enqueued
@@ -203,6 +204,10 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
+  if (status == kIOReturnSuccess) {
+    is_opened_ = true;
+  }
+
   base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
                                 base::WrapRefCounted(socket()),
@@ -215,6 +220,7 @@
     IOBluetoothRFCOMMChannel* channel) {
   DCHECK_EQ(channel_, channel);
   channel_ = nil;
+  is_opened_ = false;
   [delegate_ resetOwner];
   delegate_ = nil;
   socket()->OnChannelClosed();
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential Use-After-Free in macOS Bluetooth Delegates on failed connections

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 potential Use-After-Free vulnerability exists in the macOS implementation of Bluetooth RFCOMM and L2CAP channels. When an outgoing connection fails, the delegate’s self-retain mechanism is bypassed, and a short-term keep-alive in the destructor is insufficient to protect against late-arriving asynchronous callbacks from the OS. An attacker could intentionally delay over-the-air protocol responses to reliably trigger the UAF, potentially leading to remote code execution.

Affected files:

  • device/bluetooth/bluetooth_rfcomm_channel_mac.mm
  • device/bluetooth/bluetooth_l2cap_channel_mac.mm

Estimated timestamp from git blame: Unknown (Google3 checkout)

Overview

A potential Use-After-Free (UAF) vulnerability exists in the macOS implementation of Bluetooth RFCOMM and L2CAP channels, specifically within BluetoothRfcommChannelDelegate and BluetoothL2capChannelDelegate (device/bluetooth/bluetooth_rfcomm_channel_mac.mm and bluetooth_l2cap_channel_mac.mm).

To mitigate a known issue where macOS’s IOBluetooth framework may fire callbacks even after a delegate is cleared (Apple Feedback FB13705522), Chromium implements a self-retain mechanism (_strongSelf) during successful connections and a fallback short-term keep-alive in the C++ owner’s destructor. However, these protections are incomplete on the failed outgoing connection path.

Root Cause Analysis

In rfcommChannelOpenComplete:status: (and the equivalent L2CAP method), the _strongSelf self-retain is only armed if the connection is successful:

- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel
                           status:(IOReturn)error {
  CHECK(_rfcommChannel);
  if (error == kIOReturnSuccess) {
    _strongSelf = self;   // Only armed on SUCCESS
  }
  // ...
}

When an outgoing connection fails (error != kIOReturnSuccess), _strongSelf remains nil. The delegate’s lifetime is then tied solely to its C++ owner, BluetoothRfcommChannelMac.

When the C++ owner is destroyed (typically immediately following the connection failure), its destructor attempts to protect the delegate by capturing it in a dispatch_async block, keeping it alive for exactly one main-run-loop turn:

BluetoothRfcommChannelMac::~BluetoothRfcommChannelMac() {
  [delegate_ resetOwner];
  [channel_ setDelegate:nil];
  [channel_ closeChannel];
  // One-turn keep-alive
  BluetoothRfcommChannelDelegate* __strong delegate = delegate_;
  dispatch_async(dispatch_get_main_queue(), ^{ (void)delegate; });
}

Because [channel_ closeChannel] initiates an asynchronous, over-the-air protocol teardown (e.g., an RFCOMM DISC command), the resulting rfcommChannelClosed: callback from the OS can arrive much later. If an attacker intentionally delays the protocol-level response from their malicious Bluetooth device, the one-turn keep-alive will expire, and the delegate will be deallocated. When the OS eventually receives the delayed response and fires the terminal callback on the dangling pointer, an Objective-C message is sent to freed memory.

Suggested Exploit Steps

Note: These are potential steps as our tooling agent does not have the ability to run a live Proof of Concept.

  1. An attacker sets up a malicious physical Bluetooth device and places it in proximity to the victim.
  2. The victim visits an attacker-controlled website, which uses the Web Serial or Web Bluetooth API to request a connection to the malicious device.
  3. The malicious device accepts the initial request but intentionally rejects the RFCOMM/L2CAP protocol connection, returning an error.
  4. The rfcommChannelOpenComplete:status: callback fires with an error. The _strongSelf retain is skipped, and the C++ channel object is destroyed.
  5. The C++ destructor calls closeChannel (sending a disconnect command over the air) and posts the 1-turn keep-alive block.
  6. The malicious device deliberately delays its acknowledgment of the disconnect command.
  7. The main thread’s keep-alive block finishes executing, and the BluetoothRfcommChannelDelegate object is deallocated by ARC.
  8. The attacker sprays the Browser process heap via standard Web API IPCs, replacing the freed delegate memory with controlled data (including a fake isa pointer).
  9. The malicious device finally replies to the disconnect command.
  10. The IOBluetooth framework (due to bug FB13705522) fires the rfcommChannelClosed: callback on the freed delegate, hijacking control flow.

Impact

Since this occurs in the highly-privileged Browser process where macOS Bluetooth services are handled, this UAF can lead to a complete Sandbox Escape and Remote Code Execution (RCE). As Objective-C objects do not typically benefit from MiraclePtr protections when passed to external frameworks, the exploitability of this issue is considered high.

Suggested Fix

Unconditionally arm the _strongSelf self-reference in the openComplete callbacks, regardless of the connection status.

- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel
                           status:(IOReturn)error {
  CHECK(_rfcommChannel);
  // Keep the delegate alive until rfcommChannelClosed.
  _strongSelf = self;
  // ...
}

Because the OS guarantees that rfcommChannelClosed: (or the L2CAP equivalent) will eventually fire, it is safe to arm the retain cycle here; the terminal callback will accurately clear _strongSelf, breaking the cycle and allowing safe deallocation.

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