Feb 14, 2026·6 min read·42 visits
A heap buffer over-read caused by incorrect integer casting in libpng's simplified write API. Introduced in 2016 to fix compiler warnings, it truncates row strides to 16 bits. This allows attackers to trigger crashes or potentially leak memory by providing negative or large row strides. Fixed in version 1.6.54.
For nearly a decade, a dormant bug lurked inside the simplified write API of libpng, the world's reference library for PNG manipulation. Born from an attempt to silence harmless compiler warnings in 2016, CVE-2026-22801 is a textbook integer truncation vulnerability. By forcibly casting row strides to 16-bit integers, the library inadvertently blinded itself to image dimensions and memory layouts, leading to a heap buffer over-read. This vulnerability allows attackers to trick applications into reading past allocated memory boundaries, potentially bleeding sensitive heap data into generated image files or crashing the process entirely.
In the world of C/C++ development, compiler warnings are like the check engine light in your car. Prudent engineers investigate the cause; lazy ones just put a piece of black tape over the dashboard. In October 2016, the maintainers of libpng reached for the black tape. To silence nagging warnings on legacy 16-bit systems, they introduced explicit casts to png_uint_16 in the simplified write API. It seemed harmless—a quick housekeeping task to clean up the build logs.
Fast forward to 2026, and that "cleanup" has been revealed as a critical structural failure. By forcing modern 32-bit and 64-bit values into a 16-bit straitjacket, the library lost its ability to do basic math regarding memory offsets. This isn't just a crash bug; it's a reminder that (type_cast) is often a developer saying, "Shut up, I know what I'm doing," right before proving they absolutely do not.
To understand the vulnerability, you have to understand the concept of a "stride." When handling raw image data in memory, the stride is the number of bytes from the beginning of one row of pixels to the beginning of the next. Usually, this is just width * bytes_per_pixel. However, in many graphics systems (looking at you, BMP), strides can be negative to indicate a bottom-up image layout, or they can include padding bytes.
The vulnerability exists in png_write_image_16bit and png_write_image_8bit. The code takes a user-supplied stride (often a standard int or long) and casts it to png_uint_16. Here is the math of the disaster:
Large Strides: If you have a massive image with a stride of 70,000 bytes, casting it to a 16-bit unsigned integer performs a modulo operation. 70000 % 65536 = 4464. The library now thinks the next row is only 4,464 bytes away, not 70,000. It reads data from the wrong location, creating a garbled image and potentially crashing.
Negative Strides: This is the juicy part. A negative stride (e.g., -100) is represented in 2's complement as a very large unsigned number (e.g., 0xFFFFFF9C on 32-bit). When you cast that to uint16, you get 0xFF9C (65,436). Instead of moving backwards in memory to flip the image, the library jumps forward by 65k bytes, rocketing past the end of the allocated buffer and into the wild heap yonder.
Let's look at the anatomy of the failure. The code below shows the logic introduced in version 1.6.26 and how it was remediated in 1.6.54. The intent was to support the simplified API, designed to make reading/writing PNGs easy for developers who don't want to manage complex png_struct pointers.
Vulnerable Code (Pre-1.6.54):
/* Inside png_write_image_8bit */
png_uint_16 row_stride; // <--- THE ROOT CAUSE
/* ... input_stride passed by caller ... */
if (input_stride < 0)
row_stride = (png_uint_16)(-input_stride); // Truncation happens here
else
row_stride = (png_uint_16)input_stride; // And here
/* Later used to calculate memory offsets */
const void *row = (const void*)(buffer + (y * row_stride));Because row_stride is defined as a 16-bit integer, any input exceeding UINT16_MAX is mutilated. The library calculates the memory address of the pixel rows based on this corrupted value.
The Fix (1.6.54): The fix is elegantly simple: stop assuming the world fits in 16 bits. The variable type was promoted, and the casts were removed or adjusted to handle full integer widths correctly.
> [!NOTE] > This highlights a common anti-pattern: prioritizing "clean builds" (zero warnings) over correct logic. A warning about "implicit conversion loses integer precision" exists for a reason.
Exploiting this requires a scenario where an attacker controls the arguments passed to png_image_write_to_file or its siblings. While this is a "local" vulnerability, consider a web service that converts raw image data or other formats into PNGs. If the service allows the user to specify metadata like input stride (or if the stride is calculated from user-supplied dimensions that are excessively large), we can trigger the bug.
The Attack Chain:
libpng < 1.6.54 to convert raw buffers to PNG.png_image_write_to_file. The internal cast corrupts the stride.libpng attempts to read row data. Due to the corrupted stride, the read pointer buffer + (y * stride) points far outside the actual input buffer.libpng obediently reads this out-of-bounds heap memory—which could contain SSL keys, session tokens, or other users' data—and treats it as pixel data. It compresses this "noise" into the resulting PNG.Why should we care about a library bug from 2016? Because libpng is everywhere. It is in your browser, your OS, your phone, and your server-side image processing pipelines (ImageMagick, GD, etc.).
Availability (DoS): This is the most immediate threat. If the calculated offset lands on an unmapped memory page, the application segfaults immediately. For a service processing user images, this is a trivial Denial of Service.
Confidentiality (Information Disclosure): This is the silent killer. As described in the exploit section, this is effectively a "Heartbleed" for image processing. If the memory layout is favorable (i.e., the offset lands in readable heap memory), the library will serialize sensitive process memory into a valid image file. Detection is difficult because the application doesn't crash; it just produces a "weird" image.
The remediation is straightforward: Update libpng to version 1.6.54 immediately.
If you are a developer using libpng directly and cannot update:
row_stride values passed to the API never exceed 65535 and are never negative if you are passing them to the simplified API functions.png_image_write_to_file. If you are calculating strides based on user input, add bounds checking before the call.For Linux administrators, this package is likely managed by your distro's package manager. Check the provided ALAS/RHSA advisories. The patch has been backported to most LTS distributions (Ubuntu, Debian, RHEL), so a standard apt update && apt upgrade should suffice.
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
libpng libpng | >= 1.6.26, < 1.6.54 | 1.6.54 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-125 (Out-of-bounds Read) |
| CWE ID | CWE-190 (Integer Overflow) |
| CVSS v3.1 | 6.8 (Medium) |
| Attack Vector | Local / Context-Dependent |
| Impact | Information Disclosure / DoS |
| Introduced | v1.6.26 (Oct 2016) |
| Fixed | v1.6.54 (Jan 2026) |
The software reads data past the end, or before the beginning, of the intended buffer.