Low watch CVSS 8.1 chrome UAF 🔧 Commit mapped

Overview

Low
Severity
8.1
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.
Tracker495783474
Fix commit375096a7dd43 (chromium/src) +40/-0
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-24 (per vendor advisory)
Fix landed2026-05-23 (public git)
Disclosed2026-06-30
Patch gap38 days — fix public in source before the advisory

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 IOBluetoothUserNotification object.
`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 / _l2capNewChannelNotification owners) that receive rfcommChannelOpened: / l2capChannelOpened: callbacks and forward them to the owning C++ BluetoothSocketMac via the _socket back-pointer.
FB13705522
the referenced Apple framework bug whereby IOBluetooth may dispatch a callback to a listener even after that listener has called unregister.

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.

Key insight
The listener was destroyed synchronously while the OS could still hold and invoke a non-owning pointer to it; the fix both neutralizes the back-pointer (_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

  1. Establish a listening socket A caller sets up an RFCOMM or L2CAP listening BluetoothSocketMac, which registers _rfcommNewChannelNotification / _l2capNewChannelNotification with IOBluetooth. (inferred)
  2. Trigger teardown with a callback in flight The socket is closed so the listener is released while IOBluetooth, due to FB13705522, still has an enqueued channel-opened notification targeting the freed listener via its unsafe_unretained pointer.
  3. Late callback dereferences freed object IOBluetooth dispatches rfcommChannelOpened: / l2capChannelOpened: to the deallocated listener, which reads the dangling _socket back-pointer, causing the use-after-free.
  4. 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

An attacker able to drive Bluetooth socket setup/teardown timing can trigger a use-after-free in the browser (privileged) process on macOS, with the precondition of Bluetooth availability and the ability to race a listener teardown against an in-flight 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

FunctionChangeNotes
if
device/bluetooth/bluetooth_socket_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_socket_mac.mm

Audit Directions

  • OS callback outliving its target
    Audit 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 handlers
    Review callback handlers that dereference a back-pointer (like _socket) without first checking it was invalidated during teardown.
  • `unregister`-only teardown
    Look for teardown paths that assume unregister synchronously stops all callbacks; verify against known framework bugs where late dispatches still occur.
From 375096a7dd434f3fa2a5cff6a8c71c3f3827da83 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
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 <mattreynolds@chromium.org>
Commit-Queue: Alvin Ji <alvinji@chromium.org>
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
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.