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

Overview

High
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.
Tracker505190999
Fix commitcc8c9154cfd6 (chromium/src) +21/-5
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-04-22 (per vendor advisory)
Fix landed2026-04-24 (public git)
Disclosed2026-05-27
Patch gap33 days — fix public in source before the advisory

Background

`BluetoothSocketMac`
Chrome’s reference-counted (scoped_refptr) socket object on macOS that owns an L2CAP/RFCOMM channel and holds the connection callback.
`OnChannelOpenComplete`
the BluetoothSocketMac method invoked when a channel finishes connecting; it runs the client’s connection callback.
Synchronous IOBluetooth completion
when opening an L2CAP/RFCOMM channel, macOS may complete the operation inline, invoking the channel delegate’s completion handler on the same call stack that initiated the connect.
`is_connecting()`
a BluetoothSocketMac state predicate that is true only while a connection is in progress.

Root Cause Analysis

When BluetoothL2capChannelMac/BluetoothRfcommChannelMac opened a channel, macOS could complete the operation synchronously, causing the delegate to call socket()->OnChannelOpenComplete(...) inline on the connect stack. That method runs the client connection callback; if the client had concurrently closed the socket, the callback dropped the last scoped_refptr, synchronously destroying the BluetoothSocketMac while its own method (and the calling channel code) were still on the stack. As the stack unwound, the channel code continued to touch the now-freed this/socket (e.g., assigning the newly created channel), a classic use-after-free from re-entrant synchronous destruction.

The fix defers OnChannelOpenComplete via base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask, decoupling the callback from the OS event stack, and wraps the socket in base::WrapRefCounted inside the bound task so the object is kept alive until the task runs. It also replaces the DCHECK(is_connecting()) in OnChannelOpenComplete with a defensive early return when !is_connecting(), so a state change between posting and running the task is handled safely instead of proceeding.

Key insight
The single mistake was invoking the socket’s connection callback synchronously inline from the OS completion, allowing the callback to free the socket while it was still in use on the stack; the fix posts OnChannelOpenComplete to the task runner with a ref-counted socket so the object outlives the callback and no freed memory is touched on stack unwind.

Attack Path

  1. Initiate a connection Client code opens an L2CAP or RFCOMM channel through BluetoothSocketMac, calling into the macOS IOBluetooth framework.
  2. Synchronous completion macOS completes the open operation inline, causing the channel delegate to call socket()->OnChannelOpenComplete(...) directly on the connect stack rather than asynchronously.
  3. Concurrent close drops the last reference During the inline callback, the client has closed the socket, so running the connection callback drops the final scoped_refptr, synchronously destroying the BluetoothSocketMac.
  4. Use-after-free on unwind As the stack unwinds back into the channel code, execution assigns the new channel to / dereferences the freed this/socket, corrupting freed memory.
  5. Memory corruption (inferred) The resulting UAF could be developed into a stronger primitive, though the patch does not demonstrate a concrete read/write or code execution.

Impact Assessment

An attacker (or timing race) that closes a Bluetooth socket exactly while an L2CAP/RFCOMM connect completes synchronously can free the BluetoothSocketMac and cause a use-after-free in the process handling Bluetooth on macOS. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1. Any escalation beyond memory corruption is (inferred). The precondition is the race between a concurrent socket close and a synchronous OS connection-completion callback.

Files Changed

  • device/bluetooth/bluetooth_l2cap_channel_mac.mm
  • device/bluetooth/bluetooth_rfcomm_channel_mac.mm
  • device/bluetooth/bluetooth_socket_mac.mm

Audit Directions

  • Synchronous OS/delegate callbacks running user callbacks
    Audit any path where an OS framework may complete inline and invoke a client callback that can destroy the owning object; such callbacks should be posted rather than run on the originating stack.
  • Ref-count lifetime across posted tasks
    Verify that objects referenced after a callback (or bound into a task) are held by a scoped_refptr/WrapRefCounted for the duration, especially when a callback can drop the last reference.
  • DCHECK-only state invariants on async boundaries
    Look for DCHECKs asserting state (like is_connecting()) that no longer hold once work is deferred across a task boundary, and confirm they are replaced with defensive runtime guards.
