Skip to content

Commit f88cfaf

Browse files
uwezkhan“rootvector2”
authored andcommitted
tests: register lzw_alloc_overflow and make detection order-independent
Wire the LZW allocation overflow regression test into tests/meson.build so it actually runs, and detect the buffer by size rather than by malloc call index so the struct-of-arrays split in LZW_GenerateStream no longer moves the target allocation. Fails on a 32-bit build without the guard, passes with it. Signed-off-by: Uwez Khan <uwezkhan053@gmail.com>
1 parent 77300dc commit f88cfaf

2 files changed

Lines changed: 42 additions & 68 deletions

File tree

tests/lzw_alloc_overflow.c

Lines changed: 34 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,43 @@
66
#include "cgif_raw.h"
77

88
/*
9-
* Regression test for integer overflow in LZW buffer allocation (PR #107).
9+
* Regression test for integer overflow in the LZW code-buffer allocation.
1010
*
11-
* The original code computed the LZW data buffer size as:
11+
* LZW_GenerateStream() sizes pLZWData from:
1212
*
1313
* (numPixel + 2 + maxResets) * sizeof(uint16_t)
1414
*
15-
* using uint32_t arithmetic. When numPixel approaches UINT32_MAX
16-
* (e.g. a 65535x65535 GIF = ~4.3 billion pixels), the additive expression
17-
* wraps, producing a drastically undersized malloc followed by a heap
18-
* buffer overflow during LZW encoding.
15+
* On a 32-bit target size_t is 32 bits wide, so for a numPixel close to
16+
* UINT32_MAX (e.g. a 65535x65535 GIF is ~4.3 billion pixels) the additive
17+
* expression wraps and malloc receives a drastically undersized request.
18+
* LZW encoding then writes past the end of that buffer.
1919
*
20-
* The fix promotes the arithmetic to size_t and adds explicit overflow
21-
* guards that return CGIF_EALLOC before calling malloc.
20+
* The fix promotes the arithmetic to size_t and returns CGIF_EALLOC via the
21+
* overflow guard before malloc is ever called.
2222
*
23-
* This test calls LZW_GenerateStream() directly (via source inclusion)
24-
* with a numPixel value large enough to trigger the overflow. A malloc
25-
* wrapper intercepts the pLZWData allocation and verifies that its size
26-
* is not truncated.
23+
* We reach the static function by including the translation unit directly and
24+
* redirecting its malloc (same approach as ealloc_raw.c).
2725
*
28-
* Expected results:
29-
* main (unfixed): pLZWData malloc has a wrapped (tiny) size => FAIL
30-
* fixed: overflow guard returns CGIF_EALLOC cleanly => PASS
26+
* Detecting the buffer without depending on malloc call order:
27+
* pLZWData is the only allocation that scales with numPixel. Every other
28+
* allocation is bounded by MAX_DICT_LEN and stays well under 64 KiB for the
29+
* inputs used here, so we treat any request larger than that threshold as the
30+
* LZW buffer. We record its size and return NULL so encoding never runs on a
31+
* dummy image buffer.
32+
*
33+
* main (unfixed), 32-bit: request wraps to ~161 KiB (< numPixel bytes) -> FAIL
34+
* fixed, 32-bit: guard returns CGIF_EALLOC, buffer never sized -> PASS
35+
* 64-bit (either): no wrap, request is > 8 GiB, we force NULL -> PASS
3136
*/
3237

33-
/* ---- malloc interception ---- */
38+
#define LZW_ALLOC_THRESHOLD 65536 /* every non-LZW allocation stays below this */
3439

35-
static int malloc_count;
36-
static int overflow_detected;
37-
static size_t overflow_alloc_size;
40+
static size_t lzwAllocSize; /* size of the LZW-buffer request, 0 if none seen */
3841

