diff --git a/.gitignore b/.gitignore index 3d08cb3d7..eb457cfea 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,7 @@ pngprefix.h /pngfix /pnggetset /pngimage +/pngreadend /pngstest /pngtest /pngunknown diff --git a/CMakeLists.txt b/CMakeLists.txt index 162d63a8d..3f0f030db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -393,6 +393,9 @@ set(pngstest_sources set(pnggetset_sources contrib/libtests/pnggetset.c ) +set(pngreadend_sources + contrib/libtests/pngreadend.c +) set(pngunknown_sources contrib/libtests/pngunknown.c ) @@ -564,6 +567,17 @@ if(PNG_TESTS AND PNG_SHARED) png_add_test(NAME pnggetset COMMAND pnggetset) + # pngreadend test: + # Reading the end of a PNG without reading the image. + add_executable(pngreadend ${pngreadend_sources}) + target_link_libraries(pngreadend + PRIVATE png_shared) + + png_add_test(NAME pngreadend + COMMAND pngreadend + FILES "${CMAKE_CURRENT_SOURCE_DIR}/contrib/testpngs/crashers/stale_zTXt_input.png" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/testpngs/crashers/stale_iCCP_input.png") + # pngvalid tests: # Internal validation of standard and progressive reading, # transforms, and gamma handling. diff --git a/Makefile.am b/Makefile.am index eb7078158..ae5f63320 100644 --- a/Makefile.am +++ b/Makefile.am @@ -13,7 +13,7 @@ ACLOCAL_AMFLAGS = -I scripts/autoconf # test programs - run on make check, make distcheck if ENABLE_TESTS -check_PROGRAMS= pngtest pnggetset pngunknown pngstest pngvalid pngimage pngcp +check_PROGRAMS= pngtest pnggetset pngreadend pngunknown pngstest pngvalid pngimage pngcp if HAVE_CLOCK_GETTIME check_PROGRAMS += timepng endif @@ -45,6 +45,9 @@ pngtest_LDADD = libpng@PNGLIB_MAJOR@@PNGLIB_MINOR@.la pnggetset_SOURCES = contrib/libtests/pnggetset.c pnggetset_LDADD = libpng@PNGLIB_MAJOR@@PNGLIB_MINOR@.la +pngreadend_SOURCES = contrib/libtests/pngreadend.c +pngreadend_LDADD = libpng@PNGLIB_MAJOR@@PNGLIB_MINOR@.la + pngvalid_SOURCES = contrib/libtests/pngvalid.c pngvalid_LDADD = libpng@PNGLIB_MAJOR@@PNGLIB_MINOR@.la @@ -77,6 +80,7 @@ if ENABLE_TESTS TESTS =\ tests/pngtest-all\ tests/pnggetset\ + tests/pngreadend\ tests/pngvalid-gamma-16-to-8\ tests/pngvalid-gamma-alpha-mode\ tests/pngvalid-gamma-background\ @@ -260,6 +264,7 @@ pngtest.o: pnglibconf.h contrib/libtests/makepng.o: pnglibconf.h contrib/libtests/pnggetset.o: pnglibconf.h contrib/libtests/pngimage.o: pnglibconf.h +contrib/libtests/pngreadend.o: pnglibconf.h contrib/libtests/pngstest.o: pnglibconf.h contrib/libtests/pngunknown.o: pnglibconf.h contrib/libtests/pngvalid.o: pnglibconf.h diff --git a/contrib/libtests/pngreadend.c b/contrib/libtests/pngreadend.c new file mode 100644 index 000000000..fe45c7db7 --- /dev/null +++ b/contrib/libtests/pngreadend.c @@ -0,0 +1,116 @@ +/* pngreadend.c + * + * Copyright (c) 2026 Cosmin Truta + * + * This code is released under the libpng license. + * For conditions of distribution and use, see the disclaimer + * and license in png.h + * + * Call png_read_end right after png_read_info, without reading the image. + * This call sequence ends in a libpng error on every input, even on a valid + * PNG. The test passes on such an error, and fails only on a crash or on a + * report from a memory sanitizer. + * + * Before the fix for CVE-2026-46675, png_read_end resumed inflation from input + * left over in the read buffer of a zTXt chunk, which a subsequent pCAL chunk + * had freed by requesting a larger one (stale_zTXt_input.png), or in an + * automatic array local to png_handle_iCCP after that function had returned + * (stale_iCCP_input.png). Both test files declare an invalid zlib window size + * on purpose, so that the stale read happens in libpng's own window check, + * which the sanitizer instruments, rather than inside zlib, which is usually + * built without it. + */ + +#include +#include + +#if defined(HAVE_CONFIG_H) && !defined(PNG_NO_CONFIG_H) +# include +#endif + +#ifdef PNG_FREESTANDING_TESTS +# include +#else +# include "../../png.h" +#endif + +/* AddressSanitizer detects an access to an automatic array after its function + * has returned only if it puts such arrays on a separate stack, which is + * optional. + */ +const char *__asan_default_options(void); + +const char * +__asan_default_options(void) +{ + return "detect_stack_use_after_return=1"; +} + +#if defined(PNG_SEQUENTIAL_READ_SUPPORTED) && defined(PNG_STDIO_SUPPORTED) +static int +read_info_then_end(const char *file_name) +{ + FILE *fp; + png_structp png_ptr; + png_infop info_ptr; + + fp = fopen(file_name, "rb"); + if (fp == NULL) + { + fprintf(stderr, "pngreadend: %s: cannot open\n", file_name); + return 1; + } + + png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (png_ptr == NULL) + { + fprintf(stderr, "pngreadend: png_create_read_struct failed\n"); + fclose(fp); + return 1; + } + + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == NULL) + { + fprintf(stderr, "pngreadend: png_create_info_struct failed\n"); + png_destroy_read_struct(&png_ptr, NULL, NULL); + fclose(fp); + return 1; + } + + if (setjmp(png_jmpbuf(png_ptr)) == 0) + { + png_init_io(png_ptr, fp); + png_read_info(png_ptr, info_ptr); + png_read_end(png_ptr, NULL); + } + + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + fclose(fp); + return 0; +} +#endif + +int +main(int argc, char **argv) +{ +#if defined(PNG_SEQUENTIAL_READ_SUPPORTED) && defined(PNG_STDIO_SUPPORTED) + int result = 0; + int i; + + for (i = 1; i < argc; ++i) + { + printf("Testing png_read_end after png_read_info on %s\n", argv[i]); + fflush(stdout); + if (read_info_then_end(argv[i]) != 0) + result = 1; + } + + return result; +#else + (void)argc; + (void)argv; + fprintf(stderr, "pngreadend: no support for sequential read or stdio\n"); + return 77; +#endif +} diff --git a/contrib/testpngs/crashers/stale_iCCP_input.png b/contrib/testpngs/crashers/stale_iCCP_input.png new file mode 100644 index 000000000..f18b38efa Binary files /dev/null and b/contrib/testpngs/crashers/stale_iCCP_input.png differ diff --git a/contrib/testpngs/crashers/stale_zTXt_input.png b/contrib/testpngs/crashers/stale_zTXt_input.png new file mode 100644 index 000000000..f62232b86 Binary files /dev/null and b/contrib/testpngs/crashers/stale_zTXt_input.png differ diff --git a/tests/pngreadend b/tests/pngreadend new file mode 100755 index 000000000..3e06fdec5 --- /dev/null +++ b/tests/pngreadend @@ -0,0 +1,7 @@ +#!/bin/sh + +# pngreadend test: +# Reading the end of a PNG without reading the image. +exec ./pngreadend \ + "${srcdir}/contrib/testpngs/crashers/stale_zTXt_input.png" \ + "${srcdir}/contrib/testpngs/crashers/stale_iCCP_input.png"