High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
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.
Tracker518237527
Fix commit673a5aee77ad (chromium/src) +26/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-30 (per vendor advisory)
Fix landed2026-06-02 (public git)
Disclosed2026-06-08
Patch gap6 days — fix public in source before the advisory

Background

`BluetoothDiscoveryManagerMacClassic`
the C++ object that drives classic (BR/EDR) Bluetooth device inquiry on macOS by owning an IOBluetoothDeviceInquiry and 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.

Key insight
The core mistake was tearing down the delegate/manager pair synchronously while an asynchronous callback could still be in flight against a non-owning (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

  1. Start an inquiry Web content (via Web Bluetooth) or a browser code path triggers classic device inquiry, creating a BluetoothDiscoveryManagerMacClassic with its IOBluetoothDeviceInquiry and delegate.
  2. Queue an async callback The OS enqueues an inquiry callback (deviceInquiryStarted:, deviceInquiryDeviceFound:device:, or deviceInquiryComplete:error:aborted:) onto the main run loop.
  3. 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).
  4. Callback fires on freed memory The enqueued callback executes on the now-deallocated delegate, and/or the delegate dereferences its dangling _manager back-pointer, producing a use-after-free.
  5. 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

An attacker who can drive Bluetooth device inquiry and win the destruction-vs-callback race can trigger a use-after-free in the browser process on macOS, giving a read/write of freed memory that may be leveraged toward memory corruption (inferred). Preconditions are macOS with classic Bluetooth available and the ability to start and then tear down an inquiry while a callback is queued. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.

Changed Functions

FunctionChangeNotes
if
device/bluetooth/bluetooth_discovery_manager_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_discovery_manager_mac.mm

Audit Directions

  • `assign`/unsafe_unretained delegates
    Audit Objective-C code that sets a delegate on system objects using assign (not weak) semantics; ensure the delegate is invalidated and outlives any in-flight callbacks when its owner is destroyed.
  • Async callbacks vs. synchronous teardown
    Look 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 work
    Review destructors that merely detach a delegate/observer without cancelling or draining already-enqueued run-loop or dispatch-queue work that could reference the object.
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);
 }
 
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. A page or extension triggers Bluetooth classic scanning (e.g., via Web Serial or chrome.bluetooth.startDiscovery()).
  2. BluetoothAdapterMac::StartScanWithFilter lazily initializes classic_discovery_manager_ (an instance of BluetoothDiscoveryManagerMacClassic) and starts the device inquiry via [inquiry_ start].
  3. 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.
  4. The user closes the tab or navigates away, destroying the adapter and destroying classic_discovery_manager_ via its unique pointer.
  5. ~BluetoothDiscoveryManagerMacClassic() sets inquiry_.delegate = nil and immediately deallocates inquiry_delegate_.
  6. The enqueued callback on the main run loop executes, invoking objc_msgSend on the freed BluetoothDeviceInquiryDelegate address, 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.

View on issue tracker