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
Sub-componentFloss
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.
Tracker516947491
Fix commit5d8fc68ff55c (chromium/src) +48/-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-27 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

`BluetoothSocketFloss`
a reference-counted (scoped_refptr) socket object in the Floss Bluetooth backend that owns listen/accept state such as is_accepting_ and a weak_ptr_factory_.
`DoConnectionStateChanged`
the method (enclosing name not shown in the hunk header but identified by the commit and test) that reacts to server-socket state changes and runs pending_listen_ready_callback_, then updates is_accepting_ and issues an Accept.
`pending_listen_ready_callback_`
a member callback that, after CompleteListen, may hold the object’s only remaining strong reference bound inside it.
`WeakPtr`-bound `RepeatingCallback`
the dispatch mechanism used to deliver connection-state changes, which by design holds no strong reference to the target on the call stack.

Root Cause Analysis

DoConnectionStateChanged is invoked through a WeakPtr-bound RepeatingCallback, so nothing on the call stack keeps BluetoothSocketFloss alive. After CompleteListen runs, the sole scoped_refptr to the socket can be the one captured inside pending_listen_ready_callback_ itself. When DoConnectionStateChanged synchronously runs that callback and the callee drops its scoped_refptr without retaining it—which occurs in production when chrome.bluetoothSocket.close() has already removed the BluetoothApiSocket before listen completed—the reference count hits zero and ~BluetoothSocketFloss() frees this mid-method. The method then continues and writes is_accepting_ and dereferences weak_ptr_factory_ on freed heap, a use-after-free.

The fix takes a local scoped_refptr<BluetoothSocketFloss> self(this) at method entry so the object is guaranteed to outlive the listen-ready, listen-close, and accept-error callbacks it invokes.

Key insight
The method ran callbacks that could hold the last strong reference to this and then kept touching member state after they returned, so a callback that dropped that reference freed the object underneath the still-executing method; the fix pins a local self reference for the method’s whole duration.

Attack Path

  1. Start a listening socket Through the chrome.bluetoothSocket extension API, create an RFCOMM service so a BluetoothSocketFloss is created and its pending_listen_ready_callback_ is set up.
  2. Drop the external owner Call chrome.bluetoothSocket.close() so GetSocket(socket_id()) returns nullptr in BluetoothSocketListenFunction::OnCreateService, meaning the listen-ready callback’s scoped_refptr parameter will be destroyed without being retained anywhere.
  3. Reduce to a single owner With CompleteListen having run, the only strong reference to the BluetoothSocketFloss is the one bound inside its own pending_listen_ready_callback_ member.
  4. Trigger the free mid-method A socket-ready state change dispatches DoConnectionStateChanged via the WeakPtr-bound callback; running pending_listen_ready_callback_ drops the last reference, ~BluetoothSocketFloss() frees the object, and the method’s subsequent write to is_accepting_ and use of weak_ptr_factory_ operate on freed heap (use-after-free confirmed by the ASan regression test).

Impact Assessment

An attacker with access to the chrome.bluetoothSocket extension surface can free a BluetoothSocketFloss while DoConnectionStateChanged is still running, producing a heap use-after-free in the browser process on ChromeOS/Floss. The advisory classifies this as a use-after-free in Bluetooth; no exploited-in-the-wild status is stated. Turning the write/read on freed memory into control of execution is (inferred), and the precondition is that the external socket owner is torn down (via close()) before the listen completes.

Changed Functions

FunctionChangeNotes
if
device/bluetooth/floss/bluetooth_socket_floss.cc
modified
TEST_F
device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
modified

Files Changed

  • device/bluetooth/floss/bluetooth_socket_floss.cc
  • device/bluetooth/floss/bluetooth_socket_floss_unittest.cc

Audit Directions

  • Callback that may hold the last self-reference
    Audit methods dispatched via WeakPtr-bound callbacks (no strong stack reference) that synchronously run member callbacks and then continue to touch member state, especially where the callback can own the object’s sole scoped_refptr.
  • Post-callback member access
    Look for any reference-counted object that writes members or dereferences a weak_ptr_factory_ after invoking user- or client-supplied callbacks without holding a local strong reference.
  • Teardown races on socket/device lifetimes
    Review the Bluetooth extension paths where close()/removal of the API socket can run before an asynchronous listen/connect completes, dropping references at unexpected points.
