Overview

Critical
Severity
9.6
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.
Tracker517021684
Fix commit429a22c5a996 (chromium/src) +12/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/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-30 (public git)
Disclosed2026-06-30
Patch gap31 days — fix public in source before the advisory

Background

`SDPQueryListener`
An Objective-C helper object that wraps an outstanding macOS IOBluetooth SDP (Service Discovery Protocol) query and receives the -sdpQueryComplete:status: callback when the query finishes.
SDP query timeout
A watchdog on BluetoothSocketMac that fires when an SDP query takes too long, after which the socket releases its reference to the SDPQueryListener.
Unretained target (FB13705522)
A macOS IOBluetooth bug in which the performSDPQuery: callback target is stored without a retain, so IOBluetooth does not keep the listener alive until it delivers the callback.
`NS_VALID_UNTIL_END_OF_SCOPE`
An Objective-C annotation that forces a strong local reference to remain valid through the end of its enclosing scope so self is not deallocated mid-method.

Root Cause Analysis

When an SDP query is issued, BluetoothSocketMac holds the only strong reference to the SDPQueryListener, while IOBluetooth stores the performSDPQuery: target unretained due to the macOS bug FB13705522. If the query exceeds the watchdog timeout, the socket drops its strong reference to the listener, and because IOBluetooth held no retaining reference, the SDPQueryListener is immediately deallocated. When the late -sdpQueryComplete:status: callback finally fires, IOBluetooth dispatches to the now-freed object, dereferencing the dangling _device weak pointer and freed instance state, causing a use-after-free.

The fix adds a strong self-reference _strongSelf that the listener sets to self at construction, guaranteeing it outlives the socket’s reference and any late callback. In -sdpQueryComplete:status:, the callback moves _strongSelf into a scope-guarded local and clears the ivar, so the listener stays alive through the method body and is then released exactly once, restoring the invariant that the listener lives until IOBluetooth is done with it.

Key insight
The single mistake was relying on the socket (and an unretained IOBluetooth target) to keep the SDPQueryListener alive across an asynchronous callback whose delivery could outlive the timeout; the fix makes the listener own a strong reference to itself for the entire query lifetime so it cannot be freed before -sdpQueryComplete:status: runs.

Attack Path

  1. Initiate SDP query Trigger a Bluetooth SDP query through BluetoothSocketMac, causing an SDPQueryListener to be created and registered with IOBluetooth via performSDPQuery:.
  2. Stall past the watchdog Arrange for the queried device to respond slowly (or not at all) so the SDP query exceeds its timeout while the callback is still pending.
  3. Force deallocation On timeout the socket drops its strong reference; because IOBluetooth stores the target unretained, the SDPQueryListener is immediately deallocated, leaving a dangling target.
  4. Deliver the late callback When IOBluetooth finally dispatches -sdpQueryComplete:status: to the freed listener, the use-after-free occurs as the method touches _device and freed instance state.
  5. Exploit reclaimed memory Groom the heap so the freed listener’s memory is reallocated with attacker-influenced data before the late callback runs, turning the UAF into corruption of controlled fields.

Impact Assessment

An attacker who can drive a stalled or hostile Bluetooth SDP query gains a use-after-free in the browser process where BluetoothSocketMac runs on macOS, potentially leading to memory corruption and code execution. The precondition is a slow or malicious SDP peer that lets the query hit its watchdog timeout so the listener is freed while a callback is still outstanding, combined with the macOS IOBluetooth unretained-target behavior (FB13705522).

Changed Functions

FunctionChangeNotes
if
device/bluetooth/bluetooth_socket_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_socket_mac.mm

Audit Directions

  • Unretained async callback targets
    Audit other IOBluetooth and platform callback registrations that store their target unretained and rely on a separate owner to keep the target alive across asynchronous delivery.
  • Timeout-driven reference drops
    Review watchdog and timeout paths that release the only strong reference to an object while an in-flight callback for that object may still fire late.
  • Self-retain lifetime symmetry
    Check listener/delegate objects that adopt a strong self-reference to ensure it is always cleared on exactly one terminal path, so neither a leak nor a double-free replaces the original UAF.
From 429a22c5a9961a9897b31134dcf050c7af51d61a Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Fri, 29 May 2026 19:49:52 -0700
Subject: [PATCH] bluetooth: prevent use-after-free in SDPQueryListener during timeout

Keep SDPQueryListener alive using a strong self-reference until the
macOS IOBluetooth callback is delivered.

On a watchdog timeout, the socket drops its strong reference to the
listener. Because IOBluetooth stores its target unretained (FB13705522),
this leads to an immediate deallocation and a subsequent Use-After-Free
(UAF) when the late callback fires. Retaining self until the callback
completes ensures the listener survives.

BUG=517021684

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

diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index 2ab7154..315f1ddf 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -53,6 +53,12 @@
 
   // The device being queried.
   IOBluetoothDevice* __weak _device;
