Skip to content

Heap buffer overflow in `png_combine_row` triggered via `png_image_finish_read`

High
ctruta published GHSA-7wv6-48j4-hj3g Nov 22, 2025

Package

libpng

Affected versions

>= 1.6.0, < 1.6.51

Patched versions

1.6.51

Description

Summary

Heap buffer overflow in the libpng simplified API function png_image_finish_read when processing 16-bit interlaced PNGs with 8-bit output format. Attacker-crafted interlaced PNG files cause heap writes beyond allocated buffer bounds.

Technical Details

Vulnerability Mechanism

  1. PNG IHDR declares 16-bit color depth with interlacing (Adam7).
  2. Application requests 8-bit output in RGBA format (PNG_FORMAT_RGBA).
  3. Application allocates PNG_IMAGE_SIZE(image) bytes (sized for 8-bit output).
  4. During interlaced image processing, png_combine_row writes using 16-bit IHDR depth before transformation.
  5. Buffer overflow occurs because the function writes 16-bit data to an 8-bit sized buffer.

Root Cause

For interlaced PNGs, the simplified API's promise that PNG_IMAGE_SIZE(image) suffices is violated during 16-to-8 bit transformations. The internal png_combine_row function processes interlaced images by writing using the IHDR bit-depth (16-bit) before the bit-depth transformation occurs, but the buffer is sized for the requested output format (8-bit). Non-interlaced images are not affected as they use a different processing path.

Proof of Concept

png_image image;
memset(&image, 0, sizeof(image));
image.version = PNG_IMAGE_VERSION;

png_image_begin_read_from_file(&image, "malicious.png");  // 16-bit RGB, interlaced

image.format = PNG_FORMAT_RGBA;  // 8-bit output
size_t size = PNG_IMAGE_SIZE(image);  // Returns 4096 bytes
void *buffer = malloc(size);

png_image_finish_read(&image, NULL, buffer, 0, NULL);  // Writes 6144 bytes (VULNERABLE!)

Overflow Calculation

32×32 pixels, 16-bit RGB (IHDR) to 8-bit RGBA (requested output):

IHDR declares: 16 bits/channel × 3 channels = 6 bytes/pixel
Output requested: 8 bits/channel × 4 channels = 4 bytes/pixel
Buffer allocated: 32×32×4 = 4096 bytes (sized for 8-bit output)
Data written: 32×32×6 = 6144 bytes (using IHDR depth)
Overflow: 6144 - 4096 = 2048 bytes

Larger images produce proportionally larger overflows. For example, for a 256×256 interlaced image, the overflow is 131072 bytes.

Impact

Attack Vector

Prerequisites:

  • Application uses simplified API (png_image_*).
  • Application processes untrusted PNG files.
  • Application requests 8-bit output format.

Attack: Craft 16-bit interlaced PNG ==> heap overflow during png_image_finish_read ==> heap corruption

Consequences

  • Memory Corruption (High): A buffer overflow of a size proportional to the image width enables heap metadata corruption.
  • Arbitrary Code Execution (Moderate): Heap corruption exploitable for control-flow hijacking.
  • Denial of Service (High): Deterministic crash.

Exploitability

  • Attack Complexity: Low (trivial IHDR modification).
  • User Interaction: Required (victim opens malicious PNG).
  • Privileges Required: None.

Affected Software

Applications using simplified API:

  • Image viewers supporting 16-bit PNG images.
  • Game engines loading 16-bit PNG textures.
  • Document processors (PDF viewers, office suites).

The low-level libpng API is not affected. Functions like png_read_png manage their buffers internally.

Fix

The fix evolved through two consecutive commits:

  1. commit 16b5e3823918840aae65c0a6da57c78a5a496a4d: the initial fix.
  2. commit 218612ddd6b17944e21eda56caf8b4bf7779d1ea: the final fix.

Evolution

