93 lines
3.7 KiB
C++
93 lines
3.7 KiB
C++
/**
|
|
* @file WSServerBufferTest.cpp
|
|
* @brief Reproduction test for CR-1: 1-byte heap OOB write in WSServer.
|
|
*
|
|
* Verifies that the receive buffer allocated in ClientReadLoop is large enough
|
|
* to hold a maximal masked WebSocket frame (14-byte header + 65536 payload)
|
|
* plus one extra byte for in-place NUL-termination, without overflowing.
|
|
*
|
|
* Build: linked into the GTest harness alongside MainGTest.cpp.
|
|
*/
|
|
|
|
#include "WSFrame.h"
|
|
#include <gtest/gtest.h>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
|
|
using namespace StreamHub;
|
|
|
|
// Mirror the WSServer.h constant (TEST_WS_MAX_RECV_PAYLOAD = 65536).
|
|
static const uint32 TEST_WS_MAX_RECV_PAYLOAD = 65536u;
|
|
|
|
// Test: A maximal masked WebSocket frame (64-bit extended length, masked)
|
|
// with payloadLen = TEST_WS_MAX_RECV_PAYLOAD must fit within kRecvBuf, and
|
|
// payload[plen] must be a valid in-bounds index (for NUL-termination).
|
|
TEST(WSServerBufferTest, MaximalFrameFitsInRecvBuffer) {
|
|
// Reproduce the exact buffer sizing logic from WSServer::ClientReadLoop.
|
|
const uint32 kRecvBuf = TEST_WS_MAX_RECV_PAYLOAD + 14u + 1u;
|
|
uint8 *buf = new uint8[kRecvBuf];
|
|
|
|
// Build a maximal masked frame: FIN + TEXT, payloadLen=65536 (64-bit ext),
|
|
// mask=1.
|
|
uint8 frame[14 + 65536];
|
|
frame[0] = WS_FIN_BIT | WS_OPCODE_TEXT; // FIN + TEXT
|
|
frame[1] = WS_MASK_BIT | 127u; // masked + 64-bit length
|
|
// 8-byte extended length = 65536
|
|
uint64 plen = TEST_WS_MAX_RECV_PAYLOAD;
|
|
for (int i = 7; i >= 0; i--) {
|
|
frame[2 + i] = static_cast<uint8>(plen & 0xFFu);
|
|
plen >>= 8u;
|
|
}
|
|
// 4-byte mask key
|
|
frame[10] = 0xAA; frame[11] = 0xBB; frame[12] = 0xCC; frame[13] = 0xDD;
|
|
// Payload (doesn't matter, just fill with zeros)
|
|
memset(frame + 14, 0, 65536);
|
|
|
|
// Copy into buf (simulating a TCP read)
|
|
ASSERT_LE(sizeof(frame), static_cast<size_t>(kRecvBuf));
|
|
memcpy(buf, frame, sizeof(frame));
|
|
|
|
// Parse the header
|
|
WSFrameHeader hdr;
|
|
ASSERT_TRUE(WSParseHeader(buf, sizeof(frame), hdr));
|
|
ASSERT_EQ(hdr.headerSize, 14u);
|
|
ASSERT_EQ(hdr.payloadLen, static_cast<uint64>(TEST_WS_MAX_RECV_PAYLOAD));
|
|
ASSERT_TRUE(hdr.masked);
|
|
|
|
// Unmask
|
|
uint8 *payload = buf + hdr.headerSize;
|
|
WSUnmask(payload, static_cast<uint32>(hdr.payloadLen), hdr.maskKey);
|
|
|
|
// The critical check: payload[plen] must be within the buffer.
|
|
// Before the fix, kRecvBuf was 65550 and payload[65536] = buf[65550]
|
|
// was one byte past the end. After the fix (+1), it's in bounds.
|
|
uint32 plenIdx = static_cast<uint32>(hdr.payloadLen);
|
|
ASSERT_LT(hdr.headerSize + plenIdx, kRecvBuf)
|
|
<< "payload[plen] would be out of bounds — buffer overflow!";
|
|
|
|
// Simulate the NUL-termination that WSServer does:
|
|
uint8 savedByte = payload[plenIdx];
|
|
payload[plenIdx] = '\0';
|
|
// Verify it's within bounds (no ASan/heap overflow)
|
|
EXPECT_EQ(payload[plenIdx], '\0');
|
|
payload[plenIdx] = savedByte;
|
|
|
|
delete[] buf;
|
|
}
|
|
|
|
// Test: Verify the old (buggy) buffer size would have overflowed.
|
|
// This documents the bug for future readers.
|
|
TEST(WSServerBufferTest, OldBufferSizeWouldOverflow) {
|
|
const uint32 oldKRecvBuf = TEST_WS_MAX_RECV_PAYLOAD + 14u; // the buggy size
|
|
const uint32 headerSize = 14u;
|
|
const uint32 plen = TEST_WS_MAX_RECV_PAYLOAD;
|
|
// headerSize + plen == oldKRecvBuf, so payload[plen] = buf[oldKRecvBuf]
|
|
// is one byte past the end.
|
|
ASSERT_EQ(headerSize + plen, oldKRecvBuf)
|
|
<< "Expected the old buffer to be exactly full (no room for NUL term)";
|
|
// The fix adds +1:
|
|
const uint32 newKRecvBuf = TEST_WS_MAX_RECV_PAYLOAD + 14u + 1u;
|
|
ASSERT_LT(headerSize + plen, newKRecvBuf)
|
|
<< "New buffer must have room for the NUL-termination byte";
|
|
}
|