Skip to content

Commit 8ff5a6e

Browse files
Copilotrdementi
authored andcommitted
Add CI regression test reproducing basic_socketbuf::overflow() OOB write
Agent-Logs-Url: https://github.com/intel-innersource/applications.analyzers.pcm/sessions/a53c9d61-0218-4c3e-a1ac-0b14b1a73f3d Co-authored-by: rdementi <25432609+rdementi@users.noreply.github.com>
1 parent 79dc482 commit 8ff5a6e

2 files changed

Lines changed: 152 additions & 0 deletions

File tree

tests/utests/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ endif()
1717
file(GLOB LSPCI_TEST_FILES lspci-utest.cpp ${CMAKE_SOURCE_DIR}/src/lspci.cpp)
1818
file(GLOB PCM_IIO_TEST_FILES pcm-iio-utest.cpp ${CMAKE_SOURCE_DIR}/src/pcm-iio-pmu.cpp ${CMAKE_SOURCE_DIR}/src/pcm-iio-topology.cpp)
1919
file(GLOB READ_NUMBER_TEST_FILES read-number-utest.cpp)
20+
file(GLOB PCM_SENSOR_SERVER_OVERFLOW_TEST_FILES pcm-sensor-server-overflow-utest.cpp)
2021

2122
if(APPLE)
2223
set(LIBS PcmMsr Threads::Threads PCM_STATIC)
@@ -27,6 +28,7 @@ endif()
2728
add_executable(lspci-utest ${LSPCI_TEST_FILES})
2829
add_executable(pcm-iio-utest ${PCM_IIO_TEST_FILES})
2930
add_executable(read-number-utest ${READ_NUMBER_TEST_FILES})
31+
add_executable(pcm-sensor-server-overflow-utest ${PCM_SENSOR_SERVER_OVERFLOW_TEST_FILES})
3032

3133
configure_file(
3234
${CMAKE_SOURCE_DIR}/src/opCode-6-174.txt
@@ -55,7 +57,15 @@ target_link_libraries(
5557
${LIBS}
5658
)
5759

60+
target_link_libraries(
61+
pcm-sensor-server-overflow-utest
62+
GTest::gtest_main
63+
GTest::gmock_main
64+
${LIBS}
65+
)
66+
5867
include(GoogleTest)
5968
gtest_discover_tests(lspci-utest)
6069
gtest_discover_tests(pcm-iio-utest)
6170
gtest_discover_tests(read-number-utest)
71+
gtest_discover_tests(pcm-sensor-server-overflow-utest)
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
public:
58+
explicit SocketDrainer(int fd) : fd_(fd), stop_(false) {
59+
thread_ = std::thread([this]() {
60+
char buf[4096];
61+
while (!stop_.load()) {
62+
ssize_t n = ::recv(fd_, buf, sizeof(buf), 0);
63+
if (n <= 0) {
64+
break;
65+
}
66+
received_.insert(received_.end(), buf, buf + n);
67+
}
68+
});
69+
}
70+
71+
~SocketDrainer() {
72+
stop_.store(true);
73+
::shutdown(fd_, SHUT_RDWR);
74+
if (thread_.joinable()) {
75+
thread_.join();
76+
}
77+
}
78+
79+
const std::vector<char>& data() const { return received_; }
80+
81+
private:
82+
int fd_;
83+
std::atomic<bool> stop_;
84+
std::thread thread_;
85+
std::vector<char> received_;
86+
};
87+
88+
} // namespace
89+
90+
TEST(PcmSensorServerOverflowTest, OverflowDoesNotWritePastOutputBuffer)
91+
{
92+
int sv[2];
93+
ASSERT_EQ(0, ::socketpair(AF_UNIX, SOCK_STREAM, 0, sv))
94+
<< "socketpair failed: " << std::strerror(errno);
95+
96+
// Peer drains whatever the socketbuf sends.
97+
SocketDrainer drainer(sv[1]);
98+
99+
auto buf = std::make_unique<ProbeSocketBuf>();
100+
buf->setSocket(sv[0]);
101+
102+
// Plant a distinctive sentinel in inputBuffer_[0]. With the vulnerable
103+
// overflow(), this byte is the one clobbered by "*pptr() = ch" when the
104+
// put area is full.
105+
constexpr unsigned char kSentinel = 0xAA;
106+
constexpr unsigned char kOverflowChar = 0x5A; // 'Z'
107+
static_assert(kSentinel != kOverflowChar,
108+
"sentinel and overflow byte must differ to detect the OOB write");
109+
buf->inputBuffer_[0] = static_cast<char>(kSentinel);
110+
111+
// Fill the put area completely. After exactly SIZE sputc() calls,
112+
// pptr() == epptr() but overflow() has not yet been invoked.
113+
for (std::size_t i = 0; i < kSocketBufSize; ++i) {
114+
ASSERT_NE(std::char_traits<char>::eof(), buf->sputc('X'))
115+
<< "sputc failed while filling the put area at index " << i;
116+
}
117+
118+
// The next sputc() must trigger overflow(kOverflowChar). With the
119+
// vulnerable implementation this is where *pptr() = ch writes past the
120+
// end of outputBuffer_ and into inputBuffer_[0].
121+
ASSERT_NE(std::char_traits<char>::eof(),
122+
buf->sputc(static_cast<char>(kOverflowChar)))
123+
<< "sputc returned eof when triggering overflow()";
124+
125+
// After overflow() returns, the put area should have been flushed to the
126+
// socket and the sentinel in inputBuffer_[0] must still be intact.
127+
EXPECT_EQ(static_cast<unsigned char>(buf->inputBuffer_[0]), kSentinel)
128+
<< "basic_socketbuf::overflow() corrupted inputBuffer_[0]: "
129+
<< "wrote ch=0x" << std::hex << static_cast<int>(kOverflowChar)
130+
<< " past the end of outputBuffer_ (SIZE=" << std::dec
131+
<< kSocketBufSize << "). See pcm-sensor-server.cpp "
132+
<< "basic_socketbuf::overflow() (lines 1226-1237): the character "
133+
<< "must be stored only *after* the full put-area has been flushed "
134+
<< "and the put pointers reset.";
135+
136+
// Release the socket before the buf destructor runs sync(); this keeps
137+
// the test output stable regardless of whether the drainer has already
138+
// exited.
139+
buf.reset();
140+
::close(sv[0]);
141+
// sv[1] is closed by the drainer's shutdown.
142+
}

0 commit comments

Comments
 (0)