Skip to content

Commit 07bf827

Browse files
committed
Add overflow protection to LZW buffer allocation
1 parent c0ee6cd commit 07bf827

3 files changed

Lines changed: 137 additions & 1 deletion

File tree

src/cgif_raw.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,19 @@ static int LZW_GenerateStream(LZWResult* pResult, const uint32_t numPixel, const
330330
// where N = max dictionary resets = numPixel / (MAX_DICT_LEN - initDictLen - 2)
331331
entriesPerCycle = MAX_DICT_LEN - initDictLen - 2; // maximum added number of dictionary entries per cycle: -2 to account for start and end code
332332
maxResets = numPixel / entriesPerCycle;
333-
pContext->pLZWData = malloc(sizeof(uint16_t) * ((size_t)numPixel + 2 + maxResets));
333+
/* Prevent overflow in addition */
334+
if ((size_t)numPixel > SIZE_MAX - 2 - (size_t)maxResets) {
335+
r = CGIF_EALLOC;
336+
goto LZWGENERATE_Cleanup;
337+
}
338+
/* Safe addition */
339+
size_t total_entries = (size_t)numPixel + 2 + (size_t)maxResets;
340+
/* Prevent overflow in multiplication */
341+
if (total_entries > SIZE_MAX / sizeof(uint16_t)) {
342+
r = CGIF_EALLOC;
343+
goto LZWGENERATE_Cleanup;
344+
}
345+
pContext->pLZWData = malloc(total_entries * sizeof(uint16_t));
334346
if(pContext->pLZWData == NULL) {
335347
r = CGIF_EALLOC;
336348
goto LZWGENERATE_Cleanup;

tests/lzw_alloc_overflow.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <stdlib.h>
2+
#include <stdint.h>
3+
#include <string.h>
4+
#include <stdio.h>
5+
6+
#include "cgif_raw.h"
7+
8+
/*
9+
* Regression test for integer overflow in LZW buffer allocation (PR #107).
10+
*
11+
* The original code computed the LZW data buffer size as:
12+
*
13+
* (numPixel + 2 + maxResets) * sizeof(uint16_t)
14+
*
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.
19+
*
20+
* The fix promotes the arithmetic to size_t and adds explicit overflow
21+
* guards that return CGIF_EALLOC before calling malloc.
22+
*
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.
27+
*
28+
* Expected results:
29+
* main (unfixed): pLZWData malloc has a wrapped (tiny) size => FAIL
30+
* fixed: overflow guard returns CGIF_EALLOC cleanly => PASS
31+
*/
32+
33+
/* ---- malloc interception ---- */
34+
35+
static int malloc_count;
36+
static int overflow_detected;
37+
static size_t overflow_alloc_size;
38+
39+
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 */
64+
}
65+
return malloc(size);
66+
}
67+
68+
/* redirect malloc calls inside cgif_raw.c to our wrapper */
69+
#define malloc(s) test_malloc(s)
70+
#include "../src/cgif_raw.c"
71+
#undef malloc
72+
73+
int main(void) {
74+
LZWResult result;
75+
/*
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
90+
*/
91+
const uint32_t numPixel = 4294000000U;
92+
const uint16_t initDictLen = 4; /* 1 << (calcInitCodeLen(1) - 1) */
93+
const uint8_t initCodeLen = 3; /* calcInitCodeLen(1) */
94+
95+
/* Minimal image data — the overflow check fires before encoding. */
96+
uint8_t imageData[1] = {0};
97+
98+
malloc_count = 0;
99+
overflow_detected = 0;
100+
overflow_alloc_size = 0;
101+
memset(&result, 0, sizeof(result));
102+
103+
int r = LZW_GenerateStream(&result, numPixel, imageData, initDictLen, initCodeLen);
104+
105+
if (overflow_detected) {
106+
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);
110+
return 1;
111+
}
112+
if (r != CGIF_EALLOC) {
113+
fprintf(stderr, "FAIL: expected CGIF_EALLOC, got %d\n", r);
114+
return 1;
115+
}
116+
return 0;
117+
}

tests/meson.build

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ test_ealloc_raw_exe = executable(
9090
)
9191
test('ealloc_raw', test_ealloc_raw_exe, priority : 0)
9292

93+
test_lzw_alloc_overflow_exe = executable(
94+
'test_lzw_alloc_overflow',
95+
'lzw_alloc_overflow.c',
96+
include_directories : ['../inc/'],
97+
)
98+
test('lzw_alloc_overflow', test_lzw_alloc_overflow_exe, priority : 0)
99+
93100
sha256sumc = find_program('scripts/sha256sum.py')
94101
# get the ordering right:
95102
# md5sum check on output GIFs should be run once all of the above tests are done.

0 commit comments

Comments
 (0)