CVE-2025-64720: libpng OOB Read via Palette Alpha Invariant Violation
libpng 1.6.0–1.6.50 misapplies background compositing during premultiplication on palette images with PNG_FLAG_OPTIMIZE_ALPHA, violating component ≤ alpha×257 and triggering an out-of-bounds read.
Libpng is one of the most widely used libraries for handling PNG image files. It's in your web browsers, your phone's camera app, and thousands of other programs. Security researchers just found a bug that could let attackers crash these programs or potentially steal data.
Here's what's happening: When images are processed, the software needs to blend colors together, kind of like mixing paint. The bug occurs when libpng tries to optimize certain types of PNG images that use color palettes, like old-school computer graphics. Instead of blending colors correctly, it accidentally reads information from parts of the computer's memory it shouldn't be touching.
Think of it like a librarian looking up a book in the wrong section of the library and accidentally grabbing confidential files instead. The program doesn't crash immediately, but it's accessing memory in unpredictable ways.
The real-world impact depends on how the vulnerable software gets used. If a web browser processes a malicious PNG, an attacker might crash your browser or cause it to leak small amounts of data from your computer's memory. The risk is relatively low for typical users right now since this hasn't been actively exploited in the wild yet.
What you should do: First, keep your software updated. Most developers who use libpng will patch this quickly. Second, be cautious about opening PNG images from untrusted sources, especially if they seem unusual or come from spam emails. Third, if you maintain any software, check whether your code uses libpng and update to the latest version when available.
Want the full technical analysis? Click "Technical" above.
CVE-2025-64720 is an out-of-bounds read in libpng's simplified API, present from version 1.6.0 through 1.6.50, patched in 1.6.51. The defect lives at the intersection of two transformation passes: palette-to-RGBA expansion and premultiplied-alpha compositing. When an application calls png_image_begin_read_from_memory() or its file equivalent with a palette-type PNG and sets PNG_IMAGE_FLAG_FAST (which enables PNG_FLAG_OPTIMIZE_ALPHA), the internal compositing logic in png_init_read_transformations() applies a background blend before the premultiplication invariant is established. The result: a palette entry can produce a composited component value that exceeds alpha × 257, causing png_image_read_composite to read beyond the bounds of the compositing lookup table.
CVSS 7.1 (HIGH) — network vector, no authentication required where an attacker controls image delivery. No in-the-wild exploitation observed as of publication.
Trigger condition: Palette PNG + simplified API + PNG_IMAGE_FLAG_FAST or explicit PNG_FLAG_OPTIMIZE_ALPHA
Impact: Out-of-bounds read; potential info-leak or crash in image-parsing applications (browsers, document renderers, game engines, image pipelines)
Root Cause Analysis
The simplified API's compositing path maintains a strict invariant for every premultiplied pixel: each colour component C must satisfy C ≤ alpha × 257. The lookup table used by png_image_read_composite is sized exactly to that bound. The bug is that png_init_read_transformations applies the background compositing formula to palette entries after palette expansion but before clamping to the premultiplied range, allowing a crafted palette + tRNS chunk to produce an out-of-range index.
/* pngtrans.c – png_init_read_transformations() [libpng 1.6.50, simplified] */
static void
png_init_read_transformations(png_structrp png_ptr)
{
/* ... palette expansion already queued ... */
if (png_ptr->transformations & PNG_COMPOSE)
{
/* BUG: background compositing applied here, before premult clamp.
* For palette entries, background_1 values are drawn directly from
* the tRNS-expanded palette without verifying the invariant
* component <= alpha * 257. A crafted palette entry with
* alpha=0x01 and R=0xFF produces composited R' = 0xFF, but the
* premult table only has 0x01*257 = 0x0101 valid entries.
* The write-back to png_ptr->palette[i].{red,green,blue} is
* unclamped, poisoning the palette before png_image_read_composite
* uses it as a direct index. */
for (i = 0; i < png_ptr->num_palette; i++)
{
if (png_ptr->trans_alpha[i] == 0)
{
png_ptr->palette[i] = png_ptr->background_1; /* fully transparent: replace */
}
else if (png_ptr->trans_alpha[i] < 255)
{
/* Composite: out = src*alpha + bg*(1-alpha) [0..255 range] */
alpha = png_ptr->trans_alpha[i];
/* BUG: result not clamped to alpha*257 range */
png_ptr->palette[i].red =
(png_byte)(((png_ptr->palette[i].red * alpha) +
(png_ptr->background_1.red * (255 - alpha)) + 127) / 255);
png_ptr->palette[i].green =
(png_byte)(((png_ptr->palette[i].green * alpha) +
(png_ptr->background_1.green * (255 - alpha)) + 127) / 255);
png_ptr->palette[i].blue =
(png_byte)(((png_ptr->palette[i].blue * alpha) +
(png_ptr->background_1.blue * (255 - alpha)) + 127) / 255);
/* alpha channel itself is NOT updated here – invariant broken */
}
}
}
/* PNG_FLAG_OPTIMIZE_ALPHA path then calls png_image_read_composite,
* which indexes a table of size [alpha*257+1]. With the poisoned
* palette entry (alpha=1, red=0xFF after composite), the index
* 0xFF > 1*257/256 => table read at offset +N bytes past allocation. */
}
/* pngread.c – png_image_read_composite() [libpng 1.6.50] */
static void
png_image_read_composite(png_voidp argument)
{
/* display->alpha_table is sized: (alpha * 257 + 1) entries */
png_bytep table = display->alpha_table;
for (/* each pixel */)
{
png_byte alpha = *inrow++;
png_byte red = *inrow++;
/* BUG: if red > alpha*257/255 (invariant violated), this indexes
* out of bounds into adjacent heap memory */
*outrow++ = table[red * 257 + 0x80 >> 8]; // OOB READ HERE
/* same for green, blue */
}
}
Root cause:png_init_read_transformations composites palette RGB components against the background colour without updating or clamping against the entry's alpha, breaking the component ≤ alpha × 257 invariant required by png_image_read_composite's fixed-size lookup table.
Memory Layout
The alpha_table is heap-allocated at simplified-API read time. Its size is derived from the alpha value of the first palette entry encountered, or a worst-case precomputed bound. With a crafted tRNS chunk supplying alpha=1, the table is tiny — but the poisoned composite value red=0xFE drives an index of (0xFE * 257 + 0x80) >> 8 = 0xFD, which is 252 entries past a 2-entry table.
HEAP STATE — alpha_table allocation (alpha=0x01):
[ chunk header @ 0xXXXXXX00 ]
[ alpha_table[0] =0x00 @ 0xXXXXXX10 ] <-- table start (2 valid bytes)
[ alpha_table[1] =0x01 @ 0xXXXXXX11 ] <-- last valid entry
[ ---- END OF ALLOCATION ----------- ]
[ NEXT HEAP CHUNK HEADER @ 0xXXXXXX18 ] <-- OOB read begins here
OOB READ: png_image_read_composite accesses table[0xFD]
=> reads 0xFD - 0x01 = 0xFC bytes (252) past end of allocation
=> traverses into adjacent chunk metadata or application heap data
INDEX CALCULATION:
alpha = 0x01 (from tRNS)
red_composited = 0xFE (from palette after background blend)
index = (0xFE * 257 + 0x80) >> 8
= (0x0000FE * 0x101 + 0x80) >> 8
= (0x0000FEFE + 0x0000FE + 0x80) >> 8
= 0x0000FDFE >> 8 [approx]
= 0xFD (decimal 253)
table_size = alpha * 257 + 1 = 1*257+1 = 258 bytes [worst-case alloc]
-- OR under optimised path: ceil(alpha/255 * 256) + 1 = 2 bytes
OOB delta = 253 - 1 = 252 bytes past valid range
Exploitation Mechanics
Reliable arbitrary read requires heap shaping to place a target buffer adjacent to alpha_table. In practice, parsers that decode multiple images in sequence (e.g., animated sticker renderers, thumbnail generators) provide natural opportunities for grooming.
EXPLOIT CHAIN:
1. Attacker delivers a crafted PNG over the network (HTTP img tag,
WebP fallback handler, document embed, etc.) to a target process
that uses libpng's simplified API with PNG_IMAGE_FLAG_FAST.
2. PNG is constructed with:
- Colour type: 3 (PLTE/indexed)
- PLTE chunk: entry[0] = { R=0xFF, G=0xFF, B=0xFF }
- tRNS chunk: trans_alpha[0] = 0x01 (near-transparent)
- Background: bKGD chunk present, colour = { 0xFE, 0xFE, 0xFE }
3. png_init_read_transformations() composites entry[0]:
R' = (0xFF*1 + 0xFE*254 + 127) / 255 = 0xFE (invariant violated)
alpha remains 0x01 in trans_alpha[]
4. png_image_read_composite() allocates alpha_table sized for alpha=0x01.
Under the optimised path this is as small as 2–258 bytes.
5. Heap-groom: issue N prior decode requests of known sizes to position
a target object (e.g., a pixel output buffer whose contents will be
returned to the caller) immediately after alpha_table.
6. png_image_read_composite indexes table[0xFD], reading up to 252 bytes
of adjacent heap into the composited output pixel stream.
7. Application returns composited image data to caller (e.g., as a
base64-encoded thumbnail or canvas ImageData), leaking heap contents.
8. Leaked data contains heap pointers; combined with a separate write
primitive (not provided by this CVE alone), ASLR defeated.
NOTE: This CVE provides a read primitive only. It does not directly
produce code execution without a second primitive. Crash (DoS) is
unconditional on affected builds with ASAN or hardened allocators.
Patch Analysis
The fix in libpng 1.6.51 adds an explicit alpha-consistency check before compositing palette entries when PNG_FLAG_OPTIMIZE_ALPHA is active. After compositing, the palette RGB values are clamped to the premultiplied range, restoring the invariant before png_image_read_composite consumes them.
An alternative approach also accepted upstream: skip the background compositing pass entirely for palette entries when PNG_FLAG_OPTIMIZE_ALPHA is set and defer compositing to the row-transform stage where the invariant can be enforced per-pixel after premultiplication.
Detection and Indicators
ASAN output (affected build):
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000051
READ of size 1 at 0x602000000051 thread T0
#0 png_image_read_composite pngread.c:3412
#1 png_safe_execute pngread.c:968
#2 png_image_finish_read pngread.c:4011
#3 decode_png_thumbnail thumbnailer.c:88
0x602000000051 is located 240 bytes to the right of 2-byte region
[0x602000000010,0x602000000012) allocated by thread T0 here:
#0 malloc
#1 png_malloc_base pngmem.c:102
#2 png_image_read_composite pngread.c:3388
Detection rules:
Static analysis: Flag any call site where png_image_finish_read is called with PNG_FORMAT_FLAG_ALPHA | PNG_IMAGE_FLAG_FAST and input is untrusted.
Fuzzing: AFL++ with the pngimage harness and ASAN; seed corpus should include palette PNGs with low-value tRNS entries and bKGD chunks.
Runtime:libpng version string check — any 1.6.x where x < 51 is affected. Check with png_get_header_ver(NULL).
Network IDS: PNG files with PLTE + tRNS + bKGD combination are a necessary (not sufficient) condition; signature on tRNS chunk containing bytes 0x01–0x0F paired with high-value PLTE RGB is a useful heuristic.
Remediation
Immediate: Upgrade to libpng 1.6.51 or later. This is the only complete fix.
Workaround (if upgrade is blocked): Do not set PNG_IMAGE_FLAG_FAST on untrusted input. Validate PNG colour type and reject type-3 (palette) images in security-sensitive decoders until patched.
Mitigation depth: Memory-safe allocators (hardened malloc, jemalloc with redzone) will turn the OOB read into an abort rather than a silent information leak, but do not prevent the bug.
Downstream consumers: Any project vendoring libpng (Chromium's third_party/libpng, Firefox's media/libpng, ImageMagick, SDL_image, Qt's QtGui PNG backend) should audit their bundled copy and cherry-pick the 1.6.51 patch commit.
Verification: After patching, run the libpng test suite with make check and add a regression PNG with alpha=1, R=0xFF, and a bKGD chunk to the corpus.