CVE-2026-12325
Overview
Background
- APNG
- the Animated PNG extension, which layers
fcTLcontrol chunks andfdATframe-data chunks on top of the base PNG format handled bylibpng. - `fdAT` chunk
- an APNG frame-data chunk whose payload is a 4-byte big-endian sequence number followed by the compressed image data for a frame.
- `png_ensure_sequence_number`
- an internal
libpnghelper that reads and validates the leading 4-byte sequence number of anfdAT(orfcTL) chunk, taking a length argument for how many payload bytes are available. - Progressive reader
- the push-model decode path (
png_push_read_chunk,png_push_read_IDAT) that consumes PNG data incrementally as it arrives over the network.
Root Cause Analysis
The vulnerable path is the APNG handling inside the progressive reader in media/libpng/pngpread.c, where png_ensure_sequence_number was invoked with a hard-coded length of 4 rather than the chunk’s actual declared size png_ptr->push_length. The invariant that a chunk must actually contain at least the 4-byte sequence number before that number is consumed and subtracted was never enforced, so a truncated fdAT chunk whose push_length is less than 4 slipped past validation. The code then computed png_ptr->idat_size = png_ptr->push_length - 4, and with push_length < 4 this unsigned subtraction underflows to an enormous value, driving the IDAT/frame-data reader far past the real payload.
The fix passes png_ptr->push_length into png_ensure_sequence_number at each call site so the helper can reject chunks that do not hold a full sequence number, and it reorders the PNG_PUSH_SAVE_BUFFER_IF_LT(4) buffering, sequence-number check, and png_crc_finish(png_ptr, png_ptr->push_length - 4) so the length is validated before any subtraction occurs. This works because the length-underflow can no longer be reached: an undersized fdAT chunk is caught at the sequence-number check instead of producing a bogus idat_size.
4 as the available-bytes argument to png_ensure_sequence_number instead of the chunk’s real length, letting a short fdAT chunk trigger an unsigned underflow of idat_size; the fix supplies png_ptr->push_length so undersized chunks are validated and rejected before the - 4 subtraction.Attack Path
- Craft a malformed APNG
An attacker builds an animated PNG containing an
fdATchunk whose declared length is less than4, too small to hold the required sequence number. - Deliver to a victim
The image is served on a web page or otherwise decoded by the browser’s ImageLib/
libpngprogressive reader. - Bypass the length check
png_ensure_sequence_number(png_ptr, 4)assumes four payload bytes exist and does not flag the short chunk. - Trigger the underflow
png_ptr->idat_size = png_ptr->push_length - 4wraps around to a huge unsigned value (orpng_crc_finishis called with an underflowed length). - Provoke the fault
The decoder attempts to process the oversized
idat_size, reading past the available buffer and crashing the decode.
Impact Assessment
libpng/ImageLib. No special privileges are needed beyond getting the victim to load the image, and the metadata rates the impact as low with no memory-corruption primitive claimed.Files Changed
media/libpng/apng.patchmedia/libpng/pngpread.c
Audit Directions
- Hard-coded length argumentsFlag any call that passes a constant byte count (e.g.
4) where the real available/declared length should be used, since it silently disables bounds validation. - Unsigned subtraction on attacker-controlled sizesReview every
push_length - 4/idat_size -= 4style expression to confirm the minuend is proven>= 4before the subtraction. - Chunk-length preconditionsAudit APNG and other chunk parsers to ensure a chunk’s declared size is validated against its minimum required payload before fixed header fields are consumed.
Patch
diff --git a/media/libpng/apng.patch b/media/libpng/apng.patch
index 10fc0184851..a06964f0d37 100644
--- a/media/libpng/apng.patch
+++ b/media/libpng/apng.patch
@@ -132,7 +132,7 @@ diff --git a/png.h b/png.h
diff --git a/pngget.c b/pngget.c
--- a/pngget.c
+++ b/pngget.c
-@@ -1367,4 +1367,166 @@ png_get_palette_max(png_const_structp pn
+@@ -1366,4 +1366,166 @@ png_get_palette_max(png_const_structp pn
# endif
#endif
@@ -324,7 +324,7 @@ diff --git a/pnginfo.h b/pnginfo.h
diff --git a/pngpread.c b/pngpread.c
--- a/pngpread.c
+++ b/pngpread.c
-@@ -200,6 +200,89 @@ png_push_read_chunk(png_structrp png_ptr
+@@ -200,6 +200,90 @@ png_push_read_chunk(png_structrp png_ptr
chunk_name = png_ptr->chunk_name;
@@ -346,9 +346,6 @@ diff --git a/pngpread.c b/pngpread.c
+
+ else if (chunk_name == png_fdAT)
+ {
-+ PNG_PUSH_SAVE_BUFFER_IF_LT(4)
-+ png_ensure_sequence_number(png_ptr, 4);
-+
+ if ((png_ptr->mode & PNG_HAVE_fcTL) == 0)
+ {
+ /* Discard trailing fdATs for frames other than the first */
@@ -356,13 +353,17 @@ diff --git a/pngpread.c b/pngpread.c
+ png_error(png_ptr, "out of place fdAT");
+
+ PNG_PUSH_SAVE_BUFFER_IF_FULL
-+ png_crc_finish(png_ptr, png_ptr->push_length);
++ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
++ png_crc_finish(png_ptr, png_ptr->push_length - 4);
+ png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
+ }
+
+ else
+ {
+ /* frame data follows */
++ PNG_PUSH_SAVE_BUFFER_IF_LT(4)
++
++ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
+ png_ptr->idat_size = png_ptr->push_length - 4;
+ png_ptr->mode |= PNG_HAVE_IDAT;
+ png_ptr->process_mode = PNG_READ_IDAT_MODE;
@@ -414,7 +415,7 @@ diff --git a/pngpread.c b/pngpread.c
if (chunk_name == png_IDAT)
{
if ((png_ptr->mode & PNG_AFTER_IDAT) != 0)
-@@ -260,6 +343,9 @@ png_push_read_chunk(png_structrp png_ptr
+@@ -268,6 +352,9 @@ png_push_read_chunk(png_structrp png_ptr
else if (chunk_name == png_IDAT)
{
@@ -424,7 +425,7 @@ diff --git a/pngpread.c b/pngpread.c
png_ptr->idat_size = png_ptr->push_length;
png_ptr->process_mode = PNG_READ_IDAT_MODE;
png_push_have_info(png_ptr, info_ptr);
-@@ -270,6 +356,20 @@ png_push_read_chunk(png_structrp png_ptr
+@@ -278,6 +365,20 @@ png_push_read_chunk(png_structrp png_ptr
return;
}
@@ -445,7 +446,7 @@ diff --git a/pngpread.c b/pngpread.c
else
{
PNG_PUSH_SAVE_BUFFER_IF_FULL
-@@ -401,7 +501,11 @@ png_push_read_IDAT(png_structrp png_ptr)
+@@ -409,7 +510,11 @@ png_push_read_IDAT(png_structrp png_ptr)
png_byte chunk_tag[4];
/* TODO: this code can be commoned up with the same code in push_read */
@@ -457,7 +458,7 @@ diff --git a/pngpread.c b/pngpread.c
png_push_fill_buffer(png_ptr, chunk_length, 4);
png_ptr->push_length = png_get_uint_31(png_ptr, chunk_length);
png_reset_crc(png_ptr);
-@@ -409,17 +513,60 @@ png_push_read_IDAT(png_structrp png_ptr)
+@@ -417,17 +522,60 @@ png_push_read_IDAT(png_structrp png_ptr)
png_ptr->chunk_name = PNG_CHUNK_FROM_STRING(chunk_tag);
png_ptr->mode |= PNG_HAVE_CHUNK_HEADER;
@@ -511,14 +512,14 @@ diff --git a/pngpread.c b/pngpread.c
+#ifdef PNG_READ_APNG_SUPPORTED
+ if (png_ptr->num_frames_read > 0)
+ {
-+ png_ensure_sequence_number(png_ptr, 4);
++ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
+ png_ptr->idat_size -= 4;
+ }
+#endif
}
if (png_ptr->idat_size != 0 && png_ptr->save_buffer_size != 0)
-@@ -493,6 +640,16 @@ png_process_IDAT_data(png_structrp png_p
+@@ -501,6 +649,16 @@ png_process_IDAT_data(png_structrp png_p
if (!(buffer_length > 0) || buffer == NULL)
png_error(png_ptr, "No IDAT data (internal error)");
@@ -535,7 +536,7 @@ diff --git a/pngpread.c b/pngpread.c
/* This routine must process all the data it has been given
* before returning, calling the row callback as required to
* handle the uncompressed results.
-@@ -926,6 +1083,18 @@ png_set_progressive_read_fn(png_structrp
+@@ -934,6 +1092,18 @@ png_set_progressive_read_fn(png_structrp
png_set_read_fn(png_ptr, progressive_ptr, png_push_fill_buffer);
}
@@ -557,7 +558,7 @@ diff --git a/pngpread.c b/pngpread.c
diff --git a/pngpriv.h b/pngpriv.h
--- a/pngpriv.h
+++ b/pngpriv.h
-@@ -620,6 +620,10 @@
+@@ -653,6 +653,10 @@
#define PNG_HAVE_CHUNK_AFTER_IDAT 0x2000U /* Have another chunk after IDAT */
#define PNG_WROTE_eXIf 0x4000U
#define PNG_IS_READ_STRUCT 0x8000U /* Else is a write struct */
@@ -568,7 +569,7 @@ diff --git a/pngpriv.h b/pngpriv.h
/* Flags for the transformations the PNG library does on the image data */
#define PNG_BGR 0x0001U
-@@ -884,6 +888,16 @@
+@@ -917,6 +921,16 @@
#define png_tRNS PNG_U32(116, 82, 78, 83)
#define png_zTXt PNG_U32(122, 84, 88, 116)
@@ -585,8 +586,8 @@ diff --git a/pngpriv.h b/pngpriv.h
/* The following will work on (signed char*) strings, whereas the get_uint_32
* macro will fail on top-bit-set values because of the sign extension.
*/
-@@ -1671,6 +1685,49 @@ PNG_INTERNAL_FUNCTION(void,png_read_push
- PNG_EMPTY);
+@@ -1880,6 +1894,49 @@ PNG_INTERNAL_FUNCTION(void, png_read_pus
+ PNG_EMPTY);
#endif /* PROGRESSIVE_READ */
+#ifdef PNG_APNG_SUPPORTED
@@ -638,7 +639,7 @@ diff --git a/pngpriv.h b/pngpriv.h
diff --git a/pngread.c b/pngread.c
--- a/pngread.c
+++ b/pngread.c
-@@ -155,16 +155,96 @@ png_read_info(png_structrp png_ptr, png_
+@@ -157,16 +157,96 @@ png_read_info(png_structrp png_ptr, png_
else if (chunk_name == png_IDAT)
{
@@ -750,7 +751,7 @@ diff --git a/pngrutil.c b/pngrutil.c
/* Set internal variables */
png_ptr->width = width;
png_ptr->height = height;
-@@ -2730,6 +2735,180 @@ png_handle_iTXt(png_structrp png_ptr, pn
+@@ -2706,6 +2711,180 @@ png_handle_iTXt(png_structrp png_ptr, pn
# define png_handle_iTXt NULL
#endif
@@ -931,7 +932,7 @@ diff --git a/pngrutil.c b/pngrutil.c
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
/* Utility function for png_handle_unknown; set up png_ptr::unknown_chunk */
static int
-@@ -4213,6 +4392,38 @@ png_read_IDAT_data(png_structrp png_ptr,
+@@ -4189,6 +4368,38 @@ png_read_IDAT_data(png_structrp png_ptr,
uInt avail_in;
png_bytep buffer;
@@ -970,7 +971,7 @@ diff --git a/pngrutil.c b/pngrutil.c
while (png_ptr->idat_size == 0)
{
png_crc_finish(png_ptr, 0);
-@@ -4224,6 +4435,7 @@ png_read_IDAT_data(png_structrp png_ptr,
+@@ -4200,6 +4411,7 @@ png_read_IDAT_data(png_structrp png_ptr,
if (png_ptr->chunk_name != png_IDAT)
png_error(png_ptr, "Not enough image data");
}
@@ -978,7 +979,7 @@ diff --git a/pngrutil.c b/pngrutil.c
avail_in = png_ptr->IDAT_read_size;
-@@ -4295,6 +4507,9 @@ png_read_IDAT_data(png_structrp png_ptr,
+@@ -4271,6 +4483,9 @@ png_read_IDAT_data(png_structrp png_ptr,
png_ptr->mode |= PNG_AFTER_IDAT;
png_ptr->flags |= PNG_FLAG_ZSTREAM_ENDED;
@@ -988,7 +989,7 @@ diff --git a/pngrutil.c b/pngrutil.c
if (png_ptr->zstream.avail_in > 0 || png_ptr->idat_size > 0)
png_chunk_benign_error(png_ptr, "Extra compressed data");
-@@ -4704,4 +4919,80 @@ defined(PNG_USER_TRANSFORM_PTR_SUPPORTED
+@@ -4680,4 +4895,80 @@ defined(PNG_USER_TRANSFORM_PTR_SUPPORTED
png_ptr->flags |= PNG_FLAG_ROW_INIT;
}
@@ -1072,7 +1073,7 @@ diff --git a/pngrutil.c b/pngrutil.c
diff --git a/pngset.c b/pngset.c
--- a/pngset.c
+++ b/pngset.c
-@@ -463,6 +463,11 @@ png_set_IHDR(png_const_structrp png_ptr,
+@@ -469,6 +469,11 @@ png_set_IHDR(png_const_structrp png_ptr,
info_ptr->pixel_depth = (png_byte)(info_ptr->channels * info_ptr->bit_depth);
info_ptr->rowbytes = PNG_ROWBYTES(info_ptr->pixel_depth, width);
@@ -1084,7 +1085,7 @@ diff --git a/pngset.c b/pngset.c
}
#ifdef PNG_oFFs_SUPPORTED
-@@ -1318,6 +1323,146 @@ png_set_sPLT(png_const_structrp png_ptr,
+@@ -1378,6 +1383,146 @@ png_set_sPLT(png_const_structrp png_ptr,
}
#endif /* sPLT */
@@ -1234,7 +1235,7 @@ diff --git a/pngset.c b/pngset.c
diff --git a/pngstruct.h b/pngstruct.h
--- a/pngstruct.h
+++ b/pngstruct.h
-@@ -392,6 +392,27 @@ struct png_struct_def
+@@ -391,6 +391,27 @@ struct png_struct_def
png_byte filter_type;
#endif
@@ -1288,7 +1289,7 @@ diff --git a/pngwrite.c b/pngwrite.c
#ifdef PNG_WRITE_CHECK_FOR_INVALID_INDEX_SUPPORTED
if (png_ptr->color_type == PNG_COLOR_TYPE_PALETTE &&
png_ptr->num_palette_max >= png_ptr->num_palette)
-@@ -2446,4 +2455,42 @@ png_image_write_to_file(png_imagep image
+@@ -2459,4 +2468,42 @@ png_image_write_to_file(png_imagep image
}
#endif /* SIMPLIFIED_WRITE_STDIO */
#endif /* SIMPLIFIED_WRITE */
@@ -1334,7 +1335,7 @@ diff --git a/pngwrite.c b/pngwrite.c
diff --git a/pngwutil.c b/pngwutil.c
--- a/pngwutil.c
+++ b/pngwutil.c
-@@ -840,6 +840,11 @@ png_write_IHDR(png_structrp png_ptr, png
+@@ -838,6 +838,11 @@ png_write_IHDR(png_structrp png_ptr, png
/* Write the chunk */
png_write_complete_chunk(png_ptr, png_IHDR, buf, 13);
@@ -1345,8 +1346,8 @@ diff --git a/pngwutil.c b/pngwutil.c
+
if ((png_ptr->do_filter) == PNG_NO_FILTERS)
{
- #ifdef PNG_WRITE_FILTER_SUPPORTED
-@@ -1026,7 +1031,17 @@ png_compress_IDAT(png_structrp png_ptr,
+ if (png_ptr->color_type == PNG_COLOR_TYPE_PALETTE ||
+@@ -1004,7 +1009,17 @@ png_compress_IDAT(png_structrp png_ptr,
#endif
if (size > 0)
@@ -1364,7 +1365,7 @@ diff --git a/pngwutil.c b/pngwutil.c
png_ptr->mode |= PNG_HAVE_IDAT;
png_ptr->zstream.next_out = data;
-@@ -1073,7 +1088,17 @@ png_compress_IDAT(png_structrp png_ptr,
+@@ -1051,7 +1066,17 @@ png_compress_IDAT(png_structrp png_ptr,
#endif
if (size > 0)
@@ -1382,7 +1383,7 @@ diff --git a/pngwutil.c b/pngwutil.c
png_ptr->zstream.avail_out = 0;
png_ptr->zstream.next_out = NULL;
png_ptr->mode |= PNG_HAVE_IDAT | PNG_AFTER_IDAT;
-@@ -1975,6 +2000,82 @@ png_write_tIME(png_structrp png_ptr, png
+@@ -1924,6 +1949,82 @@ png_write_tIME(png_structrp png_ptr, png
}
#endif
@@ -1465,7 +1466,7 @@ diff --git a/pngwutil.c b/pngwutil.c
/* Initializes the row writing capability of libpng */
void /* PRIVATE */
png_write_start_row(png_structrp png_ptr)
-@@ -2828,4 +2929,39 @@ png_write_filtered_row(png_structrp png_
+@@ -2777,4 +2878,39 @@ png_write_filtered_row(png_structrp png_
}
#endif /* WRITE_FLUSH */
}
diff --git a/media/libpng/pngpread.c b/media/libpng/pngpread.c
index e0fb81d047f..5cbf0c296a4 100644
--- a/media/libpng/pngpread.c
+++ b/media/libpng/pngpread.c
@@ -218,9 +218,6 @@ png_push_read_chunk(png_structrp png_ptr, png_inforp info_ptr)
else if (chunk_name == png_fdAT)
{
- PNG_PUSH_SAVE_BUFFER_IF_LT(4)
- png_ensure_sequence_number(png_ptr, 4);
-
if ((png_ptr->mode & PNG_HAVE_fcTL) == 0)
{
/* Discard trailing fdATs for frames other than the first */
@@ -228,13 +225,17 @@ png_push_read_chunk(png_structrp png_ptr, png_inforp info_ptr)
png_error(png_ptr, "out of place fdAT");
PNG_PUSH_SAVE_BUFFER_IF_FULL
- png_crc_finish(png_ptr, png_ptr->push_length);
+ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
+ png_crc_finish(png_ptr, png_ptr->push_length - 4);
png_ptr->mode &= ~PNG_HAVE_CHUNK_HEADER;
}
else
{
/* frame data follows */
+ PNG_PUSH_SAVE_BUFFER_IF_LT(4)
+
+ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
png_ptr->idat_size = png_ptr->push_length - 4;
png_ptr->mode |= PNG_HAVE_IDAT;
png_ptr->process_mode = PNG_READ_IDAT_MODE;
@@ -571,7 +572,7 @@ png_push_read_IDAT(png_structrp png_ptr)
#ifdef PNG_READ_APNG_SUPPORTED
if (png_ptr->num_frames_read > 0)
{
- png_ensure_sequence_number(png_ptr, 4);
+ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
png_ptr->idat_size -= 4;
}
#endif