Low firefox Logic Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactlow
DescriptionDenial-of-service in the Graphics: ImageLib component
ComponentMedia
Bug ClassLogic Error
Tracker2039443
Fix commit20eeca1fd1d5 (firefox) +40/-38
CISA KEVNot listed
CreditedSecurin
Disclosed2026-06-16

Background

APNG
the Animated PNG extension, which layers fcTL control chunks and fdAT frame-data chunks on top of the base PNG format handled by libpng.
`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 libpng helper that reads and validates the leading 4-byte sequence number of an fdAT (or fcTL) 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.

Key insight
The single mistake was passing a constant 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

  1. Craft a malformed APNG An attacker builds an animated PNG containing an fdAT chunk whose declared length is less than 4, too small to hold the required sequence number.
  2. Deliver to a victim The image is served on a web page or otherwise decoded by the browser’s ImageLib/libpng progressive reader.
  3. Bypass the length check png_ensure_sequence_number(png_ptr, 4) assumes four payload bytes exist and does not flag the short chunk.
  4. Trigger the underflow png_ptr->idat_size = png_ptr->push_length - 4 wraps around to a huge unsigned value (or png_crc_finish is called with an underflowed length).
  5. Provoke the fault The decoder attempts to process the oversized idat_size, reading past the available buffer and crashing the decode.

Impact Assessment

An attacker gains a denial-of-service: a specially crafted APNG causes an integer underflow and out-of-bounds read that aborts image decoding in the process hosting 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.patch
  • media/libpng/pngpread.c

Audit Directions

  • Hard-coded length arguments
    Flag 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 sizes
    Review every push_length - 4 / idat_size -= 4 style expression to confirm the minuend is proven >= 4 before the subtraction.
  • Chunk-length preconditions
    Audit 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.
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
Loading diff…