Critical watch CVSS 9 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
9
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Sub-componentMacOS Bluetooth
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.
Tracker505140741
Fix commit718de6b8e9b7 (chromium/src) +11/-2
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-22 (per vendor advisory)
Fix landed2026-04-22 (public git)
Disclosed2026-05-27
Patch gap35 days — fix public in source before the advisory

Background

`IOBluetoothL2CAPChannel`
the macOS IOBluetooth framework object representing an L2CAP channel; it retains a delegate to which it dispatches connection events.
`BluetoothL2capChannelDelegate`
Chrome’s Objective-C delegate that receives L2CAP channel callbacks (e.g., l2capChannelOpenComplete:status:) on behalf of a BluetoothL2capChannelMac.
Delegate unregistration
to safely tear down, a delegate must clear itself from the native channel (via the channel it holds in _l2capChannel) so the OS stops dispatching events to a freed object.
Outgoing L2CAP connection
an L2CAP channel opened by Chrome via openL2CAPChannelAsync:withPSM:delegate:, which returns the native channel object to the caller.

Root Cause Analysis

For outgoing L2CAP connections, BluetoothL2capChannelDelegate never received a pointer to the native IOBluetoothL2CAPChannel it was serving, so its _l2capChannel member stayed null. During connection teardown the delegate therefore could not unregister itself from the native channel, leaving the macOS Bluetooth framework holding a dangling reference to the delegate after it was deallocated. If the framework subsequently dispatched an event (such as a completion or close callback) to that freed delegate, the dangling pointer was used, producing a use-after-free.

The fix adds a setL2capChannel: setter and calls it in OpenAsync immediately after a successful openL2CAPChannelAsync: so _l2capChannel is populated, enabling correct unregistration; a CHECK(_l2capChannel) in l2capChannelOpenComplete:status: and a CHECK(!_l2capChannel) in the setter enforce the invariant. This mirrors the already-correct RFCOMM implementation fixed earlier in bluetooth_rfcomm_channel_mac.mm.

Key insight
The delegate for outgoing L2CAP connections was created without a reference to its native channel, so it could never unregister on teardown; the fix supplies that reference via setL2capChannel: right after the channel is opened, matching the correct RFCOMM pattern.

Attack Path

  1. Initiate an outgoing L2CAP connection Code (reachable via the Bluetooth stack) opens an outgoing L2CAP channel; OpenAsync calls openL2CAPChannelAsync:withPSM:delegate: and stores the native channel in channel->channel_, but pre-patch never told the delegate about it, leaving _l2capChannel null.
  2. Tear down the channel During connection teardown the delegate attempts to clean up, but without _l2capChannel it cannot unregister itself from the native IOBluetoothL2CAPChannel.
  3. Delegate is deallocated while still registered The delegate object is freed while the macOS framework still holds a pointer to it, creating a dangling reference.
  4. Event dispatched to freed delegate A subsequent Bluetooth event dispatched by the OS to the dangling delegate pointer triggers the use-after-free (inferred as to which specific event fires).
  5. Memory corruption (inferred) Use of the freed delegate can corrupt memory and potentially be leveraged further, though the patch does not demonstrate a concrete read/write primitive or code execution.

Impact Assessment

An attacker able to influence outgoing L2CAP connection teardown timing can cause the macOS Bluetooth framework to dispatch to a freed BluetoothL2capChannelDelegate, yielding a use-after-free in the browser/device process that handles Bluetooth. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 9.0. Any escalation to control-flow hijack or code execution is (inferred). Preconditions include the ability to trigger an outgoing L2CAP connection and its teardown on macOS.

Changed Functions

FunctionChangeNotes
if
device/bluetooth/bluetooth_l2cap_channel_mac.mm
modified

Files Changed

  • device/bluetooth/bluetooth_l2cap_channel_mac.mm

Audit Directions

  • Delegate/native-object back-references
    Audit every macOS IOBluetooth (and similar OS-callback) delegate to confirm it stores a reference to the native object it must unregister from, for both the incoming/listening and outgoing/connecting paths.
  • Asymmetric fixes across sibling channel types
    Since RFCOMM was fixed earlier but L2CAP was missed, look for other paired implementations (L2CAP vs RFCOMM, incoming vs outgoing) where a correctness fix was applied to one variant but not its twin.
  • Teardown-time unregistration invariants
    Review teardown/dealloc paths of OS-retained delegates to ensure the framework’s pointer is cleared before the delegate is freed, and consider CHECKs that enforce the required reference is present.
From 718de6b8e9b77d656b340d708f79768098f576cc Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Wed, 22 Apr 2026 10:19:12 -0700
Subject: [PATCH] [macOS Bluetooth] Fix UAF in L2CAP channel teardown

For outgoing L2CAP connections, the BluetoothL2capChannelDelegate fails
to properly unregister itself from the native IOBluetoothL2CAPChannel
during connection teardown because it lacks a reference to the native
channel object. This leads to a dangling pointer within the macOS
Bluetooth framework, which can be triggered if subsequent events are
dispatched to the deallocated delegate.

This CL fixes the issue by adding a setL2capChannel: setter to the
delegate and calling it after the native channel is created in
OpenAsync. This matches the existing (and correct) implementation
pattern used for RFCOMM channels in bluetooth_rfcomm_channel_mac.mm,
which was fixed in https://crrev.com/c/5750170.

Fixed: 505140741
Change-Id: I18c0ccb6a145856d3ebeb1d62983a0c5518f3adc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7785775
Reviewed-by: Matt Reynolds <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1618950}
---