The fix required two commits:

  • Initial (16b5e38): Rejected all 16-to-8 bit transformations via validation. Too restrictive—broke legitimate non-interlaced cases.
  • Final (218612d): Removed validation, added intermediate buffer for interlaced images only. Maintains full API compatibility.

For downstream maintainers: Apply both commits together. The first alone will reject legitimate transformations.

Implementation

For interlaced 16-to-8 bit conversions, the fix uses an intermediate buffer:

/* Detect interlaced images requiring 16-to-8 scaling */
if (png_ptr->interlaced != 0)
   do_local_scale = 1;

/* Allocate intermediate buffer sized for transformed output */
png_voidp row = png_malloc(png_ptr, png_get_rowbytes(png_ptr, info_ptr));
display->local_row = row;

/* Process via png_image_read_direct_scaled using intermediate buffer */
result = png_safe_execute(image, png_image_read_direct_scaled, display);
png_free(png_ptr, row);

The new png_image_read_direct_scaled function reads interlaced passes into local_row (sized for transformed 8-bit output), then copies to the user buffer. This prevents png_combine_row from writing 16-bit data into an 8-bit sized buffer.

Key Changes

  1. Non-interlaced images: Continue using the fast path unchanged (safe).
  2. Interlaced images: Use intermediate local_row buffer sized for 16-bit data.
  3. API compatibility: 16-to-8 bit transformations remain supported.

Rationale: This targeted fix addresses the vulnerability specifically for interlaced images where png_combine_row writes using IHDR bit-depth. The intermediate buffer prevents overflow while maintaining full API compatibility and performance for non-interlaced images.

Mitigation

Application Developers

Immediate (before upgrading to libpng 1.6.51):

If you cannot upgrade immediately and need to process untrusted PNG files:

/* WARNING: This is an UNSUPPORTED workaround that accesses internal libpng
 * structures (image.opaque) which violate the simplified API contract and
 * are NOT part of the public API. This code may break in ANY future libpng
 * version, and may fail compilation if libpng changes internal definitions.
 *
 * USE ONLY as an emergency temporary measure.
 * Upgrade to libpng 1.6.51+ immediately.
 */

png_image image;
memset(&image, 0, sizeof(image));
image.version = PNG_IMAGE_VERSION;

if (png_image_begin_read_from_file(&image, filename))
{
   png_structp png_ptr = image.opaque->png_ptr;
   png_infop info_ptr = image.opaque->info_ptr;

   /* Workaround: For interlaced 16-bit PNGs, force 16-bit output */
   if (png_get_bit_depth(png_ptr, info_ptr) == 16 &&
       png_get_interlace_type(png_ptr, info_ptr) != PNG_INTERLACE_NONE)
      image.format = PNG_FORMAT_LINEAR_RGBA;  // Force 16-bit output
   else
      image.format = PNG_FORMAT_RGBA;  // Safe to use 8-bit output

   size_t size = PNG_IMAGE_SIZE(image);
   void *buffer = malloc(size);
   png_image_finish_read(&image, NULL, buffer, 0, NULL);
}

Long-term: Upgrade to libpng 1.6.51+ where 16-to-8 bit transformations are fully supported and safe for all image types.

Distribution Maintainers

Priority: High

Apply to all supported versions. Notify downstream packages.

Detection

Vulnerable Code Pattern

image.format = PNG_FORMAT_RGBA;  // Hardcoded 8-bit
png_image_finish_read(&image, ...);  // No depth validation

Testing

Create test case with 16-bit interlaced PNG:

convert -depth 16 -size 32x32 -interlace Adam7 xc:red test.png
./your_app test.png

Expected results:

  • Vulnerable versions: Crash with heap-buffer-overflow (detected by ASan)
  • Patched versions: Successfully process the image
  • Note: Non-interlaced 16-bit PNGs do NOT trigger the vulnerability

References

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H

CVE ID

CVE-2025-65018

Weaknesses

Out-of-bounds Write

The product writes data past the end, or before the beginning, of the intended buffer. Learn more on MITRE.

Credits