Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Sub-componentBluetooth
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516987814
Fix commit75b964de97d2 (chromium/src) +23/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-05-29 (public git)
Disclosed2026-06-08
Patch gap10 days — fix public in source before the advisory

Background

`IOBluetoothUserNotification`
a macOS IOBluetooth callback registration whose target Objective-C listener is invoked when a device connects or disconnects.
`dispatch_get_main_queue`
the macOS main run loop queue onto which IOBluetooth notification callbacks are enqueued and later fired.
`_ui_task_runner`
the listener’s pointer to the C++ owner’s task runner, used to PostTask connect/disconnect notifications back to BluetoothAdapterMac.
UAF (Use-After-Free)
a bug where code dereferences an object after its backing memory has been freed, yielding memory corruption or hijackable control flow.

Root Cause Analysis

The macOS Bluetooth listener objects registered via IOBluetoothUserNotification outlive their C++ owners because notification callbacks are enqueued on the main run loop and can fire after the owner is torn down. When the C++ owner is destroyed and stopListening unregisters the notification, an already-enqueued callback such as deviceConnected:device: could still fire and dereference the now-freed _ui_task_runner (or, in the classic-device listener, a freed _device), violating the invariant that a callback never touches owner state after teardown.

The fix makes deviceConnected:device: bail out early when !_ui_task_runner, nulls out _ui_task_runner and _adapter in stopListening, and — following the FB13705522 workaround — keeps self alive briefly by capturing a __strong strongSelf inside a block dispatched to dispatch_get_main_queue. This guarantees the listener object is not deallocated until any pending main-run-loop notifications have drained, and those notifications become harmless no-ops because the state pointers are already null.

Key insight
The core mistake was allowing an Objective-C notification listener to be deallocated while callbacks referencing its freed owner state were still queued on the main run loop; the fix defers the listener’s deallocation past those queued callbacks and neutralizes them by nulling _ui_task_runner/_device so they become no-ops.

Attack Path

  1. Establish a Bluetooth listener The renderer/site drives the Web Bluetooth stack so a deviceConnected or deviceDisconnect IOBluetoothUserNotification is registered against the macOS listener object.
  2. Trigger a queued notification An attacker-influenced connect/disconnect event causes IOBluetooth to enqueue a callback on the main run loop while the callback still references live owner state.
  3. Destroy the owner The C++ owner (BluetoothAdapterMac or the classic device) is torn down and stopListening unregisters the notification, but the enqueued callback remains pending and the listener would be freed.
  4. Fire the stale callback The queued deviceConnected:device: callback runs on the main run loop and dereferences the freed _ui_task_runner (or freed _device), producing a use-after-free.
  5. Exploit the freed access The attacker leverages the dangling dereference and PostTask on freed memory to corrupt state or steer control flow in the browser process.

Impact Assessment

An attacker who can influence Bluetooth connect/disconnect timing gains a use-after-free in the browser process on macOS, where the Bluetooth adapter and device listeners run. Exploitation requires the platform-specific race in which an IOBluetooth notification is enqueued on the main run loop and fires after the C++ owner is destroyed. Successful exploitation can lead to memory corruption and potentially to code execution or sandbox-escape-relevant control in the privileged browser process.

Changed Functions

FunctionChangeNotes
if
device/bluetooth/bluetooth_adapter_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_adapter_mac.mm
  • device/bluetooth/bluetooth_classic_device_mac.mm

Audit Directions

  • Objective-C listeners over run-loop callbacks
    Audit any IOBluetoothUserNotification or delegate/listener whose callbacks are enqueued on dispatch_get_main_queue and can fire after the owning C++ object is destroyed.
  • Teardown without lifetime extension
    Review stopListening-style teardown paths that unregister notifications but do not keep the listener alive long enough for already-enqueued callbacks to drain.
  • Nullable-state no-op guards
    Check that every callback which reads owner state (like _ui_task_runner or _device) null-checks that state so a post-teardown invocation degrades to a no-op rather than a freed-memory dereference.
From 75b964de97d2781d1f8329f2fb754862dc212c63 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Fri, 29 May 2026 13:59:07 -0700
Subject: [PATCH] bluetooth: Prevent UAF in macOS Bluetooth listeners

Implement the FB13705522 workaround for BluetoothDevicesConnectListener
and BluetoothDeviceDisconnectListener on macOS. This ensures that these
listeners stay alive past any already-enqueued notifications on the main
run loop, preventing Use-After-Free (UAF) when the C++ owner is
destroyed.

Bug: 516987814
Change-Id: I542200f213f499370dc89a7cf68dde9fc6299285
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7884382
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1638719}
---

