Feb 14, 2026·6 min read·22 visits
Libpng versions 1.6.51 through 1.6.53 contain a heap buffer over-read vulnerability triggered when processing 16-bit interlaced PNGs into 8-bit output. A flaw in how row strides are calculated allows `memcpy` to read out-of-bounds, causing Denial of Service (DoS) or information disclosure.
A regression in the world's most popular image library turns a routine memory copy into a massive heap over-read. By exploiting the 'simplified API' in libpng, attackers can force the library to read beyond allocated bounds using negative or padded row strides, leading to application crashes or potentially leaking heap memory during 16-bit to 8-bit downscaling.
If you've ever opened an image on a computer, you've likely used libpng. It is the reference library for the Portable Network Graphics (PNG) standard, embedded in everything from your web browser to your OS kernel. It is the plumbing of the visual internet. And like all ancient C plumbing, sometimes it leaks.
CVE-2026-22695 is a classic case of "no good deed goes unpunished." It stems from the library's Simplified API—a set of functions designed to save developers from the headache of manual PNG chunk parsing. Specifically, the function png_image_read_direct_scaled acts as a magic black box: you give it a complex PNG (maybe 16-bit, maybe interlaced), and ask for a simple 8-bit buffer back.
The problem? In the quest to handle image scaling and format conversion automatically, the library made a dangerous assumption about memory layout. It trusted the caller's definition of "row stride" (the width of a row in bytes) a little too much, forgetting that the source buffer might be smaller than the destination's stride. This creates a disconnect between where we are reading from and how much we are trying to read.
This vulnerability is a regression, which is developer-speak for "we fixed a bug and created a new one." It was introduced in commit 218612ddd, ironically while trying to fix a different vulnerability (CVE-2025-65018). The developers were trying to optimize how interlaced images are processed when being downscaled from 16-bit to 8-bit color depth.
Here is the logic flaw: When libpng processes an image, it often uses a temporary buffer, let's call it local_row, to hold the data for a single line of the image. The size of this buffer is determined by png_get_rowbytes(), which calculates the actual data width.
However, the output buffer provided by the application might have a different width. This is defined by row_stride. Why would row_stride be different? Two reasons:
The vulnerability occurs because the code used the destination's row_stride to determine how many bytes to copy from the source local_row. If the stride is larger than the actual row width (padding), or if it's negative (which casts to a massive unsigned integer), the memcpy operation goes off the rails.
Let's look at the crime scene in pngread.c. The code essentially performed a memory copy where the size argument was derived from user-controlled input (row_stride) rather than the actual size of the source buffer.
> [!NOTE]
> In C, memcpy(dest, src, size) requires that both dest and src be at least size bytes long. If src is smaller, you are reading uninitialized heap memory.
Here is the vulnerable logic path:
// pngread.c (Vulnerable Logic)
// 1. local_row is allocated based on the ACTUAL image width
png_alloc_size_t row_bytes = png_get_rowbytes(png_ptr, info_ptr);
local_row = png_malloc(png_ptr, row_bytes);
// ... inside the processing loop ...
// 2. The code uses the caller's 'row_stride' to determine copy size
// If row_stride is negative (e.g. -100), casting to size_t makes it HUGE.
memcpy(output_row, local_row, (size_t)row_stride);
// 3. Advance the output pointer
output_row += row_stride;The fix is simple but critical. We must decouple the amount of data we copy from the amount we advance the pointer. We only copy what we have (row_bytes), but we still jump ahead by row_stride to maintain the application's alignment requirements.
// pngread.c (Patched Logic in 1.6.54)
// Only copy the valid data available in local_row
memcpy(output_row, local_row, row_bytes);
// Advance the pointer by the stride (which handles the padding/gap)
output_row += row_stride;To exploit this, we don't need a debugger; we need a weird image and a standard API call. The goal is to trick the application into asking libpng to perform an impossible copy.
The Attack Recipe:
do_local_scale logic path.png_image_read_direct_scaled API (or similar simplified APIs) and request an 8-bit output format.row_stride that is either significantly larger than the image width (excessive padding) OR negative.While an attacker can't always control the arguments passed to the library (the row_stride), many applications calculate this based on metadata or platform standards. For example, an application parsing a PNG to display it on a Windows surface might default to a negative stride for bottom-up rendering.
If the stride is negative, (size_t)row_stride becomes a number close to $2^{64}$. The memcpy tries to read until it hits an unmapped memory page, causing an immediate segmentation fault (DoS). If the stride is just largely padded, it copies adjacent heap chunks into the image output, potentially leaking sensitive data.
So, is this the end of the world? Probably not. Is it bad? Yes.
Denial of Service (DoS) is the guaranteed impact. By feeding a malicious image to a server processing user uploads (e.g., a profile picture resizer), an attacker could crash the worker process. If the process doesn't restart automatically, or if the attacker floods the service, the application goes down.
Information Disclosure is the more insidious risk. Since this is an over-read, libpng is copying data from the heap into the pixel buffer. That means whatever was sitting next to local_row in memory—previous HTTP requests, encryption keys, other users' images—gets baked into the resulting image.
If the attacker can download the processed image (which they usually can, seeing as they uploaded it), they can render the pixels and reconstruct the leaked memory. It's like Heartbleed, but for pixels.
The remediation is straightforward: Update libpng to version 1.6.54.
If you are a developer using libpng directly, verify that you are not statically linking an older version. Check your dependencies. If you are a Linux sysadmin, apt update or yum update should be your priority today.
For those who cannot patch immediately, you might be able to mitigate this by ensuring your application:
png_image_read...).Ultimately, patch the library. It's the only way to be sure.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
libpng PNG Group | >= 1.6.51, <= 1.6.53 | 1.6.54 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-125 (Heap Buffer Over-read) |
| CVSS v3.1 | 6.1 (Medium) |
| Attack Vector | Local (File Processing) |
| Availability Impact | High (Crash) |
| Confidentiality Impact | Low (Heap Leak) |
| Exploit Status | PoC Available |
The software reads data past the end, or before the beginning, of the intended buffer.