mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
The LZRC decompressor's only bounds check for output (and input) was a debug-only _dbg_assert_msg_, which is a no-op in release builds. The NPDRM demo block device also passed a hardcoded 1 MiB output length while the real destination buffer (blockBuf_) could be as small as 2048 bytes, allowing a crafted NPDRM image to trigger an unbounded heap overflow during game load. Changes: - rc_putbyte/rc_getbyte now enforce real bounds and set an error flag instead of relying on debug asserts; decompression aborts with -1 on overflow or truncated input. - normalize() reads via rc_getbyte so it stays in bounds. - Plain-text path clamps the copy size to both the output buffer and the remaining input (and no longer interprets the size as signed). - NPDRMDemoBlockDevice::ReadBlock passes blockSize_ (the real buffer size) instead of 0x00100000 to lzrc_decompress. - Add unittest/TestLzrc (synthetic input, no test data files): checks the plain-text clamp, truncated input, and output overflow all fail safely. - AGENTS.md: note to reuse existing format handlers/decompressors before writing new ones.
68 lines
2.1 KiB
C++
68 lines
2.1 KiB
C++
#include <cstring>
|
|
#include <cstdio>
|
|
|
|
#include "Common/CommonTypes.h"
|
|
#include "UnitTest.h"
|
|
|
|
// No header exists for the LZRC decompressor; it's declared locally by callers.
|
|
int lzrc_decompress(void *out, int out_len, void *in, int in_len);
|
|
|
|
// The plain-text path (lc bit 0x80 set) must clamp the copy size to both the
|
|
// output buffer and the remaining input, not read past the end of input.
|
|
static bool TestLzrcPlainTextClamp() {
|
|
// lc = 0x80 (plain text), code = 0xFFFFFF (huge), then "hello".
|
|
u8 input[] = { 0x80, 0xFF, 0xFF, 0xFF, 0x00, 'h', 'e', 'l', 'l', 'o' };
|
|
u8 output[16];
|
|
memset(output, 0xAA, sizeof(output));
|
|
|
|
int result = lzrc_decompress(output, sizeof(output), input, sizeof(input));
|
|
// copySize = min(0xFFFFFF, 16, 10 - 5 = 5) = 5.
|
|
EXPECT_EQ_INT(result, 5);
|
|
EXPECT_TRUE(memcmp(output, "hello", 5) == 0);
|
|
EXPECT_TRUE(output[5] == 0xAA);
|
|
return true;
|
|
}
|
|
|
|
// Truncated input (fewer than the 5 header bytes) must not read past the buffer.
|
|
static bool TestLzrcTruncatedInput() {
|
|
u8 input[] = { 0x00 };
|
|
u8 output[16];
|
|
memset(output, 0xAA, sizeof(output));
|
|
|
|
int result = lzrc_decompress(output, sizeof(output), input, 1);
|
|
EXPECT_EQ_INT(result, -1);
|
|
// Nothing should have been written.
|
|
for (size_t i = 0; i < sizeof(output); ++i) {
|
|
EXPECT_TRUE(output[i] == 0xAA);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// A compressed stream that would overflow a tiny output buffer must return an
|
|
// error and not write past the buffer.
|
|
static bool TestLzrcOutputOverflow() {
|
|
// lc = 0x00 (compressed), code = 0. Feed enough bytes for the decoder to run.
|
|
u8 input[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
|
u8 output[4];
|
|
memset(output, 0xBB, sizeof(output));
|
|
|
|
int result = lzrc_decompress(output, 0, input, sizeof(input));
|
|
// The decoder may bail out as an error (-1) or see end-of-stream (0),
|
|
// but must never write into the (zero-sized) output.
|
|
EXPECT_TRUE(result == -1 || result == 0);
|
|
for (size_t i = 0; i < sizeof(output); ++i) {
|
|
EXPECT_TRUE(output[i] == 0xBB);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TestLzrc() {
|
|
if (!TestLzrcPlainTextClamp())
|
|
return false;
|
|
if (!TestLzrcTruncatedInput())
|
|
return false;
|
|
if (!TestLzrcOutputOverflow())
|
|
return false;
|
|
return true;
|
|
}
|