Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Bluetooth
DescriptionUse after free in Bluetooth
ComponentBluetooth
Bug ClassUAF
Tracker499022239
Fix commite940e425d4fa (chromium/src) +30/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
TEST_F
device/bluetooth/socket_unittest.cc
modified

Files Changed

  • device/bluetooth/socket.cc
  • device/bluetooth/socket_unittest.cc
From e940e425d4fadb301240e0c6aa144b15cf96a735 Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@chromium.org>
Date: Fri, 29 May 2026 17:12:12 -0700
Subject: [PATCH] [Bluetooth] Fix Use-After-Unmap vulnerability in bluetooth::Socket

Resolve a Use-After-Unmap vulnerability in Socket::SendMore() by
replacing the non-owning net::WrappedIOBuffer with an owning
net::IOBufferWithSize for asynchronous socket writes.

Previously, Socket::SendMore() wrapped a base::span pointing directly
into the Mojo Core shared memory ring buffer with a
net::WrappedIOBuffer. Because WrappedIOBuffer is non-owning and the
socket write occurs asynchronously on a background socket thread, the
buffer's lifetime could exceed the mapping of the Mojo shared memory
ring buffer. If the Mojo data pipe was closed or reset before the
background socket write completed, Mojo Core synchronously unmapped the
shared memory, leaving the background thread with a dangling pointer,
resulting in a Use-After-Unmap crash.

To resolve this, we replace WrappedIOBuffer with an owning
IOBufferWithSize. Before posting the async write operation, the data is
copied from the Mojo ring buffer into the newly allocated
IOBufferWithSize. Since the IOBufferWithSize owns its heap storage and
is ref-counted by the background socket task, the memory remains valid
throughout the asynchronous write execution regardless of the Mojo
stream or Socket lifecycle.

Bug: 499022239
Test: autoninja -C out/Default device_unittests && ./out/Default/device_unittests --gtest_filter='SocketTest.*' --single-process-tests
Change-Id: I4c0f2d86c76f31031c2df80343405eb9f42bf916
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7886436
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1638838}
---

diff --git a/device/bluetooth/socket.cc b/device/bluetooth/socket.cc
index c15adbd..05ba38f 100644
--- a/device/bluetooth/socket.cc
+++ b/device/bluetooth/socket.cc
@@ -6,15 +6,12 @@
 
 #include <string>
 #include <utility>
-#include <vector>
 
 #include "base/containers/span.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
 #include "base/logging.h"
-#include "base/memory/ptr_util.h"
 #include "base/numerics/safe_conversions.h"
-#include "base/strings/string_view_util.h"
 #include "device/bluetooth/bluetooth_socket.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/receiver.h"
@@ -169,9 +166,11 @@
     return;
   }
 
-  std::string_view chars = base::as_string_view(pending_read_buffer);
-  bluetooth_socket_->Send(base::MakeRefCounted<net::WrappedIOBuffer>(chars),
-                          chars.size(),
+  auto io_buffer =
+      base::MakeRefCounted<net::IOBufferWithSize>(pending_read_buffer.size());
+  io_buffer->span().copy_from(pending_read_buffer);
+  const int buffer_size = io_buffer->size();
+  bluetooth_socket_->Send(std::move(io_buffer), buffer_size,
                           base::BindOnce(&Socket::OnBluetoothSocketSend,
                                          weak_ptr_factory_.GetWeakPtr()),
                           base::BindOnce(&Socket::OnBluetoothSocketSendError,
diff --git a/device/bluetooth/socket_unittest.cc b/device/bluetooth/socket_unittest.cc
index 5a0e4aac..08a15f9 100644
--- a/device/bluetooth/socket_unittest.cc
+++ b/device/bluetooth/socket_unittest.cc
@@ -202,4 +202,29 @@
   WriteAndVerifySend("message_6", /*success=*/true);
 }
 
+TEST_F(SocketTest, TestSend_ClosedDuringSend) {
+  EXPECT_FALSE(send_stream_->QuerySignalsState().never_writable());
+  EXPECT_FALSE(fake_bluetooth_socket_->HasSendArgs());
+
+  std::string message = "test_message";
+  size_t actually_written_bytes = 0;
+  EXPECT_EQ(MOJO_RESULT_OK,
+            send_stream_->WriteData(base::as_byte_span(message),
+                                    MOJO_WRITE_DATA_FLAG_NONE,
+                                    actually_written_bytes));
+  EXPECT_EQ(message.size(), actually_written_bytes);
+
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(fake_bluetooth_socket_->HasSendArgs());
+  auto send_args = fake_bluetooth_socket_->TakeSendArgs();
+
+  send_stream_.reset();
+
+  base::RunLoop().RunUntilIdle();
+
+  auto success_callback = std::move(std::get<2>(*send_args));
+  std::move(success_callback).Run(/*num_bytes_sent=*/message.size());
+}
+
 }  // namespace bluetooth
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/device/bluetooth/socket_unittest.cc b/device/bluetooth/socket_unittest.cc
index 5a0e4aac..08a15f9 100644
--- a/device/bluetooth/socket_unittest.cc
+++ b/device/bluetooth/socket_unittest.cc
@@ -202,4 +202,29 @@
   WriteAndVerifySend("message_6", /*success=*/true);
 }
 
