CVE-2026-9881
Overview
Background
- `IOBluetoothL2CAPChannel`
- the macOS IOBluetooth framework object representing an L2CAP channel; it retains a delegate to which it dispatches connection events.
- `BluetoothL2capChannelDelegate`
- Chrome’s Objective-C delegate that receives L2CAP channel callbacks (e.g.,
l2capChannelOpenComplete:status:) on behalf of aBluetoothL2capChannelMac. - Delegate unregistration
- to safely tear down, a delegate must clear itself from the native channel (via the channel it holds in
_l2capChannel) so the OS stops dispatching events to a freed object. - Outgoing L2CAP connection
- an L2CAP channel opened by Chrome via
openL2CAPChannelAsync:withPSM:delegate:, which returns the native channel object to the caller.
Root Cause Analysis
For outgoing L2CAP connections, BluetoothL2capChannelDelegate never received a pointer to the native IOBluetoothL2CAPChannel it was serving, so its _l2capChannel member stayed null. During connection teardown the delegate therefore could not unregister itself from the native channel, leaving the macOS Bluetooth framework holding a dangling reference to the delegate after it was deallocated. If the framework subsequently dispatched an event (such as a completion or close callback) to that freed delegate, the dangling pointer was used, producing a use-after-free.
The fix adds a setL2capChannel: setter and calls it in OpenAsync immediately after a successful openL2CAPChannelAsync: so _l2capChannel is populated, enabling correct unregistration; a CHECK(_l2capChannel) in l2capChannelOpenComplete:status: and a CHECK(!_l2capChannel) in the setter enforce the invariant. This mirrors the already-correct RFCOMM implementation fixed earlier in bluetooth_rfcomm_channel_mac.mm.
setL2capChannel: right after the channel is opened, matching the correct RFCOMM pattern.Attack Path
- Initiate an outgoing L2CAP connection
Code (reachable via the Bluetooth stack) opens an outgoing L2CAP channel;
OpenAsynccallsopenL2CAPChannelAsync:withPSM:delegate:and stores the native channel inchannel->channel_, but pre-patch never told the delegate about it, leaving_l2capChannelnull. - Tear down the channel
During connection teardown the delegate attempts to clean up, but without
_l2capChannelit cannot unregister itself from the nativeIOBluetoothL2CAPChannel. - Delegate is deallocated while still registered The delegate object is freed while the macOS framework still holds a pointer to it, creating a dangling reference.
- Event dispatched to freed delegate A subsequent Bluetooth event dispatched by the OS to the dangling delegate pointer triggers the use-after-free (inferred as to which specific event fires).
- Memory corruption (inferred) Use of the freed delegate can corrupt memory and potentially be leveraged further, though the patch does not demonstrate a concrete read/write primitive or code execution.
Impact Assessment
BluetoothL2capChannelDelegate, yielding a use-after-free in the browser/device process that handles Bluetooth. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.0. Any escalation to control-flow hijack or code execution is (inferred). Preconditions include the ability to trigger an outgoing L2CAP connection and its teardown on macOS.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/bluetooth_l2cap_channel_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_l2cap_channel_mac.mm
Audit Directions
- Delegate/native-object back-referencesAudit every macOS IOBluetooth (and similar OS-callback) delegate to confirm it stores a reference to the native object it must unregister from, for both the incoming/listening and outgoing/connecting paths.
- Asymmetric fixes across sibling channel typesSince RFCOMM was fixed earlier but L2CAP was missed, look for other paired implementations (L2CAP vs RFCOMM, incoming vs outgoing) where a correctness fix was applied to one variant but not its twin.
- Teardown-time unregistration invariantsReview teardown/
deallocpaths of OS-retained delegates to ensure the framework’s pointer is cleared before the delegate is freed, and considerCHECKs that enforce the required reference is present.
Patch
From 718de6b8e9b77d656b340d708f79768098f576cc Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 22 Apr 2026 10:19:12 -0700
Subject: [PATCH] [macOS Bluetooth] Fix UAF in L2CAP channel teardown
For outgoing L2CAP connections, the BluetoothL2capChannelDelegate fails
to properly unregister itself from the native IOBluetoothL2CAPChannel
during connection teardown because it lacks a reference to the native
channel object. This leads to a dangling pointer within the macOS
Bluetooth framework, which can be triggered if subsequent events are
dispatched to the deallocated delegate.
This CL fixes the issue by adding a setL2capChannel: setter to the
delegate and calling it after the native channel is created in
OpenAsync. This matches the existing (and correct) implementation
pattern used for RFCOMM channels in bluetooth_rfcomm_channel_mac.mm,
which was fixed in https://crrev.com/c/5750170.
Fixed: 505140741
Change-Id: I18c0ccb6a145856d3ebeb1d62983a0c5518f3adc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785775
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1618950}
---
diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index 5bcbde96..aafa433 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -28,6 +28,7 @@
- (instancetype)initWithChannel:(device::BluetoothL2capChannelMac*)channel
l2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;
+- (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;
@end
@@ -45,6 +46,7 @@
- (void)l2capChannelOpenComplete:(IOBluetoothL2CAPChannel*)l2capChannel
status:(IOReturn)error {
+ CHECK(_l2capChannel);
if (error == kIOReturnSuccess) {
// Keep the delegate alive until l2capChannelClosed.
_strongSelf = self;
@@ -92,6 +94,11 @@
_channel = nullptr;
}
+- (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel {
+ CHECK(!_l2capChannel);
+ _l2capChannel = l2capChannel;
+}
+
@end
namespace device {
@@ -128,10 +135,12 @@
*status = [device openL2CAPChannelAsync:&l2cap_channel
withPSM:psm
delegate:channel->delegate_];
- if (*status == kIOReturnSuccess)
+ if (*status == kIOReturnSuccess) {
channel->channel_ = l2cap_channel;
- else
+ [channel->delegate_ setL2capChannel:l2cap_channel];
+ } else {
channel.reset();
+ }
return channel;
}
Original Bug Report
Potential Use-After-Free in BluetoothL2capChannelDelegate on macOS
Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A potential Use-After-Free vulnerability exists in the macOS Bluetooth L2CAP implementation due to a failure to clear the native IOBluetoothL2CAPChannel delegate when closing outgoing connections. The delegate object is deallocated while the native OS channel still holds a dangling pointer to it, which can be triggered if late events are dispatched.
Affected files:
device/bluetooth/bluetooth_l2cap_channel_mac.mm
Estimated timestamp from git blame: 2024-04-03
Summary
A potential Use-After-Free (UAF) vulnerability exists in the macOS Bluetooth L2CAP implementation in Chromium. For outgoing L2CAP connections, the BluetoothL2capChannelDelegate fails to properly unregister itself from the native IOBluetoothL2CAPChannel during connection teardown. This leads to a dangling pointer within the macOS Bluetooth framework, which can be triggered if subsequent events (such as late data arrival) are dispatched to the deallocated delegate.
Technical Details
In device/bluetooth/bluetooth_l2cap_channel_mac.mm, when an outgoing L2CAP connection is initiated via BluetoothL2capChannelMac::OpenAsync, the following sequence occurs:
- A
BluetoothL2capChannelMacwrapper object is constructed with its internalchannel_member explicitly set tonil(line 124). - The constructor immediately calls
SetSocket(), which instantiates aBluetoothL2capChannelDelegate. The delegate is initialized usingchannel_, meaning its internal_l2capChannelivar is set tonil(lines 147-148). [device openL2CAPChannelAsync:...]is called, which creates the nativeIOBluetoothL2CAPChanneland registers the delegate with it (line 128).BluetoothL2capChannelMac::channel_is updated with the new native channel object (line 132). However, the delegate’s internal_l2capChannelivar is never updated.
When the connection is later closed, the macOS Bluetooth stack invokes l2capChannelClosed: on the delegate:
- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel {
[_l2capChannel setDelegate:nil]; // This is a no-op because _l2capChannel is nil
// ...
_strongSelf = nil; // This releases the delegate object
}
Because _l2capChannel is nil for outgoing connections, the delegate fails to call setDelegate:nil on the actual native channel object. When _strongSelf is cleared, the delegate is deallocated via ARC. However, the native IOBluetoothL2CAPChannel object inside the OS framework still holds an unsafe_unretained (raw) pointer to this deallocated delegate.
This implementation is inconsistent with the RFCOMM implementation in device/bluetooth/bluetooth_rfcomm_channel_mac.mm, which correctly provides a setter (setRfcommChannel:) to update the delegate’s channel reference during OpenAsync.
Potential Exploitation
(Note: These are suggested steps based on static analysis; a working proof-of-concept has not been executed.)
This UAF occurs in the Browser process. Furthermore, Objective-C objects are allocated using the system allocator and are not protected by MiraclePtr/PartitionAlloc.
An attacker could potentially trigger this by:
- Creating a malicious Web Extension that uses the
chrome.bluetoothSocketAPI, or operating a malicious Bluetooth peripheral that Chrome connects to. - Establishing an L2CAP connection.
- Racing a connection teardown (e.g., closing the socket) against incoming data packets from the peripheral.
- If a packet arrives and is processed by the OS after
l2capChannelClosed:has released the delegate, but before the native channel is fully destroyed, the OS will attempt to dispatch an Objective-C message (e.g.,l2capChannelData:) to the freed memory. - Using heap spraying techniques, an attacker could replace the freed Objective-C object to gain control of the instruction pointer, leading to arbitrary code execution and a sandbox escape.
Suggested Fix
Similar to the RFCOMM implementation, update BluetoothL2capChannelDelegate to include a setter method for the channel, and call it after a successful connection:
- In
BluetoothL2capChannelDelegate, add- (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;. - In
BluetoothL2capChannelMac::OpenAsync, update the delegate afteropenL2CAPChannelAsyncsucceeds:if (*status == kIOReturnSuccess) { channel->channel_ = l2cap_channel; [channel->delegate_ setL2capChannel:l2cap_channel]; } else { // ...
Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.