3942
static void* test_malloc(size_t size) {
40-
++malloc_count;
41-
/*
42-
* Inside LZW_GenerateStream the malloc calls are:
43-
* #1 pContext (small)
44-
* #2 pTreeInit (small)
45-
* #3 pTreeList (small)
46-
* #4 pTreeMap (small)
47-
* #5 pLZWData <-- the one that overflows on main
48-
*
49-
* On the fixed branch the overflow guard fires between #4 and #5,
50-
* so malloc_count never reaches 5.
51-
*
52-
* For #5 we check whether the requested size is suspiciously small.
53-
* The correct size for numPixel ~ 4.3 billion is > 8 GB.
54-
* A uint32-wrapped size would be < 1 MB.
55-
* We always return NULL for #5 to prevent the function from trying
56-
* to LZW-encode our dummy (1-byte) image buffer.
57-
*/
58-
if (malloc_count == 5) {
59-
overflow_alloc_size = size;
60-
if (size < 100000000) { /* < 100 MB => uint32_t overflow occurred */
61-
overflow_detected = 1;
62-
}
63-
return NULL; /* never allocate — image data is a dummy */
43+
if(size > LZW_ALLOC_THRESHOLD) {
44+
lzwAllocSize = size;
45+
return NULL; /* abort before encoding a dummy image */
6446
}
6547
return malloc(size);
6648
}
@@ -73,43 +55,27 @@ static void* test_malloc(size_t size) {
7355
int main(void) {
7456
LZWResult result;
7557
/*
76-
* numPixel that triggers uint32_t overflow in (numPixel + 2 + maxResets):
77-
*
78-
* initDictLen = 4 (for a 1-colour palette)
79-
* entriesPerCycle = MAX_DICT_LEN - 4 - 2 = 4090
80-
* maxResets = 4294000000 / 4090 = 1049877
81-
* sum = 4294000000 + 2 + 1049877 = 4295049879 (> UINT32_MAX)
82-
* wrapped uint32 = 82583
83-
* wrapped malloc = 82583 * 2 = 165166 bytes (~161 KB)
84-
*
85-
* On 32-bit (with fix): size_t overflow guard fires -> CGIF_EALLOC
86-
* On 64-bit (with fix): correct size_t addition (8.6 GB) -> malloc #5
87-
* -> our wrapper returns NULL -> CGIF_EALLOC
88-
* On main (no fix): wrapped uint32 -> malloc #5 with 161 KB size
89-
* -> overflow_detected = 1 -> FAIL
58+
* numPixel large enough to overflow (numPixel + 2 + maxResets) in 32-bit
59+
* size_t arithmetic. A 65535x65535 frame produces a value in this range.
9060
*/
9161
const uint32_t numPixel = 4294000000U;
92-
const uint16_t initDictLen = 4; /* 1 << (calcInitCodeLen(1) - 1) */
93-
const uint8_t initCodeLen = 3; /* calcInitCodeLen(1) */
62+
const uint16_t initDictLen = 4; /* 1-color palette */
63+
const uint8_t initCodeLen = 3;
9464

95-
/* Minimal image data — the overflow check fires before encoding. */
9665
uint8_t imageData[1] = {0};
9766

98-
malloc_count = 0;
99-
overflow_detected = 0;
100-
overflow_alloc_size = 0;
67+
lzwAllocSize = 0;
10168
memset(&result, 0, sizeof(result));
10269

10370
int r = LZW_GenerateStream(&result, numPixel, imageData, initDictLen, initCodeLen);
10471

105-
if (overflow_detected) {
72+
if(lzwAllocSize != 0 && lzwAllocSize < (size_t)numPixel) {
10673
fprintf(stderr,
107-
"FAIL: uint32 overflow in pLZWData allocation "
108-
"(requested %zu bytes for %u pixels, expected > 8 GB)\n",
109-
overflow_alloc_size, numPixel);
74+
"FAIL: LZW buffer request wrapped (%zu bytes for %u pixels)\n",
75+
lzwAllocSize, numPixel);
11076
return 1;
11177
}
112-
if (r != CGIF_EALLOC) {
78+
if(r != CGIF_EALLOC) {
11379
fprintf(stderr, "FAIL: expected CGIF_EALLOC, got %d\n", r);
11480
return 1;
11581
}

tests/meson.build

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ test_ealloc_rgb_exe = executable(
9898
)
9999
test('ealloc_rgb', test_ealloc_rgb_exe, priority : 0)
100100

101+
# overflow guard test (compile source directly to intercept malloc)
102+
test_lzw_alloc_overflow_exe = executable(
103+
'test_lzw_alloc_overflow',
104+
'lzw_alloc_overflow.c',
105+
include_directories : ['../inc/'],
106+
)
107+
test('lzw_alloc_overflow', test_lzw_alloc_overflow_exe, priority : 0)
108+
101109
sha256sumc = find_program('scripts/sha256sum.py')
102110
# get the ordering right:
103111
# md5sum check on output GIFs should be run once all of the above tests are done.

0 commit comments

Comments
 (0)