From cc8c9154cfd6ab0183148852f53a8daad1f3eaa0 Mon Sep 17 00:00:00 2001
From: Rob Pitkin <[email protected]>
Date: Fri, 24 Apr 2026 14:41:50 -0700
Subject: [PATCH] bluetooth: Fix Use-After-Free in BluetoothSocketMac connection handling.

Fix a Use-After-Free vulnerability in BluetoothSocketMac on macOS
caused by the synchronous execution of delegate callbacks originating
from the native IOBluetooth framework.

When initiating L2CAP or RFCOMM connections, macOS can complete the
operation synchronously. This immediately triggers OnChannelOpenComplete
inline, running the connection callback. If the client concurrently
closed the socket, the callback drops the final scoped_refptr,
synchronously destroying the BluetoothSocketMac instance. Upon stack
unwinding, execution attempts to assign the new channel to a freed
`this` pointer, resulting in memory corruption.

This CL resolves the race condition by:
1. Deferring socket callback execution in OnChannelOpenComplete
    using base::SingleThreadTaskRunner::PostTask, safely decoupling
    the OS event from the Chrome execution stack.
2. Wrapping the socket in base::WrapRefCounted inside the bind payload
    to guarantee the object survives in the task queue.
3. Upgrading the DCHECK to a defensive early return if the socket
    state changes before the task is processed.

Bug: 505190999
Change-Id: If3d786a1aa0967f6e8ed00fdb7706350bf073719
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7787138
Commit-Queue: Rob Pitkin <[email protected]>
Reviewed-by: Matt Reynolds <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1620484}
---

diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index aafa433..f1c5b39 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -7,7 +7,10 @@
 #include <memory>
 
 #include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "device/bluetooth/bluetooth_classic_device_mac.h"
 #include "device/bluetooth/bluetooth_socket_mac.h"
 
@@ -184,8 +187,12 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
-  socket()->OnChannelOpenComplete(
-      BluetoothClassicDeviceMac::GetDeviceAddress([channel device]), status);
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
+                                base::WrapRefCounted(socket()),
+                                BluetoothClassicDeviceMac::GetDeviceAddress(
+                                    [channel device]),
+                                status));
 }
 
 void BluetoothL2capChannelMac::OnChannelClosed(
diff --git a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
index ebc6684..635a30f4 100644
--- a/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
+++ b/device/bluetooth/bluetooth_rfcomm_channel_mac.mm
@@ -7,7 +7,10 @@
 #include <memory>
 
 #include "base/check_op.h"
+#include "base/functional/bind.h"
+#include "base/location.h"
 #include "base/memory/raw_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "device/bluetooth/bluetooth_classic_device_mac.h"
 #include "device/bluetooth/bluetooth_socket_mac.h"
 
@@ -184,8 +187,12 @@
     DCHECK_EQ(status, kIOReturnSuccess);
   }
 
