CVE-2026-17737
Overview
Background
- `BluetoothSocketAndroid`
- the browser-process C++ wrapper around an Android Bluetooth socket, whose
Disconnecttears down the connection and its helper threads. - `socket_thread_`
- a dedicated worker thread (the “Socket Thread”) on which blocking Java socket I/O such as
DoDisconnectruns. - `receiving_thread_`
- a
base::Threadowned by the object that runs the socket’s receive loop; abase::Threadmust be stopped and destroyed on the sequence that manages it. - `PostTaskAndReply`
- posts a task to a target sequence and then posts a reply closure back to the sequence that made the call (here the UI thread), giving a clean way to split work across threads.
Root Cause Analysis
Before the patch, Disconnect posted a single DoDisconnect task to socket_thread_, and DoDisconnect both closed the Java socket and called receiving_thread_->Stop() followed by receiving_thread_.reset() — i.e. it stopped and freed the receiving_thread_ object while running on the Socket Thread rather than on the UI thread that manages it. Stopping and destroying a base::Thread from a foreign sequence violates its thread-affinity/lifetime invariant, so in-flight receive tasks or state associated with receiving_thread_ (and the object it references) could still be touched after teardown, producing the use-after-free.
The fix rewrites Disconnect to use PostTaskAndReply: the Socket-Thread portion (DoDisconnect) now only performs the Java close(), and the new reply, PostDisconnect, performs receiving_thread_->Stop(), receiving_thread_.reset(), and finally runs success_callback — all on the UI thread. Because the reply runs on the originating (UI) sequence, the thread is now stopped and destroyed on the correct sequence, restoring the invariant. The comment added in PostDisconnect states the intent explicitly: stop and destroy receiving_thread_ on the UI thread, not on the Socket Thread.
receiving_thread_ from within a task running on the Socket Thread; the fix splits the disconnect into a Socket-Thread close and a UI-thread PostDisconnect reply so the thread is torn down on the sequence that owns it.Attack Path
- Establish a Bluetooth socket
Content that can reach the Android Bluetooth socket path (via the Web Bluetooth / device Bluetooth stack) causes a
BluetoothSocketAndroidwith an activereceiving_thread_to exist. (inferred) - Trigger disconnect
A
Disconnectcall postsDoDisconnecttosocket_thread_, which on the old code path closes the Java socket and then callsreceiving_thread_->Stop()/.reset()on the Socket Thread. - Cross-thread teardown race
Stopping and freeing
receiving_thread_off its managing sequence lets receive-loop work or object state be accessed after the thread/object is torn down, yielding a use-after-free. (inferred) - Exploit the freed memory With control over reallocation timing, the dangling access can be steered toward attacker-influenced memory. (inferred)
Impact Assessment
(inferred). The advisory states no exploited-in-the-wild status.Files Changed
device/bluetooth/bluetooth_socket_android.ccdevice/bluetooth/bluetooth_socket_android.h
Audit Directions
- base::Thread cross-sequence teardownSearch for
->Stop()/.reset()onbase::Threadmembers inside tasks posted to a different sequence; these should run on the owning sequence, often viaPostTaskAndReply. - PostTask that mixes I/O and lifetime managementFlag worker-thread tasks that both perform blocking work and destroy objects/threads owned by the caller’s sequence; the lifetime work belongs in a reply.
- Callback ordering after teardownVerify that completion callbacks run after, and on the same sequence as, the destruction of the resources they signal to prevent use-after-free on shared members.
Patch
From e9dafc9b0a2ccb75075a93eeaaac1e45c4a82050 Mon Sep 17 00:00:00 2001
From: Takashi Sakamoto <tasak@google.com>
Date: Thu, 02 Apr 2026 01:54:33 -0700
Subject: [PATCH] Make receiving_thread_ stop and destroy on UI thread.
Bug: 498000415
Change-Id: I42a7bffac1e7c2f5b58f95fab4399aa69430c9e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718965
Reviewed-by: Keishi Hattori <keishi@chromium.org>
Reviewed-by: Colin Blundell <blundell@chromium.org>
Commit-Queue: Takashi Sakamoto <tasak@google.com>
Cr-Commit-Position: refs/heads/main@{#1609029}
---
diff --git a/device/bluetooth/bluetooth_socket_android.cc b/device/bluetooth/bluetooth_socket_android.cc
index b1742b1..1bbb74cd 100644
--- a/device/bluetooth/bluetooth_socket_android.cc
+++ b/device/bluetooth/bluetooth_socket_android.cc
@@ -112,20 +112,26 @@
}
void BluetoothSocketAndroid::Disconnect(base::OnceClosure success_callback) {
- socket_thread_->task_runner()->PostTask(
- FROM_HERE, base::BindOnce(&BluetoothSocketAndroid::DoDisconnect, this,
- std::move(success_callback)));
+ socket_thread_->task_runner()->PostTaskAndReply(
+ FROM_HERE,
+ base::BindOnce(&BluetoothSocketAndroid::DoDisconnect,
+ base::Unretained(this)),
+ base::BindOnce(&BluetoothSocketAndroid::PostDisconnect,
+ base::Unretained(this), std::move(success_callback)));
}
-void BluetoothSocketAndroid::DoDisconnect(base::OnceClosure success_callback) {
+void BluetoothSocketAndroid::DoDisconnect() {
CHECK(socket_thread_->task_runner()->RunsTasksInCurrentSequence());
Java_ChromeBluetoothSocket_close(AttachCurrentThread(), j_socket_);
+}
+void BluetoothSocketAndroid::PostDisconnect(
+ base::OnceClosure success_callback) {
+ // Stop and destroy `receiving_thread_` on UI thread, not on Socket Thread.
receiving_thread_->Stop();
receiving_thread_.reset();
-
- ui_task_runner_->PostTask(FROM_HERE, std::move(success_callback));
+ std::move(success_callback).Run();
}
void BluetoothSocketAndroid::Receive(
diff --git a/device/bluetooth/bluetooth_socket_android.h b/device/bluetooth/bluetooth_socket_android.h
index cbb9857..b0f888e 100644
--- a/device/bluetooth/bluetooth_socket_android.h
+++ b/device/bluetooth/bluetooth_socket_android.h
@@ -64,7 +64,8 @@
void DoConnect(base::OnceClosure success_callback,
ErrorCompletionCallback error_callback);
- void DoDisconnect(base::OnceClosure success_callback);
+ void DoDisconnect();
+ void PostDisconnect(base::OnceClosure success_callback);
void DoReceive(size_t buffer_size,
ReceiveCompletionCallback success_callback,
ReceiveErrorCompletionCallback error_callback);
Original Bug Report
Use-After-Free in BluetoothSocketAndroid via Race Condition
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A race condition in BluetoothSocketAndroid between the UI thread and the socket thread can lead to a Use-After-Free (UAF). A compromised renderer with Web Serial permissions could potentially trigger this by concurrently initiating close and read operations. This may allow an attacker to achieve arbitrary code execution in the highly privileged browser process.
Affected files:
device/bluetooth/bluetooth_socket_android.ccdevice/bluetooth/bluetooth_socket_android.h
Estimated timestamp from git blame: 2025-03-21
Summary
There is a potential Use-After-Free (UAF) vulnerability in the Android implementation of BluetoothSocketAndroid within the browser process. The issue stems from an unsynchronized cross-thread access to the receiving_thread_ member (a std::unique_ptr<base::Thread>). A data race between the UI thread reading this member and a background thread destroying it can lead to a dangling pointer dereference and potentially Remote Code Execution (RCE).
Technical Details
BluetoothSocketAndroid manages a dedicated base::Thread (receiving_thread_) for blocking JNI read operations.
- When
Disconnect()is called, it postsDoDisconnect()to thesocket_thread_. DoDisconnect()stops and destroys the thread:// device/bluetooth/bluetooth_socket_android.cc:125 receiving_thread_->Stop(); receiving_thread_.reset(); // Destroys the base::Thread object- Concurrently,
Receive()is called on the UI thread (e.g., viaBluetoothSerialPortImpl::ReadMore()).Receive()performs an unsynchronized read ofreceiving_thread_:// device/bluetooth/bluetooth_socket_android.cc:135 if (!receiving_thread_) { // ... error handling } receiving_thread_->task_runner()->PostTask(...);
A race condition occurs if the UI thread evaluates the !receiving_thread_ check as false, but is then preempted. The socket_thread_ can then execute receiving_thread_.reset(), freeing the memory. When the UI thread resumes, it dereferences the now-freed pointer (cached in a register) to call task_runner().
Inside base::Thread::task_runner(), it calls delegate_ ? delegate_->GetDefaultTaskRunner() : nullptr. Since the base::Thread object has been freed, if an attacker reclaims this heap memory, they can control the delegate_ pointer. Dereferencing delegate_ to call the virtual method GetDefaultTaskRunner() allows the attacker to hijack the instruction pointer.
Potential Steps to Trigger
Note: These are suggested steps; our tooling has not yet executed a working proof of concept to verify the exploit end-to-end.
- Compromise a Renderer: The attacker gains execution in a renderer process.
- Obtain Permissions: The attacker’s origin must have Web Serial API permissions for a paired Bluetooth device (e.g., previously granted by the user via a chooser dialog).
- Initiate Race: The compromised renderer opens the serial port and concurrently sends a
Close()Mojo message immediately followed by aStartReading()Mojo message to themojom::SerialPortinterface. - Heap Grooming: The attacker leverages other Mojo IPC calls to spray the browser process heap, aiming to reallocate the memory freed by
receiving_thread_.reset()with controlled data. - Execution: The
StartReading()call reachesBluetoothSocketAndroid::Receive()on the UI thread, hits the race condition, and dereferences the attacker-controlled vtable via thedelegate_pointer, leading to arbitrary code execution in the browser process.
Proposed Fix
Synchronize access to receiving_thread_. This can be achieved by protecting receiving_thread_ with a base::Lock, or by ensuring that all operations involving receiving_thread_ (including the null check and task posting in Receive) are trampolined to the socket_thread_ so they execute in the same sequence as DoConnect and DoDisconnect.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.