CVE-2026-79091
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
delegate_device/bluetooth/bluetooth_l2cap_channel_mac.mm |
modified | |
ifdevice/bluetooth/bluetooth_l2cap_channel_mac.mm |
modified | |
delegate_device/bluetooth/bluetooth_rfcomm_channel_mac.mm |
modified | |
ifdevice/bluetooth/bluetooth_rfcomm_channel_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_l2cap_channel_mac.hdevice/bluetooth/bluetooth_l2cap_channel_mac.mmdevice/bluetooth/bluetooth_rfcomm_channel_mac.hdevice/bluetooth/bluetooth_rfcomm_channel_mac.mm
Patch
From 725513ffa178c24467ee4b1bcb78d29784b5c68b Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> 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 <[email protected]> Commit-Queue: Alvin Ji <[email protected]> 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();
Original Bug Report
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.mmdevice/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.
- An attacker sets up a malicious physical Bluetooth device and places it in proximity to the victim.
- The victim visits an attacker-controlled website, which uses the Web Serial or Web Bluetooth API to request a connection to the malicious device.
- The malicious device accepts the initial request but intentionally rejects the RFCOMM/L2CAP protocol connection, returning an error.
- The
rfcommChannelOpenComplete:status:callback fires with an error. The_strongSelfretain is skipped, and the C++ channel object is destroyed. - The C++ destructor calls
closeChannel(sending a disconnect command over the air) and posts the 1-turn keep-alive block. - The malicious device deliberately delays its acknowledgment of the disconnect command.
- The main thread’s keep-alive block finishes executing, and the
BluetoothRfcommChannelDelegateobject is deallocated by ARC. - The attacker sprays the Browser process heap via standard Web API IPCs, replacing the freed delegate memory with controlled data (including a fake
isapointer). - The malicious device finally replies to the disconnect command.
- The
IOBluetoothframework (due to bug FB13705522) fires therfcommChannelClosed: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.