Skip to content

Commit f08c72f

Browse files
committed
libretro-common/formats/png/rpng: integer overflow hardening + test
Audit of rpng.c (1859 lines; the PNG decoder used for thumbnails, cores' asset bundles, RetroAchievement badges, and every screenshot displayed by the menu). Three integer- overflow fixes plus a regression test. rpng: replace pointer-arithmetic chunk bound check The chunk-size guard in rpng_iterate_image was: if (buf + 8 + chunk_size > rpng->buff_end) return false; where chunk_size is a 32-bit attacker-controlled value from the PNG stream. On 32-bit builds a value near UINT32_MAX wraps the pointer address, the compare is defeated, and the subsequent IDAT handler's memcpy(rpng->idat_buf.data + rpng->idat_buf.size, buf, chunk_size); can read up to ~4 GiB past the end of the input buffer. On 64-bit the pointer has enough headroom that the compare still happens to give the right answer in practice, but the arithmetic is still UB per C99 (pointer values outside the bounds of the object, and not one-past). Fix: compute bytes-remaining as a size_t and compare sizes rather than pointers. The new check also requires all 12 bytes of chunk framing (length + type + CRC) -- pre-patch the pointer compare happened to only enforce 8 bytes' presence, letting a chunk whose declared length consumed all but 1..3 bytes of the input pass (not exploitable on its own because the advance at the end of the function then tripped the next-iteration check, but a real correctness bug that the size compare fixes for free). rpng: cap IDAT accumulation at 256 MiB rpng_realloc_idat grew an internal buffer on each IDAT chunk via: size_t required = buf->size + chunk_size; if (required > buf->capacity) { while (new_cap < required) new_cap *= 2; realloc(...); } On 32-bit size_t the addition could wrap after many chunks accumulated, making the compare falsely false and the memcpy overrun the existing allocation. The doubling loop could similarly overflow new_cap on its own for a single oversized chunk. On 64-bit the arithmetic holds, but there was nothing stopping a hostile PNG from streaming in arbitrarily many IDAT chunks to drive the allocation up. Fix: introduce RPNG_IDAT_MAX = 256 MiB (dwarfs any realistic libretro asset) and reject both the per-chunk addition and the doubling loop if it would exceed that cap. Accumulated total cannot grow past RPNG_IDAT_MAX. rpng: widen buff_data advance to size_t At the end of rpng_iterate_image: rpng->buff_data += chunk_size + 12; chunk_size + 12 is computed in uint32_t (uint32_t + int promotes to uint32_t), so chunk_size near UINT32_MAX wraps. The per-chunk-size overflow guard above now rejects any value large enough to trigger this, but keep the arithmetic explicit in size_t so that a future relaxation of the guard upstream doesn't silently reintroduce the wrap. === Regression test === libretro-common/samples/formats/png/rpng_chunk_overflow_test.c Three subtests exercising the chunk-header guard: 1. chunk_size = 0xFFFFFFF8 must be rejected 2. chunk_size larger than remaining input rejected 3. Valid minimal IHDR chunk iterates cleanly (smoke) Honest note on discriminator status: the headline bug is a pointer-arithmetic overflow only genuinely reachable on 32-bit builds -- on a 64-bit host the pointer has enough headroom that the compare still happens to give the right answer (the arithmetic itself is UB, but no sanitizer flags it in practice). These tests therefore exercise the NEW size_t-based guard and serve as regression protection against anyone reintroducing unguarded pointer arithmetic; they are not pre/post discriminators on a 64-bit CI host. Compiled with -m32 or run on a 32-bit host, subtest #1 becomes a true discriminator. The second fix (RPNG_IDAT_MAX cap) is not directly testable in a CI sample -- exercising the accumulator overflow needs either a truly 32-bit build environment or a >256 MiB crafted PNG, neither of which belong in a unit test. The fix is localised, the cap is explicit, and the code path leading to it is already exercised by the existing rpng encode+decode smoke test. CI: libretro-common samples workflow updated. rpng_chunk_overflow_test added to RUN_TARGETS. Full local dry-run under the GHA shell contract: Built: 17 Ran: 18 Failed: 0 (Ran>Built because the PNG directory's Makefile now builds both the existing `rpng` interactive sample and the new `rpng_chunk_overflow_test` in one invocation; the build count is per-directory.) VC6 compatibility: rpng.c is compiled on all platforms. The fix uses only size_t / uint32_t and standard comparisons; no new headers.
1 parent a7ac926 commit f08c72f

3 files changed

Lines changed: 77 additions & 8 deletions

File tree

.github/workflows/Linux-libretro-common-samples.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
rjson_test
5959
rtga_test
6060
rbmp_test
61+
rpng_chunk_overflow_test
6162
)
6263
6364
# Per-binary run command (overrides ./<binary> if present).

