CVE-2026-79219
Overview
Background
- `BluetoothSocketFloss`
- a reference-counted (
scoped_refptr) socket object in the Floss Bluetooth backend that owns listen/accept state such asis_accepting_and aweak_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 updatesis_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.
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
- Start a listening socket
Through the
chrome.bluetoothSocketextension API, create an RFCOMM service so aBluetoothSocketFlossis created and itspending_listen_ready_callback_is set up. - Drop the external owner
Call
chrome.bluetoothSocket.close()soGetSocket(socket_id())returns nullptr inBluetoothSocketListenFunction::OnCreateService, meaning the listen-ready callback’sscoped_refptrparameter will be destroyed without being retained anywhere. - Reduce to a single owner
With
CompleteListenhaving run, the only strong reference to theBluetoothSocketFlossis the one bound inside its ownpending_listen_ready_callback_member. - Trigger the free mid-method
A socket-ready state change dispatches
DoConnectionStateChangedvia theWeakPtr-bound callback; runningpending_listen_ready_callback_drops the last reference,~BluetoothSocketFloss()frees the object, and the method’s subsequent write tois_accepting_and use ofweak_ptr_factory_operate on freed heap (use-after-free confirmed by the ASan regression test).
Impact Assessment
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
| 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
Audit Directions
- Callback that may hold the last self-referenceAudit 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 solescoped_refptr. - Post-callback member accessLook 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 lifetimesReview the Bluetooth extension paths where
close()/removal of the API socket can run before an asynchronous listen/connect completes, dropping references at unexpected points.
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