From 5d8fc68ff55c002566acc9852f491ef60639d203 Mon Sep 17 00:00:00 2001
From: Gigi Joseph <gigij@google.com>
Date: Tue, 21 Jul 2026 07:14:46 -0700
Subject: [PATCH] [floss] Keep BluetoothSocketFloss alive across listen callbacks

DoConnectionStateChanged is dispatched through a WeakPtr-bound
RepeatingCallback, so the call stack does not hold a strong reference to
the socket. After CompleteListen, the only scoped_refptr to the socket
may be the one bound inside pending_listen_ready_callback_ itself.
Running that callback can therefore release the last reference before
the rest of the method writes is_accepting_ and issues the Accept call.

Hold a local scoped_refptr for the duration of the method so the object
is guaranteed to outlive the listen-ready, listen-close, and
accept-error callbacks it invokes. Add a unit test that exercises a
listen-ready callback which drops its socket reference.

Bug: b:516947491
Test: out/cros_asan/device_unittests --gtest_filter='BluetoothSocketFlossTest.ListenReadyCallbackDropsLastRef'
Change-Id: I90ab7ae2bb01687d94a863e4f61bdf067fc20c78
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8114226
Reviewed-by: Hsin-chen Chuang <chharry@google.com>
Commit-Queue: Gigi Joseph (xWF) <gigij@google.com>
Reviewed-by: Michael Sun <michaelfsun@google.com>
Cr-Commit-Position: refs/heads/main@{#1665454}
---
diff --git a/device/bluetooth/floss/bluetooth_socket_floss.cc b/device/bluetooth/floss/bluetooth_socket_floss.cc
index 376a8157..d29dcc5 100644
--- a/device/bluetooth/floss/bluetooth_socket_floss.cc
+++ b/device/bluetooth/floss/bluetooth_socket_floss.cc
@@ -211,6 +211,10 @@
     FlossSocketManager::ServerSocketState state,
     FlossSocketManager::FlossListeningSocket socket,
     FlossDBusClient::BtifStatus status) {
+  // The callbacks invoked below may hold the last reference to |this|, so keep
+  // a local reference for the duration of the method.
+  scoped_refptr<BluetoothSocketFloss> self(this);
+
   // If we don't already have socket info, store it.
   if (!listening_socket_info_) {
     listening_socket_info_ = socket;
diff --git a/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc b/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
index c1205a3..987f8cbf 100644
--- a/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
+++ b/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
@@ -371,4 +371,48 @@
   DisconnectSocket(server_socket.get());
 }
 
+// Regression test for a use-after-free in DoConnectionStateChanged.
+//
+// After CompleteListen(), the sole strong reference to the BluetoothSocketFloss
+// is held by its own |pending_listen_ready_callback_| member.
+// DoConnectionStateChanged is dispatched via a WeakPtr-bound RepeatingCallback
+// (no strong ref on the stack) and synchronously runs that callback. If the
+// callee drops the scoped_refptr without retaining it (which happens in
+// production when chrome.bluetoothSocket.close() removed the BluetoothApiSocket
+// before listen completed), |this| is freed mid-method and the subsequent
+// member writes/reads are use-after-free.
+TEST_F(BluetoothSocketFlossTest, ListenReadyCallbackDropsLastRef) {
+  FlossSocketManager::SocketId id = GetFakeFlossSocketManager()->GetNextId();
+
+  // Simulates BluetoothSocketListenFunction::OnCreateService when
+  // GetSocket(socket_id()) returns nullptr: the scoped_refptr parameter is
+  // destroyed without being retained anywhere.
+  auto drop_last_ref = [](scoped_refptr<device::BluetoothSocket> socket) {
+    // |socket| destructs here. Without a self-ref in DoConnectionStateChanged
+    // this is the last reference -> refcount hits 0 -> ~BluetoothSocketFloss().
+  };
+
+  adapter_->CreateRfcommService(
+      device::BluetoothUUID(FakeFlossSocketManager::kRfcommUuid),
+      BluetoothAdapter::ServiceOptions(),
+      base::BindLambdaForTesting(drop_last_ref),
+      base::BindLambdaForTesting(
+          [](const std::string& msg) { FAIL() << msg; }));
+
+  // At this point CompleteListen has run synchronously (FakeFlossSocketManager
+  // invokes the response callback inline), so the only owner of the
+  // BluetoothSocketFloss is its own pending_listen_ready_callback_ member.
+  //
+  // SendSocketReady dispatches DoConnectionStateChanged via the WeakPtr-bound
+  // ConnectionStateChanged callback. Inside, pending_listen_ready_callback_ is
+  // run, |drop_last_ref| destroys the last scoped_refptr, the object is freed,
+  // and DoConnectionStateChanged then writes is_accepting_ and dereferences
+  // weak_ptr_factory_ on freed heap. ASan reports heap-use-after-free here.
+  GetFakeFlossSocketManager()->SendSocketReady(
+      id, device::BluetoothUUID(FakeFlossSocketManager::kRfcommUuid),
+      FlossDBusClient::BtifStatus::kSuccess);
+
+  base::RunLoop().RunUntilIdle();
+}
+
 }  // namespace floss
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc b/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
index c1205a3..987f8cbf 100644
--- a/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
+++ b/device/bluetooth/floss/bluetooth_socket_floss_unittest.cc
@@ -371,4 +371,48 @@
   DisconnectSocket(server_socket.get());
 }
 