+
+  // While the SDP query is outstanding, the listener holds a strong reference
+  // to itself so that it outlives any late -sdpQueryComplete:status: dispatch
+  // from IOBluetooth, which stores the performSDPQuery: target unretained.
+  // This is a workaround for a macOS bug, see Apple Feedback report FB13705522.
+  SDPQueryListener* __strong _strongSelf;
 }
 
 - (instancetype)initWithSocket:(scoped_refptr<device::BluetoothSocketMac>)socket
@@ -77,6 +83,8 @@
     _device = device;
     _success_callback = std::move(success_callback);
     _error_callback = std::move(error_callback);
+    // Retain self until IOBluetooth delivers -sdpQueryComplete:status:.
+    _strongSelf = self;
   }
 
   return self;
@@ -92,6 +100,10 @@
 
 - (void)sdpQueryComplete:(IOBluetoothDevice*)device status:(IOReturn)status {
   DCHECK_EQ(device, _device);
+  // IOBluetooth has called back; drop the self-retain. Keep |self| alive for
+  // the remainder of this method via a local strong reference.
+  NS_VALID_UNTIL_END_OF_SCOPE SDPQueryListener* strongSelf = _strongSelf;
+  _strongSelf = nil;
   if (!_error_callback) {
     // This can happen when the target is called after SDP query timeout.
     return;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in BluetoothSocketMac on macOS during SDP query timeout

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 Bluetooth socket implementation when an SDP query times out. If the timeout fires, the strong reference to the SDPQueryListener delegate is released, immediately deallocating it while the macOS IOBluetooth framework still holds an unsafe_unretained reference to it. When the late callback or internal query completion eventually fires, the OS dispatches a selector to the deallocated object, potentially causing a browser process crash or arbitrary code execution.

Affected files:

  • device/bluetooth/bluetooth_socket_mac.mm

Estimated timestamp from git blame: 2024-06-13

Problem Description

In device/bluetooth/bluetooth_socket_mac.mm, SDPQueryListener is registered as the target of -[IOBluetoothDevice performSDPQuery:], a legacy macOS system API. Because the macOS IOBluetooth framework dates back to early versions of macOS, it stores its target/delegate using an unsafe_unretained (raw) pointer to avoid strong reference cycles.

Chromium’s only strong reference keeping the SDPQueryListener alive is the sdp_query_listener_ instance variable in BluetoothSocketMac (declared as SDPQueryListener* __strong sdp_query_listener_; in the header).

When an SDP query takes longer than the local 10-second watchdog timer, BluetoothSocketMac::OnSDPQueryTimeout() is executed. It clears the callback and resets the listener pointer:

void BluetoothSocketMac::OnSDPQueryTimeout() {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!sdp_query_listener_) {
    return;
  }
  auto error_callback = [sdp_query_listener_ takeErrorCallback];
  if (error_callback) {
    std::move(error_callback).Run(kSDPQueryTimeout);
  }
  sdp_query_listener_ = nil;   // Sole strong reference is dropped, deallocating the listener
}

Resetting sdp_query_listener_ to nil immediately deallocates the SDPQueryListener instance. However, the OS-level SDP query is still running asynchronously in the background. When the query eventually completes (or times out at the system level), the OS Bluetooth framework attempts to invoke -sdpQueryComplete:status: on the deallocated pointer, causing a Use-After-Free inside objc_msgSend.

Since the Device/Bluetooth Service runs inside the unsandboxed Browser process on macOS due to TCC system entitlement constraints, this issue could potentially be leveraged to escape the browser sandbox and execute arbitrary code with browser privileges.

Potential Steps to Trigger the Vulnerability

Note: Our analysis is static and based on code review; we do not currently have an active runtime test environment to execute a proof of concept.

  1. A user selects a paired, malicious Bluetooth Classic device via a Web Bluetooth/Web Serial chooser prompt.
  2. The renderer requests a connection, initiating BluetoothSocketMac::Connect and dispatching [device performSDPQuery:sdp_query_listener_] with a 10-second local watchdog timer.
  3. The malicious Bluetooth device intentionally delays responding to the SDP query for more than 10 seconds.
  4. The 10-second timer expires, and BluetoothSocketMac::OnSDPQueryTimeout drops the reference, immediately deallocating the SDPQueryListener object under ARC.
  5. The attacker attempts to reclaim the freed memory layout by spraying the heap (e.g., via Mojo or other browser allocations) to establish a fake isa pointer and method cache.
  6. The malicious Bluetooth device finally sends the SDP response, prompting the OS to call -sdpQueryComplete:status: on the dangling target pointer, triggering a control flow hijack.

Suggested Remediation

Since macOS does not provide a reliable API to cancel a pending SDP query once dispatched to IOBluetoothDevice, the SDPQueryListener must not be deallocated until the OS callback is received.

To safely resolve this:

  1. When a timeout occurs, clear the internal references to prevent processing the callback, but do not immediately deallocate the listener.
  2. Have the SDPQueryListener hold a strong self-reference (__strong id selfKeepAlive = self;) when it is orphaned/detached from the C++ socket.
  3. Release this self-reference at the very end of sdpQueryComplete:status:, ensuring that the object is safely kept alive until the OS is done interacting with it.

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