libpng heap overflow for large interlaced images

My analysis is based on the original report and the version I'll refer to in the first part is 1.6.15.

Routine png_read_start_row() from pngrutil.c has the following code flow (png_size_t is just size_t, see pngconf.h):

   int max_pixel_depth;
   png_size_t row_bytes;

[...]

   max_pixel_depth = png_ptr->pixel_depth;

[...]

   /* Align the width on the next larger 8 pixels.  Mainly used
    * for interlacing
    */
   row_bytes = ((png_ptr->width + 7) & ~((png_uint_32)7));
   /* Calculate the maximum bytes needed, adding a byte and a pixel
    * for safety's sake
    */
   row_bytes = PNG_ROWBYTES(max_pixel_depth, row_bytes) +
       1 + ((max_pixel_depth + 7) >> 3); 

#ifdef PNG_MAX_MALLOC_64K
   if (row_bytes > (png_uint_32)65536L)
      png_error(png_ptr, "This image requires a row greater than 64KB");
#endif

   if (row_bytes + 48 > png_ptr->old_big_row_buf_size)
   {
     png_free(png_ptr, png_ptr->big_row_buf);
     png_free(png_ptr, png_ptr->big_prev_row);

     if (png_ptr->interlaced != 0)
        png_ptr->big_row_buf = (png_bytep)png_calloc(png_ptr,
            row_bytes + 48);

     else
        png_ptr->big_row_buf = (png_bytep)png_malloc(png_ptr, row_bytes + 48);

     png_ptr->big_prev_row = (png_bytep)png_malloc(png_ptr, row_bytes + 48);

#ifdef PNG_ALIGNED_MEMORY_SUPPORTED
     /* Use 16-byte aligned memory for row_buf with at least 16 bytes
      * of padding before and after row_buf; treat prev_row similarly.
      * NOTE: the alignment is to the start of the pixels, one beyond the start
      * of the buffer, because of the filter byte.  Prior to libpng 1.5.6 this
      * was incorrect; the filter byte was aligned, which had the exact
      * opposite effect of that intended.
      */
     {
        png_bytep temp = png_ptr->big_row_buf + 32;
        int extra = (int)((temp - (png_bytep)0) & 0x0f);
        png_ptr->row_buf = temp - extra - 1/*filter byte*/;
        
        temp = png_ptr->big_prev_row + 32;
        extra = (int)((temp - (png_bytep)0) & 0x0f);
        png_ptr->prev_row = temp - extra - 1/*filter byte*/;
     }  
     
#else
     /* Use 31 bytes of padding before and 17 bytes after row_buf. */
     png_ptr->row_buf = png_ptr->big_row_buf + 31;
     png_ptr->prev_row = png_ptr->big_prev_row + 31;
#endif
     png_ptr->old_big_row_buf_size = row_bytes + 48;
   } 
   
#ifdef PNG_MAX_MALLOC_64K
   if (png_ptr->rowbytes > 65535)
      png_error(png_ptr, "This image requires a row greater than 64KB");

#endif
   if (png_ptr->rowbytes > (PNG_SIZE_MAX - 1))
      png_error(png_ptr, "Row has too many bytes to allocate in memory");

   memset(png_ptr->prev_row, 0, png_ptr->rowbytes + 1);
We are also interested in PNG_ROWBYTES from pngpriv.h:
/* Added to libpng-1.2.6 JB */
#define PNG_ROWBYTES(pixel_bits, width) \
    ((pixel_bits) >= 8 ? \
    ((png_size_t)(width) * (((png_size_t)(pixel_bits)) >> 3)) : \
    (( ((png_size_t)(width) * ((png_size_t)(pixel_bits))) + 7) >> 3) )
that reads width * (pixel_bits / 8) when pixel_bits >= 8 and (width * pixel_bits + 7) / 8 otherwise. So, it calculates number of bytes that are needed to store the line of pixels each taking pixel_bits of memory.

Returning to the png_read_start_row(): we are interested in the value of row_bytes. It calculates with the following sequence:

  1. we round png_ptr->width to the next multiple of 8;
  2. we calculate number of bytes that are needed to store that many pixels and add a byte and space for an extra pixel.

PNG is limited to the 16 bits per color channel, so for ARGB (PNG_COLOR_TYPE_RGB_ALPHA) we will have 64 bits per pixel. Thus it is permissible to have png_ptr->pixel_depth equal to 64, that gives us 8 bytes of memory per pixel.

So, PNG_ROWBYTES will return width * 8 in this case. Original analysis claims that for width = 0x2000 0000 we will get integer overflow and PNG_ROWBYTES will yield zero.