+// Regression test for a use-after-free in DoConnectionStateChanged.
+//
+// After CompleteListen(), the sole strong reference to the BluetoothSocketFloss
+// is held by its own |pending_listen_ready_callback_| member.
+// DoConnectionStateChanged is dispatched via a WeakPtr-bound RepeatingCallback
+// (no strong ref on the stack) and synchronously runs that callback. If the
+// callee drops the scoped_refptr without retaining it (which happens in
+// production when chrome.bluetoothSocket.close() removed the BluetoothApiSocket
+// before listen completed), |this| is freed mid-method and the subsequent
+// member writes/reads are use-after-free.
+TEST_F(BluetoothSocketFlossTest, ListenReadyCallbackDropsLastRef) {
+  FlossSocketManager::SocketId id = GetFakeFlossSocketManager()->GetNextId();
+
+  // Simulates BluetoothSocketListenFunction::OnCreateService when
+  // GetSocket(socket_id()) returns nullptr: the scoped_refptr parameter is
+  // destroyed without being retained anywhere.
+  auto drop_last_ref = [](scoped_refptr<device::BluetoothSocket> socket) {
+    // |socket| destructs here. Without a self-ref in DoConnectionStateChanged
+    // this is the last reference -> refcount hits 0 -> ~BluetoothSocketFloss().
+  };
+
+  adapter_->CreateRfcommService(
+      device::BluetoothUUID(FakeFlossSocketManager::kRfcommUuid),
+      BluetoothAdapter::ServiceOptions(),
+      base::BindLambdaForTesting(drop_last_ref),
+      base::BindLambdaForTesting(
+          [](const std::string& msg) { FAIL() << msg; }));
+
+  // At this point CompleteListen has run synchronously (FakeFlossSocketManager
+  // invokes the response callback inline), so the only owner of the
+  // BluetoothSocketFloss is its own pending_listen_ready_callback_ member.
+  //
+  // SendSocketReady dispatches DoConnectionStateChanged via the WeakPtr-bound
+  // ConnectionStateChanged callback. Inside, pending_listen_ready_callback_ is
+  // run, |drop_last_ref| destroys the last scoped_refptr, the object is freed,
+  // and DoConnectionStateChanged then writes is_accepting_ and dereferences
+  // weak_ptr_factory_ on freed heap. ASan reports heap-use-after-free here.
+  GetFakeFlossSocketManager()->SendSocketReady(
+      id, device::BluetoothUUID(FakeFlossSocketManager::kRfcommUuid),
+      FlossDBusClient::BtifStatus::kSuccess);
+
+  base::RunLoop().RunUntilIdle();
+}
+
 }  // namespace floss
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in BluetoothSocketFloss::DoConnectionStateChanged

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 vulnerability exists in BluetoothSocketFloss::DoConnectionStateChanged because invoking the pending listen callback can synchronously release the final reference to the socket. If the socket is deleted during the callback, subsequent code in the method attempts to access and modify member variables of the destroyed object. This could potentially lead to memory corruption or instability in the browser process.

