CVE-2026-11699
Overview
Background
- `BluetoothDiscoveryManagerMacClassic`
- the C++ object that drives classic (BR/EDR) Bluetooth device inquiry on macOS by owning an
IOBluetoothDeviceInquiryand its Objective-C delegate. - `BluetoothDeviceInquiryDelegate`
- the Objective-C delegate object that receives asynchronous inquiry callbacks from the OS and forwards them to the C++ manager through a back-pointer named
_manager. - `assign` delegate property
- an Objective-C property attribute (like a raw C pointer) that holds no strong reference and is not nil-ed automatically, so a stale delegate pointer can be dereferenced after free.
- `dispatch_async(dispatch_get_main_queue(), ...)`
- a macOS API that enqueues a block to run on a later turn of the main run loop, used here to defer object destruction by one iteration.
Root Cause Analysis
When a BluetoothDiscoveryManagerMacClassic was destroyed, its destructor dropped the manager’s strong reference to BluetoothDeviceInquiryDelegate while an asynchronous inquiry callback could already be enqueued on the main run loop. Because the delegate keeps a plain back-pointer (_manager) to the manager and the IOBluetoothDeviceInquiry delegate property is declared assign rather than weak, a callback firing after destruction would either invoke a method on a freed delegate or, through the delegate, call into the freed manager via _manager->.... The old destructor only reset inquiry_.delegate = nil, which does not cancel callbacks already queued nor prevent the delegate’s own _manager dereference.
The fix stops the inquiry, calls resetOwner to null out _manager, adds if (!_manager) return; guards to every delegate callback so any late callback becomes a no-op, and extends the delegate’s lifetime by one run-loop turn via dispatch_async so an already-enqueued callback fires against a still-live (but neutered) delegate instead of freed memory.
assign) pointer; the fix neutralizes the back-pointer (resetOwner + null checks) and keeps the delegate alive for one extra run-loop turn so pending callbacks land safely as no-ops.Attack Path
- Start an inquiry
Web content (via Web Bluetooth) or a browser code path triggers classic device inquiry, creating a
BluetoothDiscoveryManagerMacClassicwith itsIOBluetoothDeviceInquiryand delegate. - Queue an async callback
The OS enqueues an inquiry callback (
deviceInquiryStarted:,deviceInquiryDeviceFound:device:, ordeviceInquiryComplete:error:aborted:) onto the main run loop. - Destroy the manager
Before that callback runs, the manager is destroyed; the pre-patch destructor drops the strong reference to the delegate and only nils
inquiry_.delegate, freeing the delegate while the callback is still pending (inferred timing/race). - Callback fires on freed memory
The enqueued callback executes on the now-deallocated delegate, and/or the delegate dereferences its dangling
_managerback-pointer, producing a use-after-free. - Corrupt reclaimed memory If the freed delegate/manager storage has been reallocated, the callback reads or writes attacker-influenced memory, potentially leading to memory corruption (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/bluetooth_discovery_manager_mac.mm |
modified |
Files Changed
device/bluetooth/bluetooth_discovery_manager_mac.mm
Audit Directions
- `assign`/unsafe_unretained delegatesAudit Objective-C code that sets a delegate on system objects using
assign(notweak) semantics; ensure the delegate is invalidated and outlives any in-flight callbacks when its owner is destroyed. - Async callbacks vs. synchronous teardownLook for objects that own delegates or callbacks with raw back-pointers and are destroyed synchronously while OS-driven callbacks may still be queued; verify the back-pointer is nulled and the callback target’s lifetime is extended past pending dispatches.
- Destructor ordering with pending workReview destructors that merely detach a delegate/observer without cancelling or draining already-enqueued run-loop or dispatch-queue work that could reference the object.
Patch
From 673a5aee77ad12e118c8edac73359acdeb7f491c Mon Sep 17 00:00:00 2001 From: Alvin Ji <[email protected]> Date: Tue, 02 Jun 2026 13:04:49 -0700 Subject: [PATCH] Fix UAF in BluetoothDeviceInquiryDelegate on macOS On destruction, BluetoothDiscoveryManagerMacClassic releases its strong reference to BluetoothDeviceInquiryDelegate while an asynchronous callback may be enqueued on the main run loop. The subsequent callback execution on a deallocated receiver could lead to memory corruption. This CL fixes the issue by: 1. Adding a `resetOwner` method to `BluetoothDeviceInquiryDelegate` to clear its weak pointer to the manager. 2. Checking if the manager is still valid in the delegate callbacks. 3. In `~BluetoothDiscoveryManagerMacClassic`, stopping the inquiry, resetting the delegate's owner, and keeping the delegate alive for one turn of the main run loop using `dispatch_async`. Bug: 518237527 Change-Id: I34d77372651e1996fcf9e6b709eaad1cf44540e6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7889579 Reviewed-by: Matt Reynolds <[email protected]> Commit-Queue: Alvin Ji <[email protected]> Cr-Commit-Position: refs/heads/main@{#1640412} --- diff --git a/device/bluetooth/bluetooth_discovery_manager_mac.mm b/device/bluetooth/bluetooth_discovery_manager_mac.mm index bccb75c..42a26ab 100644 --- a/device/bluetooth/bluetooth_discovery_manager_mac.mm +++ b/device/bluetooth/bluetooth_discovery_manager_mac.mm @@ -26,6 +26,8 @@ - (instancetype)initWithManager: (device::BluetoothDiscoveryManagerMacClassic*)manager; +- (void)resetOwner; + @end namespace device { @@ -48,10 +50,21 @@ const BluetoothDiscoveryManagerMacClassic&) = delete; ~BluetoothDiscoveryManagerMacClassic() override { + [inquiry_ stop]; + [inquiry_delegate_ resetOwner]; // IOBluetoothDeviceInquiry's delegate property is configured as "assign" // rather than "weak". If it is not manually reset then our delegate could be // accessed after we drop our strong reference and the object is freed. inquiry_.delegate = nil; + + // Keep the delegate alive for one more turn of the main run loop to allow + // any already-enqueued callbacks to fire safely (and become no-ops since + // the owner has been reset to nullptr) rather than hitting a deallocated + // object. See FB13705522. + BluetoothDeviceInquiryDelegate* __strong delegate = inquiry_delegate_; + dispatch_async(dispatch_get_main_queue(), ^{ + (void)delegate; + }); } // BluetoothDiscoveryManagerMac override. @@ -213,18 +226,31 @@ return self; } +- (void)resetOwner { + _manager = nullptr; +} + - (void)deviceInquiryStarted:(IOBluetoothDeviceInquiry*)sender { + if (!_manager) { + return; + } _manager->DeviceInquiryStarted(sender); } - (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender device:(IOBluetoothDevice*)device { + if (!_manager) { + return; + } _manager->DeviceFound(sender, device); } - (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry*)sender error:(IOReturn)error aborted:(BOOL)aborted { + if (!_manager) { + return; + } _manager->DeviceInquiryComplete(sender, error, aborted); }
Original Bug Report
Potential macOS browser-process Use-After-Free in BluetoothDeviceInquiryDelegate
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 due to a missing callback keep-alive in BluetoothDiscoveryManagerMacClassic. On destruction, the manager releases its strong reference to BluetoothDeviceInquiryDelegate while an asynchronous callback may be enqueued on the main run loop. The subsequent callback execution on a deallocated receiver could lead to memory corruption in the unsandboxed browser process.
Affected files:
device/bluetooth/bluetooth_discovery_manager_mac.mm
Estimated timestamp from git blame: 2014-06-13
Description
A potential Use-After-Free (UAF) vulnerability exists in device/bluetooth/bluetooth_discovery_manager_mac.mm due to a missing keep-alive mechanism for BluetoothDeviceInquiryDelegate during the destruction of BluetoothDiscoveryManagerMacClassic.
In macOS Chromium Bluetooth code, several classes use Objective-C delegates to receive asynchronous callbacks from system frameworks (such as IOBluetooth). Due to a known macOS issue (referenced internally as FB13705522), these system frameworks can enqueue asynchronous callback messages on the main thread’s run loop and capture the delegate’s raw address at enqueue time. If the owner is destroyed before these enqueued callbacks run, they will target a deallocated delegate receiver.
To mitigate this, a keep-alive campaign was implemented across sibling IOBluetooth delegate classes (e.g., in BluetoothRfcommChannelMac, BluetoothL2capChannelMac, BluetoothSocketMac, etc.) using dispatch_async to keep the delegate alive for an extra turn of the main run loop. However, BluetoothDiscoveryManagerMacClassic was missed.
Potential Root Cause
In device/bluetooth/bluetooth_discovery_manager_mac.mm, the destructor ~BluetoothDiscoveryManagerMacClassic only resets the delegate property of the inquiry object:
~BluetoothDiscoveryManagerMacClassic() override {
inquiry_.delegate = nil;
}
Directly after this destructor body completes, the strong reference inquiry_delegate_ is released, and the BluetoothDeviceInquiryDelegate is deallocated. If an asynchronous callback was already enqueued by IOBluetooth, it will invoke objc_msgSend on the deallocated delegate pointer.
Potential Step-by-Step Trigger Sequence
Note: The following steps are theoretical and represent a potential path, as our tooling does not currently have the capability to execute code or verify exploitation dynamically:
- A page or extension triggers Bluetooth classic scanning (e.g., via Web Serial or
chrome.bluetooth.startDiscovery()). BluetoothAdapterMac::StartScanWithFilterlazily initializesclassic_discovery_manager_(an instance ofBluetoothDiscoveryManagerMacClassic) and starts the device inquiry via[inquiry_ start].- A nearby physical Bluetooth device responds, causing the macOS framework to enqueue an asynchronous delegate callback (e.g.,
-deviceInquiryDeviceFound:device:) on the main run loop. - The user closes the tab or navigates away, destroying the adapter and destroying
classic_discovery_manager_via its unique pointer. ~BluetoothDiscoveryManagerMacClassic()setsinquiry_.delegate = niland immediately deallocatesinquiry_delegate_.- The enqueued callback on the main run loop executes, invoking
objc_msgSendon the freedBluetoothDeviceInquiryDelegateaddress, potentially leading to memory corruption in the unsandboxed browser process.
Suggested Fix
To remediate this issue, implement the same lifetime extension workaround used by the sibling classes. Keep a strong reference to the delegate alive across one main run loop turn during destruction:
~BluetoothDiscoveryManagerMacClassic() override {
inquiry_.delegate = nil;
BluetoothDeviceInquiryDelegate* __strong delegate = inquiry_delegate_;
dispatch_async(dispatch_get_main_queue(), ^{
(void)delegate;
});
}
Evaluated with Chrome root at commit: 208ca3371d87589335b108c431b95a36d768dc47
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.