This is true only for 32-bit platforms where sizeof(size_t) = 4. For 64-bit platforms we need width = 0x2000 0000 0000 0000 for this to happen. But pngstruct.h where png_ptr aka struct png_struct_def * is defined has width and height defined as 32-bit integers,

   png_uint_32 width;         /* width of image in pixels */
   png_uint_32 height;        /* height of image in pixels */
   png_uint_32 num_rows;      /* number of rows in current pass */
   png_uint_32 usr_width;     /* width of row at start of write */
   png_size_t rowbytes;       /* size of row in bytes */
   png_uint_32 iwidth;        /* width of current interlaced row in pixels */
so this can't really happen for a 64-bit system.

But there could be some checks for the sanity of image width. First candidate is png_check_IHDR() from png.c. Prior to libpng 1.6.16rc01 (code drop) width checks used to read

   /* Check for width and height valid values */
   if (width == 0)
   {
      png_warning(png_ptr, "Image width is zero in IHDR");
      error = 1;
   }
   else if (width > PNG_UINT_31_MAX)
   {
      png_warning(png_ptr, "Invalid image width in IHDR");
      error = 1;
   }
   else
   {
#     ifdef PNG_SET_USER_LIMITS_SUPPORTED
      if (width > png_ptr->user_width_max)
#     else
      if (width > PNG_USER_WIDTH_MAX)
#     endif
      {
         png_warning(png_ptr, "Image width exceeds user limit in IHDR");
         error = 1;
      }
   }

PNG_UINT_31_MAX is just 231-1 (look at png.h):

#define PNG_UINT_31_MAX ((png_uint_32)0x7fffffffL)
Unfortunately, PNG_USER_WIDTH_MAX by-default contains the same value (glance over pngpriv.h):
/* SECURITY and SAFETY:
 *
 * By default libpng is built without any internal limits on image size,
 * individual heap (png_malloc) allocations or the total amount of memory used.
 * If PNG_SAFE_LIMITS_SUPPORTED is defined, however, the limits below are used
 * (unless individually overridden).  These limits are believed to be fairly
 * safe, but builders of secure systems should verify the values against the
 * real system capabilities.
 */
#ifdef PNG_SAFE_LIMITS_SUPPORTED
   /* 'safe' limits */
#  ifndef PNG_USER_WIDTH_MAX
#     define PNG_USER_WIDTH_MAX 1000000
#  endif
#  ifndef PNG_USER_HEIGHT_MAX
#     define PNG_USER_HEIGHT_MAX 1000000
#  endif
#  ifndef PNG_USER_CHUNK_CACHE_MAX
#     define PNG_USER_CHUNK_CACHE_MAX 128
#  endif
#  ifndef PNG_USER_CHUNK_MALLOC_MAX
#     define PNG_USER_CHUNK_MALLOC_MAX 8000000
#  endif
#else
   /* values for no limits */
#  ifndef PNG_USER_WIDTH_MAX
#     define PNG_USER_WIDTH_MAX 0x7fffffff
#  endif
#  ifndef PNG_USER_HEIGHT_MAX
#     define PNG_USER_HEIGHT_MAX 0x7fffffff
#  endif
#  ifndef PNG_USER_CHUNK_CACHE_MAX
#     define PNG_USER_CHUNK_CACHE_MAX 0
#  endif
#  ifndef PNG_USER_CHUNK_MALLOC_MAX
#     define PNG_USER_CHUNK_MALLOC_MAX 0
#  endif
#endif
so both checks will pass the value of 0x2000000.

Starting with libpng 1.6.16rc01, another test emerged just after the check for PNG_UINT_31_MAX:

   else if (png_gt(width,
                   (PNG_SIZE_MAX >> 3) /* 8-byte RGBA pixels */
                   - 48                /* big_row_buf hack */
                   - 1                 /* filter byte */
                   - 7*8               /* rounding width to multiple of 8 pix */
                   - 8))               /* extra max_pixel_depth pad */
   {
      /* The size of the row must be within the limits of this architecture.
       * Because the read code can perform arbitrary transformations the
       * maximum size is checked here.  Because the code in png_read_start_row
       * adds extra space "for safety's sake" in several places a conservative
       * limit is used here.
       *
       * NOTE: it would be far better to check the size that is actually used,
       * but the effect in the real world is minor and the changes are more
       * extensive, therefore much more dangerous and much more difficult to
       * write in a way that avoids compiler warnings.
       */
      png_warning(png_ptr, "Image width is too large for this architecture");
      error = 1;
   }

Our allocation size is (row_bytes + 48), where

