Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker498000415
Fix commite9dafc9b0a2c (chromium/src) +14/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Files Changed

  • device/bluetooth/bluetooth_socket_android.cc
  • device/bluetooth/bluetooth_socket_android.h
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);
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • device/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.

  1. When Disconnect() is called, it posts DoDisconnect() to the socket_thread_.
  2. DoDisconnect() stops and destroys the thread:
    // device/bluetooth/bluetooth_socket_android.cc:125
    receiving_thread_->Stop();
    receiving_thread_.reset(); // Destroys the base::Thread object
    
  3. Concurrently, Receive() is called on the UI thread (e.g., via BluetoothSerialPortImpl::ReadMore()). Receive() performs an unsynchronized read of receiving_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.

  1. Compromise a Renderer: The attacker gains execution in a renderer process.
  2. 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).
  3. Initiate Race: The compromised renderer opens the serial port and concurrently sends a Close() Mojo message immediately followed by a StartReading() Mojo message to the mojom::SerialPort interface.
  4. 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.
  5. Execution: The StartReading() call reaches BluetoothSocketAndroid::Receive() on the UI thread, hits the race condition, and dereferences the attacker-controlled vtable via the delegate_ 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.

View on issue tracker