AISLE discovers CVE-2026-22695: libpng buffer over-read
Author
Joshua RogersDate Published

A few days ago, we released AISLE PRO, our developer-friendly GitHub PR analyzer that analyzes changes in the source code of a repository in pull requests, allowing developers to discover vulnerabilities before they ever reach production. During the development of our product, we discovered a buffer over-read in pnggroup/libpng, the PNG image parser used by browsers (and many other systems). In this post, we will detail what caused this bug, some technical details of how PNG images are constructed, and how our analyzer discovered the issue in the first place.
Shifting Left: AI Security Analysis in CI/CD
To test the effectiveness of AISLE PRO, we set out to demonstrate that AISLE's CI/CD-integrated analyzer works in practice by showing it would have caught known vulnerabilities at the moment they were introduced as a pull request, preventing them from ever being merged to production code. To test this, we took several well-known open-source projects and replayed their development history by re-creating each pull request and running our analyzer in CI before every merge, from the earliest change onward. This allowed us to verify, across an entire project timeline, that the same vulnerabilities would have been blocked if the analyzer had been integrated at the time.
One of the projects we tested was pnggroup/libpng, a widely used image library (including in Chrome and Firefox). We ran the CI/CD analyzer on every major change to see whether it could detect security issues when they were introduced, rather than after release.
In addition to confirming that our CI/CD analyzer works by finding previously-fixed vulnerabilities in libpng at the commit they were introduced, we also found a vulnerability which had not been discovered yet. Our analyzer discovered a new, previously undiscovered out-of-bounds read in libpng's simplified image API, specifically in png_image_read_direct_scaled(). The AISLE PRO GitHub bot informed us of the technical details of the issue and succinctly described it as "a stride-versus-row-width confusion" that leads to one of two problems. It either reads unintended heap memory causing data leakage, or causes a crash.
Since we're not image parser experts, we wanted to investigate what "a stride-versus-row-width confusion" actually is, and how this vulnerability came to exist.
Rows, pixels, and stride
libpng's functionality is simple: read and write PNGs. Its simplified API is meant to make "read this PNG into my buffer" straightforward. The caller provides a destination buffer, chooses an output format, and supplies a row_stride value that describes the layout of the destination in memory. But what actually is a row stride?
A row stride is the number of bytes separating the start of one row from the next row in a 2-dimensional image buffer. This isn't the same as the width of the row, as the row stride may also include extra padding for alignment. The important part to note is that row_stride is not the amount of pixel data in a row. It is the distance in bytes from the start of one output row to the start of the next.
As we can see, that distance can be larger than the true row width if the application stores padding bytes at the end of each row for alignment: the line_stride (aka row_stride) is greater than the width when there is padding. The stride can also be negative if the application stores rows bottom-up.
From a safety perspective, libpng must maintain a simple invariant: row_stride controls where the next row starts, while row width controls how many bytes of real pixel data exist in the row. If those two values are treated as interchangeable, you will end up copying too much.
Root cause
The vulnerable logic lived in libpng's png_image_read_direct_scaled(), a helper introduced in commit 218612ddd as part of a rework for the fix to CVE-2025-65018 (a heap buffer overwrite). The helper is used when libpng needs to scale a high-precision input into a lower precision output, which involves reading a row into an internal temporary buffer and then copying that row into the application buffer. That internal temporary buffer is allocated to the correctly transformed row width:
C1 png_voidp local_row = png_malloc(png_ptr, png_get_rowbytes(png_ptr, info_ptr));
This is correct. local_row is only as large as the actual row data that libpng produces. The vulnerability stemmed from the copy that follows. Note that the code uses the caller-provided stride as the length argument to memcpy:
C1 ptrdiff_t row_bytes = display->row_bytes; /* derived from row_stride, NOT png_get_rowbytes(png_ptr, info_ptr) */23 memcpy(output_row, local_row, (size_t)row_bytes);4 output_row += row_bytes;
If row_stride is larger than the real row width, memcpy() reads past the end of local_row. If row_stride is negative, the cast to size_t turns a small negative number into a huge unsigned value, and memcpy() attempts a huge read starting from local_row.
Heap memory read primitive
When row_stride is padded, the over-read bytes come from adjacent heap memory. Those bytes are then placed into the output image buffer. If the application later re-encodes that buffer, transmits it, or processes it in a way that exposes its contents, the over-read becomes an information disclosure primitive (uninitialized data, stale heap contents, or unrelated application data).
When row_stride is negative, the signed-to-unsigned cast makes the copy length effectively unbounded. In practice this usually crashes immediately.
Triggering the vulnerability
This bug is reachable via png_image_finish_read(), which can call into png_image_read_direct_scaled() for certain input and output format combinations. Triggering the over-read relies on an application passing a non-minimal (for example, padded) row_stride to libpng like so:
C1ptrdiff_t stride = (row_width + 15) & ~15;2png_image_finish_read(&image, NULL, buffer, stride, NULL);
We can create an interlaced 16-bit test PNG like so:
Shell1convert -depth 16 -size 32x32 -interlace PNG xc:red test.png
With affected libpng versions and an output format that triggers the scaled path (for example, decoding a 16-bit interlaced input into an 8-bit output), the padded bytes in row_stride can cause the copy length to exceed the true row width. When the PNG is decoded, extra bytes from adjacent heap memory can be copied into the output buffer, potentially leaking sensitive data. If the row_stride value can be attacker-controlled, the amount of leaked data may be attacker-controlled, or the application can be crashed with a negative stride.
Fix
The fix was simple: copy only the real row width, and use row_stride only to advance the output pointer:
C1size_t copy_bytes = png_get_rowbytes(png_ptr, info_ptr);23memcpy(output_row, local_row, copy_bytes);4output_row += row_bytes; /* may be padded or negative */
The full fix can be found in commit e4f7ad4ea2.
Timeline
- 2025-11-19: bug introduced in commit 218612ddd ("Rearchitect the fix to the buffer overflow in png_image_finish_read").
- 2025-11-21: libpng 1.6.51 released (first affected release).
- 2025-12-03: libpng 1.6.52 released (affected).
- 2025-12-05: libpng 1.6.53 released (affected).
- 2026-01-06: report filed by AISLE's Petr Simecek, Stanislav Fort, and Pavel Kohout, with a proposed fix.
- 2026-01-09: fix committed as e4f7ad4ea2. ("Fix a heap buffer over-read in png_image_read_direct_scaled").
- 2026-01-12: libpng 1.6.54 released (fixed).
Increasing coverage with AISLE
This bug is a good example of what AISLE's continuous analyzer is meant to catch: subtle bounds and contract mismatches introduced as regressions during refactors. It even catches issues introduced in prior security fixes, where two values that both look like "bytes per row" actually have very different meanings. If you're interested in learning more about AISLE PRO, check out our offering. Contact our team at [email protected] if you’d like to learn more about our enterprise option, including full-repository scanning, SCA, and auto-triage and auto-fix.
And if you’re interested in more of our research, you may enjoy:
- OpenSSL Stack Overflow: CVE-2025-15467 Deep Dive
- AISLE Discovered 12 out of 12 OpenSSL Vulnerabilities
Our appreciation goes to the libpng maintainers for their professionalism. This discovery was made by Petr Simecek, Stanislav Fort, and Pavel Kohout using the AISLE analyzer.