row_bytes = PNG_ROWBYTES(max_pixel_depth, (width + 7) % 8) + 1 + ((max_pixel_depth + 7)/8)
it means that it shouldn't hit size_t value limit. Inverting the relation we see that
(width + 7) % 8 < [SIZE_T_MAX - 48 - 1 - (max_pixel_depth + 7)/8]/[max_pixel_depth/8]
or for max_pixel_depth that is divisible by 8,
(width + 7) % 8 < [SIZE_T_MAX - 48 - 1]/[max_pixel_depth/8] - 1
Check for
   else if (png_gt(width,
                   (PNG_SIZE_MAX >> 3) /* 8-byte RGBA pixels */
                   - 48                /* big_row_buf hack */
                   - 1                 /* filter byte */
                   - 7*8               /* rounding width to multiple of 8 pix */
                   - 8))               /* extra max_pixel_depth pad */
introduces even lower sanity limit (will not pass some legitimate values that won't cause overflows), so it works too.

Affected versions

I'll start with 1.5.x and will go downwards.

libpng 1.5.0-beta01, the earliest release from 1.5 series, has the following check inside png.c, png_check_IHDR():

   if (width > PNG_UINT_31_MAX)
   {
      png_warning(png_ptr, "Invalid image width in IHDR");
      error = 1;
   }

[...]

   if ( width > (PNG_UINT_32_MAX
                 >> 3)      /* 8-byte RGBA pixels */
                 - 64       /* bigrowbuf hack */
                 - 1        /* filter byte */
                 - 7*8      /* rounding of width to multiple of 8 pixels */
                 - 8)       /* extra max_pixel_depth pad */
      png_warning(png_ptr, "Width is too large for libpng to process pixels");
and pngrutil.c has the above-analyzed size calculations inside png_read_start_row():
   row_bytes = ((png_ptr->width + 7) & ~((png_uint_32)7));
   /* Calculate the maximum bytes needed, adding a byte and a pixel
    * for safety's sake
    */
   row_bytes = PNG_ROWBYTES(max_pixel_depth, row_bytes) +
      1 + ((max_pixel_depth + 7) >> 3);

[...]

   if (row_bytes + 48 > png_ptr->old_big_row_buf_size)
   {
     png_free(png_ptr, png_ptr->big_row_buf);
     if (png_ptr->interlaced)
        png_ptr->big_row_buf = (png_bytep)png_calloc(png_ptr,
            row_bytes + 48);
     else
        png_ptr->big_row_buf = (png_bytep)png_malloc(png_ptr,
            row_bytes + 48);
     png_ptr->old_big_row_buf_size = row_bytes + 48;

The check in png_check_IHDR() and the function itself appeared at revision 134bbe416de32c39a64eb4eb102b6978f57f4ae2 (version 1.4.0beta82 [September 24, 2009]). Previously it was living inside png_get_IHDR() of pngget.c:

     if (info_ptr->width > (PNG_UINT_32_MAX
                >> 3)      /* 8-byte RGBA pixels */
                - 64       /* bigrowbuf hack */
                - 1        /* filter byte */
                - 7*8      /* rounding of width to multiple of 8 pixels */
                - 8)       /* extra max_pixel_depth pad */
     {
        png_warning(png_ptr,
           "Width too large for libpng to process image data");
     }
As you may see, both versions (from 1.4.0 and 1.5.0beta01) don't raise errors in case of overflow and only warning message is given.

This check and warning was removed in revision 62c3f9f0fe681b6daf44bcfeb0e37ee690189585 and this variant first appeared at libpng version 1.5.18beta05 (January 10, 2014).

The check was resurrected in revision fb31308aeaedeecce527838b8e922fc25f95c55c in version 1.5.21rc01 [December 21, 2014] and started to have

     png_warning(png_ptr, "Image width is too large for this architecture");
     error = 1;
as the condition body, thus correctly yielding error this time.

Returning to the older versions, we see that the check in png_get_IHDR() of pngget.c can be dated back to revision 272489dc69a8e20e6be44c5a3649bb8d78971124 (libpng 1.2.6rc1 - August 4, 2004), where it had replaced the following code:

      rowbytes_per_pixel = (pixel_depth + 7) >> 3;
[...]
      if (*width > PNG_UINT_32_MAX/rowbytes_per_pixel - 64)
      {
         png_error(png_ptr,
            "Width too large for libpng to process image data.");
      }
We see that png_error() was used here, so 1.2.6rc1 is the version where this vulnerability was introduced.

Here are commits to 1.5.x and 1.6.x that fixed the problem by reintroducing correct check and bailing out with error, not just spitting the warning.

This means that versions from 1.2.6 and lower than 1.5.21rc01 and 1.6.x lower than 1.6.16rc01 are vulnerable.

libpng 1.4.x and 1.2.x hadn't received fixes (as of 05.01.2015), all of them are vulnerable.

References