Affected files:

  • device/bluetooth/floss/bluetooth_socket_floss.cc

Estimated timestamp from git blame: 2023-09-01

Root Cause Analysis

BluetoothSocketFloss (defined in device/bluetooth/floss/bluetooth_socket_floss.h) is a reference-counted thread-safe object. During the asynchronous socket setup phase, the final strong reference to a BluetoothSocketFloss instance may be held within pending_listen_ready_callback_.

Inside BluetoothSocketFloss::DoConnectionStateChanged (located in device/bluetooth/floss/bluetooth_socket_floss.cc), the class executes this callback:

void BluetoothSocketFloss::DoConnectionStateChanged(
    FlossSocketManager::ServerSocketState state,
    FlossSocketManager::FlossListeningSocket socket,
    FlossDBusClient::BtifStatus status) {
  if (!listening_socket_info_) {
    listening_socket_info_ = socket;
  }
  if (pending_listen_ready_callback_) {
    std::move(pending_listen_ready_callback_).Run();   // <--- Potential synchronous destruction of |this|
  }
  is_accepting_ = false;                               // <--- Use-After-Free write
  if (state == FlossSocketManager::ServerSocketState::kReady &&
      status == FlossDBusClient::BtifStatus::kSuccess) {
    FlossDBusManager::Get()->GetSocketManager()->Accept(
        listening_socket_info_->id, std::nullopt,      // <--- Use-After-Free read
        base::BindOnce(&BluetoothSocketFloss::CompleteAccept,
                       weak_ptr_factory_.GetWeakPtr()));
    return;
  }
  ...
}

Because the callback is run synchronously, if the owner or the caller of the socket releases their reference during the callback execution (for example, if an extension closes the socket prematurely), the reference count of the BluetoothSocketFloss instance can drop to zero. This triggers immediate synchronous deletion of this. When control returns to DoConnectionStateChanged, the function continues to execute, performing a write (is_accepting_ = false) and reading from listening_socket_info_, which results in a use-after-free condition.

Potential Trigger Scenario

Note: These are potential steps derived from static analysis of the codebase, as our environment does not support dynamic execution.

  1. An application or extension creates a Bluetooth socket and requests to listen on a service via listenUsingRfcomm or listenUsingL2cap.
  2. The system initiates the asynchronous D-Bus call, storing the completion callbacks.
  3. The application immediately requests to close or release the socket before the D-Bus response is fully processed, removing the external references to the socket.
  4. When the Floss daemon responds, DoConnectionStateChanged is invoked on the UI thread. At this point, the callback stored in pending_listen_ready_callback_ holds the last remaining strong reference.
  5. Moving and running pending_listen_ready_callback_ destroys the reference inside the callback’s scope. If the callback resolves the request by noticing the socket has been closed or removed from the active registry, the reference count drops to 0, destroying the socket object.
  6. Control returns to DoConnectionStateChanged, leading to member variable access on the deleted object.

To prevent the object from being destroyed while its own method is on the call stack, acquire a strong reference to this at the beginning of DoConnectionStateChanged. This keeps the object alive until the function scope exits, regardless of any synchronous reference drops during the callback execution.

void BluetoothSocketFloss::DoConnectionStateChanged(
    FlossSocketManager::ServerSocketState state,
    FlossSocketManager::FlossListeningSocket socket,
    FlossDBusClient::BtifStatus status) {
  scoped_refptr<BluetoothSocketFloss> self(this); // Protects |this| from deletion

  if (!listening_socket_info_) {
    listening_socket_info_ = socket;
  }
  ...
}

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.

Raised in root component due to access or custom field issues on 1131776

View on issue tracker