CVE-2026-79219
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdevice/bluetooth/floss/bluetooth_socket_floss.cc |
modified | |
TEST_Fdevice/bluetooth/floss/bluetooth_socket_floss_unittest.cc |
modified |
Files Changed
device/bluetooth/floss/bluetooth_socket_floss.ccdevice/bluetooth/floss/bluetooth_socket_floss_unittest.cc
Patch
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
Regression Test / PoC
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
Original Bug Report
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.
- An application or extension creates a Bluetooth socket and requests to listen on a service via
listenUsingRfcommorlistenUsingL2cap. - The system initiates the asynchronous D-Bus call, storing the completion callbacks.
- 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.
- When the Floss daemon responds,
DoConnectionStateChangedis invoked on the UI thread. At this point, the callback stored inpending_listen_ready_callback_holds the last remaining strong reference. - 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. - Control returns to
DoConnectionStateChanged, leading to member variable access on the deleted object.
Recommended Remediation
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