diff --git a/device/bluetooth/bluetooth_adapter_mac.mm b/device/bluetooth/bluetooth_adapter_mac.mm
index f300e8d3..b71deca 100644
--- a/device/bluetooth/bluetooth_adapter_mac.mm
+++ b/device/bluetooth/bluetooth_adapter_mac.mm
@@ -100,6 +100,9 @@
 
 - (void)deviceConnected:(IOBluetoothUserNotification*)notification
                  device:(IOBluetoothDevice*)device {
+  if (!_ui_task_runner) {
+    return;
+  }
   _ui_task_runner->PostTask(
       FROM_HERE,
       base::BindOnce(&device::BluetoothAdapterMac::OnConnectNotification,
@@ -108,6 +111,17 @@
 
 - (void)stopListening {
   [_connectNotification unregister];
+  _ui_task_runner = nullptr;
+  _adapter = 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 _ui_task_runner is now null) rather than hitting a deallocated
+  // object. See FB13705522.
+  __strong auto strongSelf = self;
+  dispatch_async(dispatch_get_main_queue(), ^{
+    (void)strongSelf;
+  });
 }
 
 @end
diff --git a/device/bluetooth/bluetooth_classic_device_mac.mm b/device/bluetooth/bluetooth_classic_device_mac.mm
index 659328c..99544514 100644
--- a/device/bluetooth/bluetooth_classic_device_mac.mm
+++ b/device/bluetooth/bluetooth_classic_device_mac.mm
@@ -87,6 +87,15 @@
   // destruction will see a null |_device| and become a no-op instead of
   // dereferencing a freed object.
   _device = 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 _device is now null) rather than hitting a deallocated object.
+  // See FB13705522.
+  __strong auto strongSelf = self;
+  dispatch_async(dispatch_get_main_queue(), ^{
+    (void)strongSelf;
+  });
 }
 
 @end
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential UAF in BluetoothDevicesConnectListener on macOS due to uncancelled enqueued notifications

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 browser process because BluetoothDevicesConnectListener does not safely prolong its lifetime when stopListening is called. Due to a known macOS behavior where unregistering does not cancel already-enqueued run loop events, a late notification can execute on a deallocated listener. This could potentially allow an attacker to trigger code execution in the unsandboxed browser process.

Affected files:

  • device/bluetooth/bluetooth_adapter_mac.mm
  • device/bluetooth/bluetooth_adapter_mac.h

Estimated timestamp from git blame: 2024-06-07

Technical Details

In device/bluetooth/bluetooth_adapter_mac.mm, the helper class BluetoothDevicesConnectListener registers itself with the macOS IOBluetooth framework to receive device connection notifications. It is owned solely via a strong reference (connect_listener_) by BluetoothAdapterMac.

As documented in Apple Feedback report FB13705522 (and handled in sibling classes like BluetoothSocketMac), calling unregister on an IOBluetoothUserNotification successfully stops future notifications from being registered but does not cancel notifications that the system has already enqueued on the main run loop.

When BluetoothAdapterMac is destroyed, its destructor calls [connect_listener_ stopListening] and sets connect_listener_ = nil. This synchronously deallocates the BluetoothDevicesConnectListener instance. If a connection notification was already enqueued on the main run loop, the loop will subsequently attempt to deliver it to the deallocated listener address via objc_msgSend, causing a Use-After-Free (UAF) in the unsandboxed browser process.

Potential Trigger Path

Note: The following steps are theoretical and represent a potential attack vector, as our tooling cannot currently execute or run proof-of-concept code.

  1. Listener Registration: A web page in a sandboxed renderer requests a Web Bluetooth operation (e.g., navigator.bluetooth.requestDevice), which prompts the browser process to initialize BluetoothAdapterMac. This instantiates BluetoothDevicesConnectListener, registering it for connection notifications.
  2. Notification Enqueue: A proximate Bluetooth Classic device initiates a connection, causing IOBluetooth to queue a notification on the main run loop targeting the listener.
  3. Adapter Destruction: The renderer frame is closed or navigated, destroying WebBluetoothServiceImpl. This releases the last reference to the ref-counted BluetoothAdapterMac instance, synchronously executing its destructor.
  4. Listener Deallocation: ~BluetoothAdapterMac calls [connect_listener_ stopListening] and sets the pointer to nil, immediately reclaiming the listener’s memory.
  5. Use-After-Free: When the main run loop drains and processes the pending notification, it attempts to dispatch the event to the deallocated listener, resulting in a UAF.

Sibling Workaround Pattern

Other macOS Bluetooth classes in Chrome (such as BluetoothSocketMac in device/bluetooth/bluetooth_socket_mac.mm) proactively mitigate this OS-level limitation by clearing their back-pointers and asynchronously dispatching a block to the main queue that holds a strong reference to self to safely outlive any enqueued messages:

- (void)stopListening {
  [_rfcommNewChannelNotification unregister];
  _socket = nullptr;
  __strong auto strongSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    (void)strongSelf;
  });
}

Proposed Fix

Modify BluetoothDevicesConnectListener::stopListening in device/bluetooth/bluetooth_adapter_mac.mm to match this safety pattern:

- (void)stopListening {
  [_connectNotification unregister];
  _adapter.reset();
  __strong auto strongSelf = self;
  dispatch_async(dispatch_get_main_queue(), ^{
    (void)strongSelf;
  });
}

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.

View on issue tracker