+TEST_F(SocketTest, TestSend_ClosedDuringSend) {
+  EXPECT_FALSE(send_stream_->QuerySignalsState().never_writable());
+  EXPECT_FALSE(fake_bluetooth_socket_->HasSendArgs());
+
+  std::string message = "test_message";
+  size_t actually_written_bytes = 0;
+  EXPECT_EQ(MOJO_RESULT_OK,
+            send_stream_->WriteData(base::as_byte_span(message),
+                                    MOJO_WRITE_DATA_FLAG_NONE,
+                                    actually_written_bytes));
+  EXPECT_EQ(message.size(), actually_written_bytes);
+
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_TRUE(fake_bluetooth_socket_->HasSendArgs());
+  auto send_args = fake_bluetooth_socket_->TakeSendArgs();
+
+  send_stream_.reset();
+
+  base::RunLoop().RunUntilIdle();
+
+  auto success_callback = std::move(std::get<2>(*send_args));
+  std::move(success_callback).Run(/*num_bytes_sent=*/message.size());
+}
+
 }  // namespace bluetooth
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Unmap Information Leak via Bluetooth Socket Send

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 without the security team.

Overview: A Use-After-Unmap vulnerability exists in device/bluetooth/Socket when sending data over a Bluetooth socket. The browser wraps a Mojo shared memory pointer in a non-owning net::WrappedIOBuffer and passes it to an asynchronous socket thread, leading to a potential information leak if the Mojo data pipe is closed while the write is pending.

Affected files:

  • device/bluetooth/socket.cc
  • device/bluetooth/bluetooth_socket_net.cc

Estimated timestamp from git blame: 2024-06-13

Mechanism

In device/bluetooth/socket.cc, Socket::SendMore() reads outgoing data from a Mojo data pipe. It calls send_stream_->BeginReadData(), which returns a base::span pointing directly into Mojo Core’s underlying shared memory ring buffer.

This span is then wrapped in a net::WrappedIOBuffer and passed to bluetooth_socket_->Send() for asynchronous transmission:

void Socket::SendMore() {
  // ...
  MojoResult result = send_stream_->BeginReadData(MOJO_WRITE_DATA_FLAG_NONE, pending_read_buffer);
  // ...
  std::string_view chars = base::as_string_view(pending_read_buffer);
  bluetooth_socket_->Send(base::MakeRefCounted<net::WrappedIOBuffer>(chars),
                          chars.size(), ...);
}

net::WrappedIOBuffer is explicitly designed as a non-owning wrapper and is documented as “ONLY safe for synchronous use.” However, on platforms using BluetoothSocketNet (such as Linux and ChromeOS), the Send() method queues the request and posts a task to a background socket_thread_ to perform the blocking I/O.

The Vulnerability

Because the WrappedIOBuffer is passed across threads for an asynchronous operation, its lifetime outlives the synchronous scope of SendMore(). If the Mojo data pipe consumer handle (send_stream_) is reset before the background thread completes the socket write, Mojo Core will immediately and synchronously unmap (munmap) the shared memory ring buffer from the browser’s address space.

Because this memory is allocated via mmap rather than PartitionAlloc, it is not protected by MiraclePtr (BackupRefPtr). The background socket_thread_ retains a dangling pointer inside the WrappedIOBuffer parked in net::SocketPosix::write_buf_.

Potential Attack Scenario

  1. An attacker compromises a process with access to device::mojom::Adapter (e.g., the utility process handling Nearby Share on ChromeOS).
  2. The attacker uses ConnectToServiceInsecurely to establish a Bluetooth connection to an attacker-controlled remote device, yielding a mojom::Socket and its associated data pipes.
  3. The attacker writes up to 64KB (the default pipe capacity) into the send stream.
  4. The attacker manipulates the remote Bluetooth device to stall L2CAP/RFCOMM network acknowledgments. This fills the kernel’s send buffer, causing the browser’s background socket_thread_ to hit EWOULDBLOCK during the send() call. net::SocketPosix stores the WrappedIOBuffer in write_buf_ and waits for the socket to become writable.
  5. The compromised utility process closes its end of the Mojo send stream.
  6. The browser’s UI thread processes the closure, calls ShutdownSend(), and resets send_stream_. This causes Mojo Core to synchronously munmap the 64KB shared memory ring buffer.
  7. The attacker sprays the browser’s virtual address space (e.g., via other IPC channels) to reallocate the newly unmapped 64KB block with sensitive browser memory.
  8. The attacker’s remote device resumes sending acknowledgments, unblocking the socket.
  9. The socket_thread_ wakes up and re-attempts the send() call using the dangling WrappedIOBuffer, reading up to 64KB of the reallocated, sensitive memory and transmitting it to the attacker’s Bluetooth device.

Suggested Fix

To fix this issue, Socket::SendMore() should not use net::WrappedIOBuffer for asynchronous operations. The data obtained from BeginReadData() should be copied into an owning buffer type before being passed to bluetooth_socket_->Send().

Alternatively, if avoiding a copy is strictly necessary, Socket::SendMore() should use a specialized adapter (similar to network::MojoToNetIOBuffer) that takes ownership of a MojoHandle to ensure the Mojo memory remains mapped until the IO buffer is destroyed.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results 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