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 <[email protected]> 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 <[email protected]> Commit-Queue: Alvin Ji <[email protected]> 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