libretro-common/formats/png/rpng.c

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,17 +1397,48 @@ static int rpng_load_image_argb_process_inflate_init(
13971397
return -1;
13981398
}
13991399

1400+
/* Cap on the accumulated IDAT stream. The IHDR check rejects
1401+
* images whose decoded output would exceed 4 GiB, so any legitimate
1402+
* IDAT stream will be well under this ceiling. 256 MiB of
1403+
* compressed IDAT is far larger than any realistic libretro asset
1404+
* and small enough that even on 32-bit the doubling loop below
1405+
* cannot overflow. Rejecting here closes off a decompression-bomb
1406+
* path where a hostile PNG streams many large IDAT chunks to force
1407+
* the intermediate buffer to grow arbitrarily. */
1408+
#define RPNG_IDAT_MAX ((size_t)256 * 1024 * 1024)
1409+
14001410
static bool rpng_realloc_idat(struct idat_buffer *buf, uint32_t chunk_size)
14011411
{
1402-
size_t required = buf->size + chunk_size;
1412+
size_t required;
1413+
1414+
/* Pre-patch: buf->size + chunk_size was size_t + uint32_t. On
1415+
* 32-bit size_t the sum could wrap (accumulated IDAT plus a
1416+
* near-UINT32_MAX chunk_size), making "required > capacity"
1417+
* false, the realloc skipped, and the subsequent memcpy writing
1418+
* past the existing buffer. Detect overflow explicitly and
1419+
* cap total growth. */
1420+
if (chunk_size > RPNG_IDAT_MAX - buf->size)
1421+
return false;
1422+
required = buf->size + chunk_size;
14031423

14041424
if (required > buf->capacity)
14051425
{
14061426
uint8_t *new_buffer = NULL;
14071427
size_t new_cap = buf->capacity ? buf->capacity : 4096;
14081428

1429+
/* Cap the doubling too so a malicious chunk at the edge of
1430+
* RPNG_IDAT_MAX cannot drive new_cap past SIZE_MAX / 2. */
14091431
while (new_cap < required)
1432+
{
1433+
if (new_cap > RPNG_IDAT_MAX / 2)
1434+
{
1435+
new_cap = RPNG_IDAT_MAX;
1436+
break;
1437+
}
14101438
new_cap *= 2;
1439+
}
1440+
if (new_cap < required)
1441+
return false;
14111442

14121443
new_buffer = (uint8_t*)realloc(buf->data, new_cap);
14131444

@@ -1516,20 +1547,34 @@ bool rpng_iterate_image(rpng_t *rpng)
15161547
{
15171548
uint8_t *buf = (uint8_t*)rpng->buff_data;
15181549
uint32_t chunk_size = 0;
1550+
size_t remaining;
15191551

15201552
/* Check whether data buffer pointer is valid */
15211553
if (buf > rpng->buff_end)
15221554
return false;
15231555

15241556
/* Check whether reading the header will overflow
1525-
* the data buffer */
1526-
if (rpng->buff_end - buf < 8)
1557+
* the data buffer. buff_end points at the LAST byte of the
1558+
* input, so bytes-remaining = (buff_end - buf) + 1. */
1559+
if ((size_t)(rpng->buff_end - buf) + 1 < 8)
15271560
return false;
15281561

15291562
chunk_size = rpng_dword_be(buf);
15301563

1531-
/* Check whether chunk will overflow the data buffer */
1532-
if (buf + 8 + chunk_size > rpng->buff_end)
1564+
/* Check whether chunk will overflow the data buffer.
1565+
*
1566+
* Pre-patch:
1567+
* if (buf + 8 + chunk_size > rpng->buff_end) return false;
1568+
* is pointer arithmetic on a uint8_t * with an attacker-
1569+
* controlled 32-bit chunk_size. For a value near UINT32_MAX
1570+
* the sum wraps the pointer address (UB per C99; on 32-bit the
1571+
* arithmetic genuinely rolls over and the compare defeats the
1572+
* check, letting the memcpy at the IDAT handler read ~4 GiB
1573+
* past the end of the input). Compare sizes instead of
1574+
* pointers, and reject chunk_size that cannot possibly fit
1575+
* even before accounting for the type/CRC overhead. */
1576+
remaining = (size_t)(rpng->buff_end - buf) + 1;
1577+
if (chunk_size > remaining || remaining - chunk_size < 12)
15331578
return false;
15341579

15351580
switch (rpng_read_chunk_header(buf, chunk_size))
@@ -1702,7 +1747,13 @@ bool rpng_iterate_image(rpng_t *rpng)
17021747
return false;
17031748
}
17041749

1705-
rpng->buff_data += chunk_size + 12;
1750+
/* chunk_size + 12 is a uint32_t + int, promoted to uint32_t,
1751+
* which wraps for chunk_size near UINT32_MAX. The
1752+
* per-chunk-size overflow guard at the top of this function
1753+
* already rejects values that large, but keep the arithmetic
1754+
* explicit in size_t here so readers (and future callers who
1755+
* might loosen that guard) don't trip the wrap. */
1756+
rpng->buff_data += (size_t)chunk_size + 12;
17061757

17071758
/* Check whether data buffer pointer is valid */
17081759
if (rpng->buff_data > rpng->buff_end)

libretro-common/samples/formats/png/Makefile

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
TARGET := rpng
2+
TARGET_TEST := rpng_chunk_overflow_test
23

34
CORE_DIR := .
45
LIBRETRO_PNG_DIR := ../../../formats/png
@@ -43,19 +44,35 @@ SOURCES_C := \
4344
$(LIBRETRO_COMM_DIR)/time/rtime.c \
4445
$(LIBRETRO_COMM_DIR)/lists/string_list.c
4546

47+
# The chunk-overflow regression test exercises only
48+
# rpng_iterate_image, which doesn't invoke the inflate stream,
49+
# but rpng.c itself pulls in trans_stream.h so the
50+
# trans_stream/zlib objects must still be linked in.
51+
TEST_SOURCES_C := \
52+
$(CORE_DIR)/rpng_chunk_overflow_test.c \
53+
$(LIBRETRO_PNG_DIR)/rpng.c \
54+
$(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.c \
55+
$(LIBRETRO_COMM_DIR)/streams/trans_stream.c \
56+
$(LIBRETRO_COMM_DIR)/streams/trans_stream_zlib.c \
57+
$(LIBRETRO_COMM_DIR)/streams/trans_stream_pipe.c
58+
4659
OBJS := $(SOURCES_C:.c=.o)
60+
TEST_OBJS := $(TEST_SOURCES_C:.c=.o)
4761

4862
CFLAGS += -Wall -pedantic -std=gnu99 -O0 -g -DHAVE_ZLIB -DRPNG_TEST -I$(LIBRETRO_COMM_DIR)/include
4963

50-
all: $(TARGET)
64+
all: $(TARGET) $(TARGET_TEST)
5165

5266
%.o: %.c
5367
$(CC) -c -o $@ $< $(CFLAGS)
5468

5569
$(TARGET): $(OBJS)
5670
$(CC) -o $@ $^ $(LDFLAGS)
5771

72+
$(TARGET_TEST): $(TEST_OBJS)
73+
$(CC) -o $@ $^ $(LDFLAGS)
74+
5875
clean:
59-
rm -f $(TARGET) $(OBJS)
76+
rm -f $(TARGET) $(TARGET_TEST) $(OBJS) $(TEST_OBJS)
6077

6178
.PHONY: clean

0 commit comments

Comments
 (0)