diff --git a/device/bluetooth/bluetooth_l2cap_channel_mac.mm b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
index 5bcbde96..aafa433 100644
--- a/device/bluetooth/bluetooth_l2cap_channel_mac.mm
+++ b/device/bluetooth/bluetooth_l2cap_channel_mac.mm
@@ -28,6 +28,7 @@
 
 - (instancetype)initWithChannel:(device::BluetoothL2capChannelMac*)channel
                    l2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;
+- (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;
 
 @end
 
@@ -45,6 +46,7 @@
 
 - (void)l2capChannelOpenComplete:(IOBluetoothL2CAPChannel*)l2capChannel
                           status:(IOReturn)error {
+  CHECK(_l2capChannel);
   if (error == kIOReturnSuccess) {
     // Keep the delegate alive until l2capChannelClosed.
     _strongSelf = self;
@@ -92,6 +94,11 @@
   _channel = nullptr;
 }
 
+- (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel {
+  CHECK(!_l2capChannel);
+  _l2capChannel = l2capChannel;
+}
+
 @end
 
 namespace device {
@@ -128,10 +135,12 @@
   *status = [device openL2CAPChannelAsync:&l2cap_channel
                                   withPSM:psm
                                  delegate:channel->delegate_];
-  if (*status == kIOReturnSuccess)
+  if (*status == kIOReturnSuccess) {
     channel->channel_ = l2cap_channel;
-  else
+    [channel->delegate_ setL2capChannel:l2cap_channel];
+  } else {
     channel.reset();
+  }
 
   return channel;
 }
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in BluetoothL2capChannelDelegate on macOS

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 vulnerability exists in the macOS Bluetooth L2CAP implementation due to a failure to clear the native IOBluetoothL2CAPChannel delegate when closing outgoing connections. The delegate object is deallocated while the native OS channel still holds a dangling pointer to it, which can be triggered if late events are dispatched.

Affected files:

  • device/bluetooth/bluetooth_l2cap_channel_mac.mm

Estimated timestamp from git blame: 2024-04-03

Summary

A potential Use-After-Free (UAF) vulnerability exists in the macOS Bluetooth L2CAP implementation in Chromium. For outgoing L2CAP connections, the BluetoothL2capChannelDelegate fails to properly unregister itself from the native IOBluetoothL2CAPChannel during connection teardown. This leads to a dangling pointer within the macOS Bluetooth framework, which can be triggered if subsequent events (such as late data arrival) are dispatched to the deallocated delegate.

Technical Details

In device/bluetooth/bluetooth_l2cap_channel_mac.mm, when an outgoing L2CAP connection is initiated via BluetoothL2capChannelMac::OpenAsync, the following sequence occurs:

  1. A BluetoothL2capChannelMac wrapper object is constructed with its internal channel_ member explicitly set to nil (line 124).
  2. The constructor immediately calls SetSocket(), which instantiates a BluetoothL2capChannelDelegate. The delegate is initialized using channel_, meaning its internal _l2capChannel ivar is set to nil (lines 147-148).
  3. [device openL2CAPChannelAsync:...] is called, which creates the native IOBluetoothL2CAPChannel and registers the delegate with it (line 128).
  4. BluetoothL2capChannelMac::channel_ is updated with the new native channel object (line 132). However, the delegate’s internal _l2capChannel ivar is never updated.

When the connection is later closed, the macOS Bluetooth stack invokes l2capChannelClosed: on the delegate:

- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel {
  [_l2capChannel setDelegate:nil];  // This is a no-op because _l2capChannel is nil
  // ...
  _strongSelf = nil; // This releases the delegate object
}

Because _l2capChannel is nil for outgoing connections, the delegate fails to call setDelegate:nil on the actual native channel object. When _strongSelf is cleared, the delegate is deallocated via ARC. However, the native IOBluetoothL2CAPChannel object inside the OS framework still holds an unsafe_unretained (raw) pointer to this deallocated delegate.

This implementation is inconsistent with the RFCOMM implementation in device/bluetooth/bluetooth_rfcomm_channel_mac.mm, which correctly provides a setter (setRfcommChannel:) to update the delegate’s channel reference during OpenAsync.

Potential Exploitation

(Note: These are suggested steps based on static analysis; a working proof-of-concept has not been executed.)

This UAF occurs in the Browser process. Furthermore, Objective-C objects are allocated using the system allocator and are not protected by MiraclePtr/PartitionAlloc.

An attacker could potentially trigger this by:

  1. Creating a malicious Web Extension that uses the chrome.bluetoothSocket API, or operating a malicious Bluetooth peripheral that Chrome connects to.
  2. Establishing an L2CAP connection.
  3. Racing a connection teardown (e.g., closing the socket) against incoming data packets from the peripheral.
  4. If a packet arrives and is processed by the OS after l2capChannelClosed: has released the delegate, but before the native channel is fully destroyed, the OS will attempt to dispatch an Objective-C message (e.g., l2capChannelData:) to the freed memory.
  5. Using heap spraying techniques, an attacker could replace the freed Objective-C object to gain control of the instruction pointer, leading to arbitrary code execution and a sandbox escape.

Suggested Fix

Similar to the RFCOMM implementation, update BluetoothL2capChannelDelegate to include a setter method for the channel, and call it after a successful connection:

  1. In BluetoothL2capChannelDelegate, add - (void)setL2capChannel:(IOBluetoothL2CAPChannel*)l2capChannel;.
  2. In BluetoothL2capChannelMac::OpenAsync, update the delegate after openL2CAPChannelAsync succeeds:
    if (*status == kIOReturnSuccess) {
      channel->channel_ = l2cap_channel;
      [channel->delegate_ setL2capChannel:l2cap_channel];
    } else {
    // ...
    

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