CVE-2026-11633
Overview
Background
- `BluetoothL2capChannelDelegate` / `BluetoothRfcommChannelDelegate`
- Objective-C delegate objects that receive
IOBluetoothcallbacks and forward channel events to their owning C++ wrapper. - `_strongSelf` self-retain
- A delegate-held strong reference to itself that keeps the delegate alive independently of the C++ wrapper, but which is only armed after a channel opens successfully.
- `resetOwner`
- A delegate method that clears the delegate’s back-pointer to the owning C++ object so it stops notifying that object after destruction.
- `dispatch_async` on `dispatch_get_main_queue()`
- A macOS mechanism that defers a block to a later turn of the main run loop, used here to extend an object’s lifetime by one run-loop iteration.
Root Cause Analysis
When a classic Bluetooth channel wrapper is destroyed on macOS, the destructor called resetOwner and closeChannel but left the native channel_ still pointing at delegate_ and immediately allowed delegate_ to be deallocated. The delegate’s _strongSelf self-retain is only armed after a successful open, so if destruction happens during a pending or failed open, the delegate has no lifetime of its own and is freed while the OS still holds an unsafe reference to it. Any IOBluetooth callback already enqueued for that channel then invokes a method on the freed delegate, producing a use-after-free.
The fix clears the native channel’s delegate with [channel_ setDelegate:nil] so no new callbacks target the delegate, and captures delegate_ into a __strong local that a dispatch_async block holds across one main-run-loop turn, guaranteeing already-enqueued callbacks land on a still-live receiver. This closes the window without relying on the self-retain that may never have been armed.
_strongSelf is only set on successful open, leaving a failed/pending-open delegate free to be deallocated while the OS still references it; the fix decouples the delegate from the native channel and artificially extends its lifetime by one run-loop turn so in-flight callbacks stay safe.Attack Path
- Trigger a channel open
An attacker-influenced page or paired peer initiates a Bluetooth Classic L2CAP or RFCOMM channel, causing a
BluetoothL2capChannelDelegateorBluetoothRfcommChannelDelegateto be created without its_strongSelfself-retain yet armed. - Force a failed or pending open
The connection fails or times out, so the delegate never reaches the successful-open state that would arm
_strongSelf. - Destroy the wrapper
The C++ channel wrapper is torn down; its destructor runs
resetOwnerandcloseChannelbut leaveschannel_still bound todelegate_, which is then deallocated. - Deliver an enqueued callback
A previously enqueued
IOBluetoothcallback fires on the main run loop and dereferences the now-freed delegate, causing a use-after-free. - Leverage the freed object By shaping heap reuse of the freed delegate before the callback runs, an attacker can turn the stale dereference into controlled memory corruption.
Impact Assessment
IOBluetooth callback.Files Changed
device/bluetooth/bluetooth_l2cap_channel_mac.mmdevice/bluetooth/bluetooth_rfcomm_channel_mac.mm
Audit Directions
- Conditional self-retain lifetimesAudit any Objective-C delegate whose self-retain (e.g.
_strongSelf) is armed only on a success path, since destruction during a pending or failed operation leaves it with no independent lifetime. - Native callback deregistrationLook for destructors that free a delegate without first clearing the native object’s delegate pointer (
setDelegate:nil), leaving the OS able to invoke callbacks on a freed receiver. - Enqueued async callbacks at teardownReview teardown paths on macOS where
IOBluetoothor other run-loop-driven callbacks may already be queued, and confirm the receiver is kept alive across the run-loop turn that drains them.
Patch
From 3da0d899cbbe299ec3ba36bb842b0b6215915b63 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Fri, 29 May 2026 15:45:54 -0700
Subject: [PATCH] bluetooth: Fix potential UAF in Bluetooth Classic channel delegates on macOS
Clears the native channel's delegate and extends the delegate's lifetime
across one run-loop turn during channel destruction.
On macOS, the classic Bluetooth channel delegates only self-retain on
successful open. On connection failure or timeout, destroying the C++
wrapper deallocates the delegate while the OS still holds an unsafe
reference, causing a UAF during subsequent async callbacks.
Bug: 516963272
Change-Id: Ice878214b875039e6eff06b0b4101a7c4ea433f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884206
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1638776}
---
diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index f1c5b39..1792d66 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -120,7 +120,16 @@
// delegate's reference to this object so the delegate will not notify us
// for events that occur after our destruction.
[delegate_ resetOwner];
+ [channel_ setDelegate:nil];
[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
+ // IOBluetooth callbacks hit a live receiver. See FB13705522.
+ BluetoothL2capChannelDelegate* __strong delegate = delegate_;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ (void)delegate;
+ });
}
// static
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
index 635a30f4..a241175 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
@@ -120,7 +120,16 @@
// delegate's reference to this object so the delegate will not notify us
// for events that occur after our destruction.
[delegate_ resetOwner];
+ [channel_ setDelegate:nil];
[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
+ // IOBluetooth callbacks hit a live receiver. See FB13705522.
+ BluetoothRfcommChannelDelegate* __strong delegate = delegate_;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ (void)delegate;
+ });
}
// static
Original Bug Report
Potential Use-After-Free in BluetoothRfcommChannelDelegate and BluetoothL2capChannelDelegate on macOS
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 (UAF) vulnerability exists in the macOS Bluetooth classic implementation within Chromium. If a connection times out or fails during initialization, the channel delegate is deallocated while the system’s IOBluetooth framework still holds an unsafe_unretained reference to it. This can lead to a browser-process crash or potential arbitrary code execution when the framework attempts to dispatch delayed asynchronous callbacks to the freed delegate.
Affected files:
device/bluetooth/bluetooth_rfcomm_channel_mac.mmdevice/bluetooth/bluetooth_l2cap_channel_mac.mm
Estimated timestamp from git blame: 2024-04-03
Description
An analysis of the macOS-specific Bluetooth classic channel implementation in Chromium reveals a potential Use-After-Free (UAF) vulnerability affecting both BluetoothRfcommChannelDelegate and BluetoothL2capChannelDelegate.
To address a macOS-specific bug (Apple Feedback report FB13705522), the Objective-C delegates attempt to keep themselves alive across asynchronous notifications using a self-retain pattern (_strongSelf = self). However, this self-retain is established only upon a successful channel opening status (error == kIOReturnSuccess):
// device/bluetooth/bluetooth_rfcomm_channel_mac.mm
- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel
status:(IOReturn)error {
CHECK(_rfcommChannel);
if (error == kIOReturnSuccess) {
// Keep the delegate alive until rfcommChannelClosed.
_strongSelf = self;
}
if (_channel) {
_channel->OnChannelOpenComplete(rfcommChannel, error);
}
}
Before a successful open completion callback is received, the only strong reference to the delegate is maintained by the C++ wrapper via its __strong delegate_ member variable. The IOBluetooth framework stores its reference to the delegate as an assign (i.e., unsafe_unretained) property.
If the connection times out (e.g., via the 10-second channel opening timer in BluetoothSocketMac::OnChannelOpeningTimeout) or fails with an error:
ReleaseChannel()is called, destroying the C++ channel wrapper.- The C++ destructor (
~BluetoothRfcommChannelMacor~BluetoothL2capChannelMac) is executed. It resets the delegate’s back-pointer and closes the channel, but does not clear the delegate registration from the native channel:
BluetoothRfcommChannelMac::~BluetoothRfcommChannelMac() {
[delegate_ resetOwner];
[channel_ closeChannel]; // Does NOT call [channel_ setDelegate:nil]
}
- Because
_strongSelfwas never set (the connection never succeeded), releasing the C++ wrapper’sdelegate_reference reduces the delegate’s ARC retain count to zero, causing it to be immediately deallocated. - Meanwhile, the
IOBluetoothframework retains itsunsafe_unretainedpointer to the deallocated delegate. When subsequent asynchronous callbacks or close notifications are dispatched to the delegate, it results in a Use-After-Free (UAF) condition duringobjc_msgSendon the main thread of the browser process.
An identical potential vulnerability pattern exists in the L2CAP implementation within device/bluetooth/bluetooth_l2cap_channel_mac.mm.
Suggested Attack Steps
An attacker could theoretically trigger this issue by doing the following (note: our analysis is static and we have not executed a physical proof of concept):
- The user is induced to connect to a paired, malicious classic Bluetooth device controlled by the attacker (e.g., via a Web Serial or Web Bluetooth prompt).
- The browser process initiates the connection, invoking
OpenAsync, which registers the delegate with the macOSIOBluetoothframework. - The attacker’s device purposefully stalls the handshake process, forcing the browser’s connection timer to expire, or actively rejects the connection with an error status.
- The browser process handles the failure/timeout by resetting the channel, deallocating the delegate.
- The system framework asynchronously processes the aborted connection state and dispatches delayed delegate selectors (like
rfcommChannelClosed:orrfcommChannelOpenComplete:status:) to the freed delegate’s memory address, resulting in a UAF.
Proposed Remediation
To safely prevent the framework from calling back into the deallocated delegate, the native channel’s delegate reference must be explicitly removed in the C++ destructor before the delegate is released:
BluetoothRfcommChannelMac::~BluetoothRfcommChannelMac() {
[delegate_ resetOwner];
[channel_ setDelegate:nil]; // Explicitly clear the delegate
[channel_ closeChannel];
}
Apply the equivalent correction to BluetoothL2capChannelMac::~BluetoothL2capChannelMac().
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.