CVE-2026-14032
Overview
Background
- `IOBluetooth` user notification
- an Apple macOS framework callback that fires on the main run loop when a Bluetooth RFCOMM/L2CAP channel event occurs, registered via an
IOBluetoothUserNotificationobject. - `unsafe_unretained`
- an Objective-C pointer attribute that holds no ownership reference and is not zeroed on deallocation, so it dangles if the pointee is freed.
- Listener delegate
- the Objective-C helper objects (
_rfcommNewChannelNotification/_l2capNewChannelNotificationowners) that receiverfcommChannelOpened:/l2capChannelOpened:callbacks and forward them to the owning C++BluetoothSocketMacvia the_socketback-pointer. - FB13705522
- the referenced Apple framework bug whereby
IOBluetoothmay dispatch a callback to a listener even after that listener has calledunregister.
Root Cause Analysis
When a BluetoothSocketMac listener was torn down, the code set rfcomm_connection_listener_ = nil / l2cap_connection_listener_ = nil (relying only on unregister in dealloc), but because of the Apple framework bug (FB13705522) IOBluetooth retained an unsafe_unretained pointer to the listener and could still dispatch an already-enqueued rfcommChannelOpened: or l2capChannelOpened: callback to the now-deallocated object. The callback handlers dereferenced the _socket back-pointer with no validity check, so a late notification firing after deallocation produced a use-after-free in the browser process.
The fix adds a stopListening method that explicitly unregisters, nulls _socket, and then posts a keep-alive block referencing a __strong strongSelf onto the main queue so the listener object outlives any notifications still queued ahead of that block; the callback handlers additionally early-return when _socket is null, turning any surviving late callback into a harmless no-op instead of a dangling dereference.
_socket = nullptr plus a null guard in each *ChannelOpened: handler) and extends the object’s lifetime via a keep-alive block so late, already-enqueued notifications land on a live, inert object.Attack Path
- Establish a listening socket
A caller sets up an RFCOMM or L2CAP listening
BluetoothSocketMac, which registers_rfcommNewChannelNotification/_l2capNewChannelNotificationwithIOBluetooth. (inferred) - Trigger teardown with a callback in flight
The socket is closed so the listener is released while
IOBluetooth, due toFB13705522, still has an enqueued channel-opened notification targeting the freed listener via itsunsafe_unretainedpointer. - Late callback dereferences freed object
IOBluetoothdispatchesrfcommChannelOpened:/l2capChannelOpened:to the deallocated listener, which reads the dangling_socketback-pointer, causing the use-after-free. - Exploitation
Reclaiming the freed listener allocation before the late callback fires could allow controlled memory to be interpreted as the listener/
_socket, potentially leading to memory corruption in the browser process (inferred).
Impact Assessment
IOBluetooth notification. The advisory classifies this as a use-after-free in Bluetooth; any escalation to controlled memory corruption or code execution is (inferred) and not demonstrated by the patch. No exploited-in-the-wild status is stated.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/bluetooth_socket_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_socket_mac.mm
Audit Directions
- OS callback outliving its targetAudit any place a platform framework holds an
unsafe_unretained/non-owning pointer to a delegate that can be destroyed while callbacks are still enqueued, and require a keep-alive plus back-pointer nulling. - Missing null-guard in async delegate handlersReview callback handlers that dereference a back-pointer (like
_socket) without first checking it was invalidated during teardown. - `unregister`-only teardownLook for teardown paths that assume
unregistersynchronously stops all callbacks; verify against known framework bugs where late dispatches still occur.
Patch
From 375096a7dd434f3fa2a5cff6a8c71c3f3827da83 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Fri, 22 May 2026 20:15:45 -0700
Subject: [PATCH] bluetooth: Fix potential browser-process UAF in socket listeners
Due to an Apple framework bug (FB13705522), IOBluetooth holds an
unsafe_unretained pointer and may dispatch late callbacks to freed
listeners after unregistration.
This CL fixes the UAF by clearing back-pointers and posting a keep-alive
block to the main queue, ensuring the listener safely outlives any
pending OS notifications.
Bug: 495783474
Change-Id: I79497e722159320158bb411fcb15f99066d7ca0e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7833747
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1635359}
---
diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index 60b5df97..2348f81a 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -122,6 +122,7 @@
channelID:(BluetoothRFCOMMChannelID)channelID;
- (void)rfcommChannelOpened:(IOBluetoothUserNotification*)notification
channel:(IOBluetoothRFCOMMChannel*)rfcommChannel;
+- (void)stopListening;
@end
@@ -150,8 +151,26 @@
[_rfcommNewChannelNotification unregister];
}
+- (void)stopListening {
+ [_rfcommNewChannelNotification unregister];
+ _socket = nullptr;
+
+ // Keep self alive for a brief period to allow any already-enqueued
+ // notifications on the main run loop to fire safely (and become no-ops
+ // since _socket is now null) rather than hitting a deallocated object.
+ // See FB13705522.
+ __strong auto strongSelf = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ (void)strongSelf;
+ });
+}
+
- (void)rfcommChannelOpened:(IOBluetoothUserNotification*)notification
channel:(IOBluetoothRFCOMMChannel*)rfcommChannel {
+ if (!_socket) {
+ return;
+ }
+
if (notification != _rfcommNewChannelNotification) {
// This case is reachable if there are pre-existing RFCOMM channels open at
// the time that the listener is created. In that case, each existing
@@ -184,6 +203,7 @@
psm:(BluetoothL2CAPPSM)psm;
- (void)l2capChannelOpened:(IOBluetoothUserNotification*)notification
channel:(IOBluetoothL2CAPChannel*)l2capChannel;
+- (void)stopListening;
@end
@@ -212,8 +232,26 @@
[_l2capNewChannelNotification unregister];
}
+- (void)stopListening {
+ [_l2capNewChannelNotification unregister];
+ _socket = nullptr;
+
+ // Keep self alive for a brief period to allow any already-enqueued
+ // notifications on the main run loop to fire safely (and become no-ops
+ // since _socket is now null) rather than hitting a deallocated object.
+ // See FB13705522.
+ __strong auto strongSelf = self;
+ dispatch_async(dispatch_get_main_queue(), ^{
+ (void)strongSelf;
+ });
+}
+
- (void)l2capChannelOpened:(IOBluetoothUserNotification*)notification
channel:(IOBluetoothL2CAPChannel*)l2capChannel {
+ if (!_socket) {
+ return;
+ }
+
if (notification != _l2capNewChannelNotification) {
// This case is reachable if there are pre-existing L2CAP channels open at
// the time that the listener is created. In that case, each existing
@@ -1001,7 +1039,9 @@
[service_record_ removeServiceRecord];
service_record_ = nil;
+ [rfcomm_connection_listener_ stopListening];
rfcomm_connection_listener_ = nil;
+ [l2cap_connection_listener_ stopListening];
l2cap_connection_listener_ = nil;
// Destroying the listener above prevents the callback delegate from being