-  socket()->OnChannelOpenComplete(
-      BluetoothClassicDeviceMac::GetDeviceAddress([channel getDevice]), status);
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(&BluetoothSocketMac::OnChannelOpenComplete,
+                                base::WrapRefCounted(socket()),
+                                BluetoothClassicDeviceMac::GetDeviceAddress(
+                                    [channel getDevice]),
+                                status));
 }
 
 void BluetoothRfcommChannelMac::OnChannelClosed(
diff --git a/device/bluetooth/bluetooth_socket_mac.mm b/device/bluetooth/bluetooth_socket_mac.mm
index e5901d2..a303bc0 100644
--- a/device/bluetooth/bluetooth_socket_mac.mm
+++ b/device/bluetooth/bluetooth_socket_mac.mm
@@ -632,7 +632,9 @@
     const std::string& device_address,
     IOReturn status) {
   DCHECK(thread_checker_.CalledOnValidThread());
-  DCHECK(is_connecting());
+  if (!is_connecting()) {
+    return;
+  }
 
   DVLOG(1) << device_address << " " << uuid_.canonical_value()
            << ": channel open complete.";
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in BluetoothSocketMac L2CAP connection handling

Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in BluetoothSocketMac::OnSDPQueryComplete on macOS when establishing L2CAP connections. If the macOS Bluetooth framework completes the connection synchronously and the socket is concurrently closed by the client, the BluetoothSocketMac object can be destroyed while executing on the stack. When the stack unwinds, a member variable assignment writes to freed memory, which could be leveraged for arbitrary code execution in the Browser process.

Affected files:

  • device/bluetooth/bluetooth_socket_mac.mm
  • device/bluetooth/bluetooth_l2cap_channel_mac.mm

Estimated timestamp from git blame: 2021-04-05

Description

A potential Use-After-Free vulnerability exists in the macOS Bluetooth socket implementation for L2CAP channels. The issue occurs because BluetoothSocketMac does not adequately protect its lifetime against synchronous delegate callbacks from the native macOS IOBluetooth framework.

During an L2CAP connection, BluetoothSocketMac::OnSDPQueryComplete calls BluetoothL2capChannelMac::OpenAsync. This method delegates to the macOS API [device openL2CAPChannelAsync:withPSM:delegate:]. In certain states (e.g., if the connection is cached), this API can complete synchronously, immediately invoking the delegate’s l2capChannelOpenComplete:status: method before OpenAsync returns.

This triggers a synchronous call to BluetoothSocketMac::OnChannelOpenComplete, which executes the pending connection success_callback. If an attacker (e.g., via a malicious Chrome Extension) has concurrently queued a close operation on the socket, this callback will fail to look up the socket and drop the final scoped_refptr keeping the BluetoothSocketMac alive. The object is deleted synchronously. As the stack unwinds back to OnSDPQueryComplete, execution resumes on the destroyed this pointer:

// device/bluetooth/bluetooth_socket_mac.mm
void BluetoothSocketMac::OnSDPQueryComplete(...) {
  // ...
  } else {
    DCHECK_NE(l2cap_psm, kInvalidL2capPsm);
    channel_ = 
        BluetoothL2capChannelMac::OpenAsync(this, device, l2cap_psm, &status);
  }
  // ... timer_.Start(...) accesses freed `this` 
}

The assignment to channel_ writes a std::unique_ptr into freed heap memory, and triggers delete on the old, attacker-controllable pointer value. This can be exploited to achieve arbitrary code execution in the Browser process.

Potential Steps to Trigger

Note: These are suggested steps based on static code analysis; our tooling agent does not currently have the ability to run code to verify them with a live Proof of Concept.

  1. A malicious Chrome extension with bluetooth permissions calls chrome.bluetoothSocket.create().
  2. The extension initiates chrome.bluetoothSocket.connect() targeting a cached or immediately available L2CAP service on a paired macOS Bluetooth device.
  3. While the SDP query is in flight asynchronously, the extension calls chrome.bluetoothSocket.close(). This synchronously removes the socket from the Extension API’s internal resource manager.
  4. When the SDP query completes, BluetoothSocketMac::OnSDPQueryComplete runs, dropping its internal strong references (timers and listeners) to itself.
  5. It then calls OpenAsync, which calls openL2CAPChannelAsync. The OS completes this synchronously, firing the completion delegate.
  6. BluetoothSocketMac::OnChannelOpenComplete runs and executes the extension’s connection callback.
  7. The callback (BluetoothSocketAbstractConnectFunction::OnConnect) fails to find the socket (since it was closed in step 3), drops the final scoped_refptr, and BluetoothSocketMac is deleted.
  8. Execution returns to OnSDPQueryComplete, which assigns the new channel to the freed channel_ member, triggering heap corruption.

Suggested Fix

There are two primary ways to fix this:

  1. Lifetime Protection: Hold a strong reference to this inside BluetoothSocketMac::OnSDPQueryComplete to guarantee the object survives the potential synchronous callback:
    scoped_refptr<BluetoothSocketMac> self(this);
    // ... call OpenAsync ...
    
  2. Callback PostTask: Alternatively, ensure that BluetoothL2capChannelMac::OnChannelOpenComplete always dispatches the result to the socket asynchronously via base::SingleThreadTaskRunner::PostTask, preventing the re-entrant synchronous destruction.

Notably, the RFCOMM implementation (BluetoothRfcommChannelMac) avoids this specific exploit path because its delegate (BluetoothRfcommChannelDelegate) contains a CHECK(_rfcommChannel) which crashes the process in the synchronous scenario before the UAF can be triggered. Adding a similar CHECK to BluetoothL2capChannelDelegate would convert this into a safe DoS crash, but properly fixing the lifetime or deferring the callback is the preferred approach.

Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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