Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker516947491
Fix commit5d8fc68ff55c (chromium/src) +48/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

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
From 5d8fc68ff55c002566acc9852f491ef60639d203 Mon Sep 17 00:00:00 2001
From: Gigi Joseph <[email protected]>
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 <[email protected]>
Commit-Queue: Gigi Joseph (xWF) <[email protected]>
Reviewed-by: Michael Sun <[email protected]>
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 [email protected]

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