|
| 1 | +// SPDX-License-Identifier: BSD-3-Clause |
| 2 | +// Copyright (c) 2009-2025, Intel Corporation |
| 3 | + |
| 4 | +// Regression test for the out-of-bounds write in |
| 5 | +// basic_socketbuf::overflow() (src/pcm-sensor-server.cpp, lines 1226-1237). |
| 6 | +// |
| 7 | +// The vulnerable overflow() writes the incoming character into *pptr() |
| 8 | +// *before* flushing the full put-area to the socket. When the put area is |
| 9 | +// full, pptr() == epptr(), i.e. it points one past the end of outputBuffer_, |
| 10 | +// which is the first byte of the adjacent inputBuffer_. This test drives the |
| 11 | +// real basic_socketbuf template through a socketpair, fills the put area to |
| 12 | +// the brim, triggers overflow(), and verifies that inputBuffer_[0] is not |
| 13 | +// corrupted. |
| 14 | +// |
| 15 | +// With the bug in place the assertion on inputBuffer_[0] fails (and, when the |
| 16 | +// binary is built with AddressSanitizer via -DPCM_NO_ASAN=OFF, the underlying |
| 17 | +// intra-object OOB write / OOB read in send() is also detected). Once |
| 18 | +// overflow() is fixed to flush first and then store the character into the |
| 19 | +// emptied buffer, the test passes. |
| 20 | + |
| 21 | +#include <sys/socket.h> |
| 22 | +#include <sys/types.h> |
| 23 | +#include <unistd.h> |
| 24 | +#include <atomic> |
| 25 | +#include <cstring> |
| 26 | +#include <memory> |
| 27 | +#include <thread> |
| 28 | +#include <vector> |
| 29 | + |
| 30 | +// Pull the real basic_socketbuf template out of pcm-sensor-server.cpp without |
| 31 | +// bringing in its main(). The same mechanism is already used by |
| 32 | +// tests/pcm-sensor-server-fuzz.cpp. |
| 33 | +#define UNIT_TEST 1 |
| 34 | +#include "../../src/pcm-sensor-server.cpp" |
| 35 | +#undef UNIT_TEST |
| 36 | + |
| 37 | +#include <gtest/gtest.h> |
| 38 | + |
| 39 | +namespace { |
| 40 | + |
| 41 | +// SIZE matches the instantiation used by basic_socketstream::buf_type |
| 42 | +// (see pcm-sensor-server.cpp line 1363). |
| 43 | +constexpr std::size_t kSocketBufSize = 16385; |
| 44 | + |
| 45 | +// Subclass that exposes the protected buffer members so the test can inspect |
| 46 | +// inputBuffer_[0] after triggering overflow(). |
| 47 | +class ProbeSocketBuf : public basic_socketbuf<kSocketBufSize, char> { |
| 48 | +public: |
| 49 | + using basic_socketbuf<kSocketBufSize, char>::inputBuffer_; |
| 50 | + using basic_socketbuf<kSocketBufSize, char>::outputBuffer_; |
| 51 | +}; |
| 52 | + |
| 53 | +// Drain the peer end of a socketpair in a background thread so that the |
| 54 | +// server-side send() inside writeToSocket() never blocks, regardless of what |
| 55 | +// the kernel's default socket buffer sizes happen to be on the CI runner. |
| 56 | +class SocketDrainer { |
| 57 | + SocketDrainer & operator = (const SocketDrainer &) = delete; |
| 58 | + SocketDrainer(const SocketDrainer &) = delete; |
| 59 | +public: |
| 60 | + explicit SocketDrainer(int fd) : fd_(fd), stop_(false) { |
| 61 | + thread_ = std::thread([this]() { |
| 62 | + char buf[4096]; |
| 63 | + while (!stop_.load()) { |
| 64 | + ssize_t n = ::recv(fd_, buf, sizeof(buf), 0); |
| 65 | + if (n <= 0) { |
| 66 | + break; |
| 67 | + } |
| 68 | + received_.insert(received_.end(), buf, buf + n); |
| 69 | + } |
| 70 | + }); |
| 71 | + } |
| 72 | + |
| 73 | + ~SocketDrainer() { |
| 74 | + stop_.store(true); |
| 75 | + if (fd_ >= 0) { |
| 76 | + ::shutdown(fd_, SHUT_RDWR); |
| 77 | + } |
| 78 | + if (thread_.joinable()) { |
| 79 | + thread_.join(); |
| 80 | + } |
| 81 | + if (fd_ >= 0) { |
| 82 | + ::close(fd_); |
| 83 | + fd_ = -1; |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + const std::vector<char>& data() const { return received_; } |
| 88 | + |
| 89 | +private: |
| 90 | + int fd_; |
| 91 | + std::atomic<bool> stop_; |
| 92 | + std::thread thread_; |
| 93 | + std::vector<char> received_; |
| 94 | +}; |
| 95 | + |
| 96 | +} // namespace |
| 97 | + |
| 98 | +TEST(PcmSensorServerOverflowTest, OverflowDoesNotWritePastOutputBuffer) |
| 99 | +{ |
| 100 | + int sv[2]; |
| 101 | + ASSERT_EQ(0, ::socketpair(AF_UNIX, SOCK_STREAM, 0, sv)) |
| 102 | + << "socketpair failed: " << std::strerror(errno); |
| 103 | + |
| 104 | + // Peer drains whatever the socketbuf sends. |
| 105 | + SocketDrainer drainer(sv[1]); |
| 106 | + |
| 107 | + auto buf = std::make_unique<ProbeSocketBuf>(); |
| 108 | + buf->setSocket(sv[0]); |
| 109 | + |
| 110 | + // Plant a distinctive sentinel in inputBuffer_[0]. With the vulnerable |
| 111 | + // overflow(), this byte is the one clobbered by "*pptr() = ch" when the |
| 112 | + // put area is full. |
| 113 | + constexpr unsigned char kSentinel = 0xAA; |
| 114 | + constexpr unsigned char kOverflowChar = 0x5A; // 'Z' |
| 115 | + static_assert(kSentinel != kOverflowChar, |
| 116 | + "sentinel and overflow byte must differ to detect the OOB write"); |
| 117 | + buf->inputBuffer_[0] = static_cast<char>(kSentinel); |
| 118 | + |
| 119 | + // Fill the put area completely. After exactly SIZE sputc() calls, |
| 120 | + // pptr() == epptr() but overflow() has not yet been invoked. |
| 121 | + for (std::size_t i = 0; i < kSocketBufSize; ++i) { |
| 122 | + ASSERT_NE(std::char_traits<char>::eof(), buf->sputc('X')) |
| 123 | + << "sputc failed while filling the put area at index " << i; |
| 124 | + } |
| 125 | + |
| 126 | + // The next sputc() must trigger overflow(kOverflowChar). With the |
| 127 | + // vulnerable implementation this is where *pptr() = ch writes past the |
| 128 | + // end of outputBuffer_ and into inputBuffer_[0]. |
| 129 | + ASSERT_NE(std::char_traits<char>::eof(), |
| 130 | + buf->sputc(static_cast<char>(kOverflowChar))) |
| 131 | + << "sputc returned eof when triggering overflow()"; |
| 132 | + |
| 133 | + // After overflow() returns, the put area should have been flushed to the |
| 134 | + // socket and the sentinel in inputBuffer_[0] must still be intact. |
| 135 | + EXPECT_EQ(kSentinel, static_cast<unsigned char>(buf->inputBuffer_[0])) |
| 136 | + << "basic_socketbuf::overflow() corrupted inputBuffer_[0]: " |
| 137 | + << "wrote ch=0x" << std::hex << static_cast<int>(kOverflowChar) |
| 138 | + << " past the end of outputBuffer_ (SIZE=" << std::dec |
| 139 | + << kSocketBufSize << "). See pcm-sensor-server.cpp " |
| 140 | + << "basic_socketbuf::overflow() (lines 1226-1237): the character " |
| 141 | + << "must be stored only *after* the full put-area has been flushed " |
| 142 | + << "and the put pointers reset."; |
| 143 | + |
| 144 | + // Release the socket before the buf destructor runs sync(); this keeps |
| 145 | + // the test output stable regardless of whether the drainer has already |
| 146 | + // exited. Resetting buf closes sv[0]. |
| 147 | + buf.reset(); |
| 148 | + // sv[1] is closed by the drainer's shutdown. |
| 149 | +} |
0 commit comments