/** * @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 #include #include 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(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(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(TEST_WS_MAX_RECV_PAYLOAD)); ASSERT_TRUE(hdr.masked); // Unmask uint8 *payload = buf + hdr.headerSize; WSUnmask(payload, static_cast(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(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"; }