FINDING-07 — APNG fdAT integer underflow in progressive PNG reader
Categories
(Core :: Graphics: ImageLib, defect)
Tracking
()
People
(Reporter: disclose, Assigned: tnikkel)
References
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-esr140.12+][adv-esr115.37+][adv-main152+])
Attachments
(1 file, 4 external files)
|
11.48 KB,
application/x-zip-compressed
|
Details |
Product
Core
Component
Graphics: ImageLib
Version
unspecified (Firefox bundled libpng with media/libpng/apng.patch)
Severity
S3
Keywords
sec-moderate, csectype-intoverflow
Whiteboard
[adv-main??]
Summary
Progressive APNG reader (Firefox-bundled libpng with apng.patch) passes hardcoded 4 to png_ensure_sequence_number(), bypassing the length < 4 guard and causing png_ptr->idat_size to underflow when a crafted fdAT chunk declares data length < 4
Description
media/libpng/pngpread.c is part of Firefox's bundled libpng with the APNG patch (media/libpng/apng.patch). Two call sites in the progressive (streaming) reader pass a hardcoded literal 4 to png_ensure_sequence_number() instead of the actual fdAT chunk length. This defeats a guard inside png_ensure_sequence_number():
Vulnerable sites
media/libpng/pngpread.c line 222 (inside the chunk_name == png_fdAT branch):
else if (chunk_name == png_fdAT)
{
PNG_PUSH_SAVE_BUFFER_IF_LT(4)
png_ensure_sequence_number(png_ptr, 4); // <-- BUG: literal 4
...
/* frame data follows */
png_ptr->idat_size = png_ptr->push_length - 4; // unsigned underflow when push_length < 4
...
}
media/libpng/pngpread.c line 574 (inside the IDAT-mode fdAT trailer):
#ifdef PNG_READ_APNG_SUPPORTED
if (png_ptr->num_frames_read > 0)
{
png_ensure_sequence_number(png_ptr, 4); // <-- same bug
png_ptr->idat_size -= 4; // underflows when idat_size < 4
}
#endif
The bypassed guard
media/libpng/pngrutil.c lines 2868-2885:
void /* PRIVATE */
png_ensure_sequence_number(png_structp png_ptr, png_uint_32 length)
{
png_byte data[4];
png_uint_32 sequence_number;
if (length < 4)
png_error(png_ptr, "invalid fcTL or fdAT chunk found");
png_crc_read(png_ptr, data, 4);
sequence_number = png_get_uint_31(png_ptr, data);
...
}
When called with length=4, the guard 4 < 4 evaluates false, so png_crc_read(png_ptr, data, 4) proceeds and reads 4 bytes — past the fdAT chunk boundary into the CRC and the start of the next chunk.
Correct (sequential) call site for contrast
media/libpng/pngrutil.c line 2856 — sequential reader passes the actual chunk length:
png_ensure_sequence_number(png_ptr, length);
Only the progressive reader (used by Firefox for network-loaded images) has the bug.
Threat model
A remote attacker hosts a crafted APNG. Loading the image in Firefox triggers progressive decode of an fdAT chunk with declared data_length < 4. Effects:
png_crc_readconsumes 4 bytes past the fdAT data boundary.png_ptr->idat_size = push_length - 4underflows to ~0xFFFFFFFC.- The decoder proceeds with a corrupt sequence-number value and a multi-GB
idat_size. Subsequent stream bytes are misclassified as IDAT data. - Outcomes observed in the live ASAN harness:
PNG unsigned integer out of range(high bit of CRC bytes set) — denial of service.fcTL or fdAT chunk with out-of-order sequence number found— denial of service plus corrupted stream pointer.
The downstream zlib feed receives wrong data; whether corrupted pixel output can leak prior buffer contents depends on the exact CRC bytes used by the attacker.
Steps to reproduce — live ASAN PoC (validated)
The PoC was confirmed to trigger the bug on:
- OS: Linux WSL2 / Ubuntu 24.04 (kernel
6.6.114.1-microsoft-standard-WSL2) - Compiler: GCC 13.3.0 with
-fsanitize=address,undefined - libpng: Firefox bundled, compiled standalone via the harness below
# 1. Generate PoC APNG variants
cd $FIREFOX_SRC
python3 artifacts/vuln-disclosure/pocs/poc_finding07_apng_fdat.py
# Produces: finding07_len0.png (181 B), finding07_len1.png, finding07_len2.png,
# finding07_len3.png, finding07_safe.png (200 B, valid control)
# 2. Build the standalone progressive-decode harness
gcc -fsanitize=address,undefined -g -O0 \
-I media/libpng \
-Wno-implicit-function-declaration \
artifacts/vuln-disclosure/pocs/test_finding07_standalone.c \
media/libpng/png.c media/libpng/pngerror.c media/libpng/pngget.c \
media/libpng/pngmem.c media/libpng/pngread.c media/libpng/pngpread.c \
media/libpng/pngrutil.c media/libpng/pngrtran.c media/libpng/pngset.c \
media/libpng/pngtrans.c media/libpng/pngrio.c \
media/libpng/pngwutil.c media/libpng/pngwrite.c media/libpng/pngwtran.c \
media/libpng/pngwio.c \
-lz -lm -o test_finding07
# 3. Reproduce
for f in finding07_len0.png finding07_len1.png finding07_len2.png finding07_len3.png; do
echo "=== $f ==="
ASAN_OPTIONS=detect_leaks=0 timeout 10 ./test_finding07 "$f"
done
echo "=== finding07_safe.png ==="
ASAN_OPTIONS=detect_leaks=0 ./test_finding07 finding07_safe.png
Expected vs Actual
| Input | Expected | Actual |
|---|---|---|
finding07_len0.png (fdAT data_length = 0) |
libpng rejects with "invalid fcTL or fdAT chunk found" before reading any chunk data | libpng reads 4 CRC bytes as the sequence number, then errors with "PNG unsigned integer out of range" |
finding07_len1/2/3.png (data_length = 1/2/3) |
Same rejection | libpng reads (data + CRC bytes) as sequence number, then errors with "fcTL or fdAT chunk with out-of-order sequence number found" |
finding07_safe.png (valid APNG) |
Decoded without error | Decoded without error |
That all four malformed inputs reach errors inside the secondary checks (not the intended length < 4 rejection) proves the guard at pngrutil.c:2874 is bypassed.
If both secondary checks happen to pass for a specific attacker-chosen CRC value, control reaches:
png_ptr->idat_size = png_ptr->push_length - 4; // underflow
…and subsequent data is consumed as IDAT.
Suggested fix
// media/libpng/pngpread.c:222
- png_ensure_sequence_number(png_ptr, 4);
+ png_ensure_sequence_number(png_ptr, png_ptr->push_length);
// media/libpng/pngpread.c:574
- png_ensure_sequence_number(png_ptr, 4);
+ png_ensure_sequence_number(png_ptr, png_ptr->idat_size);
After this patch, an fdAT with data_length < 4 is rejected with "invalid fcTL or fdAT chunk found" before any underflow can occur, matching the sequential-reader behavior at pngrutil.c:2856.
This is a Firefox-specific bug introduced by the APNG patch (media/libpng/apng.patch); upstream libpng is not affected.
Attachments to upload
artifacts/vuln-disclosure/pocs/finding07_len0.png(181 bytes)artifacts/vuln-disclosure/pocs/finding07_len1.png(182 bytes)artifacts/vuln-disclosure/pocs/finding07_len2.png(183 bytes)artifacts/vuln-disclosure/pocs/finding07_len3.png(184 bytes)artifacts/vuln-disclosure/pocs/finding07_safe.png(200 bytes, valid control)artifacts/vuln-disclosure/pocs/test_finding07_standalone.c(ASAN harness)artifacts/vuln-disclosure/pocs/poc_finding07_apng_fdat.py(PoC regenerator)artifacts/vuln-disclosure/pocs/VALIDATION_RESULTS.md
References
- Source:
media/libpng/pngpread.clines 219-242, 568-578 - Source:
media/libpng/pngrutil.clines 2855-2885 - Patch:
media/libpng/apng.patch
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 1•5 months ago
|
||
Looks like this is very much related to
https://github.com/pnggroup/libpng/commit/9ec49c2d56cec19107ddc458b648ce224c9697b3
(In reply to Securin Disclose from comment #0)
This is a Firefox-specific bug introduced by the APNG patch (
media/libpng/apng.patch); upstream libpng is not affected.
libpng 1.8 incorporated apng support (we're still on libpng 1.6, I think most people are? not sure if 1.8 is considered stable yet), and I believe it used a patch that was very similar to the apng.patch we have in tree.
| Assignee | ||
Updated•5 months ago
|
Comment 2•5 months ago
|
||
Confirmed. This is the upstream bug originally reported as pnggroup/libpng#854 by Ryo Shimada, although that report covered only the IDAT-trailer call site. Both call sites that passed a literal 4 are fixed on the libpng18 branch: one in commit 9ec49c2d56, and the other in commit faf0692468 (the latter being part of the chunk-smuggling fix described below). The diagnosis is correct, and the upstream fix uses push_length at both sites for consistency. A full backport to media/libpng/apng.patch in Firefox requires both commits.
Here's more information: a sibling defect in the same APNG push path (chunk-body smuggling) was reported separately by Seung Min Shin and fixed on libpng18 in commit faf0692468 (fix: Consume chunk body in APNG push-mode discard paths). It is tracked as CVE-2026-40930 / GHSA-c4v6-gxrq-6g2x, with the advisory currently in draft. If it would help your release coordination ahead of tomorrow's publication, I can paste the current advisory text (not yet world-readable) into this bug.
Timothy Nikkel is correct that libpng-1.8 incorporates the APNG patch natively, and the defect was indeed present there. On the other hand, upstream libpng-1.6 has no APNG support and is not affected; the APNG-patched libpng-1.6 used downstream (in Firefox and Thunderbird, and in several Linux distros like Gentoo) is affected.
I intend to publish the chunk-body smuggling advisory CVE-2026-40930 tomorrow (Friday), 2026-May-15, at 12:00 UTC. If that timing creates a problem at your end, please let me know.
As for the fdAT length defect: the IDAT-trailer call site was fixed two weeks ago in commit 9ec49c2d56, in response to GitHub Issue 854, without a separate security advisory. The other call site in png_push_read_chunk was fixed a month ago in commit faf0692468 as part of the chunk-smuggling restructure tracked as CVE-2026-40930. If you wish to assign a product-level CVE for the APNG-patched libpng-1.6 bundled in Firefox, and you need my input on the technical write-up, I'll be happy to help with a review.
| Assignee | ||
Comment 3•5 months ago
|
||
Thanks!
(In reply to Cosmin Truta from comment #2)
I intend to publish the chunk-body smuggling advisory CVE-2026-40930 tomorrow (Friday), 2026-May-15, at 12:00 UTC. If that timing creates a problem at your end, please let me know.
ni dveditz
Comment 4•5 months ago
|
||
Hi,
I just made CVE-2026-40930 public. You may see it here:
https://github.com/pnggroup/libpng/security/advisories/GHSA-c4v6-gxrq-6g2x
There are still a few edits that I want to make for completeness. I will send the public announcement on the usual channels after that.
Comment 5•5 months ago
|
||
Hello, again,
The advisory edits are done, and the oss-security announcement is posted:
https://www.openwall.com/lists/oss-security/2026/05/15/21
Comment 6•5 months ago
|
||
... fixed a month ago ... tracked as CVE-2026-40930. If you wish to assign a product-level CVE for the APNG-patched libpng-1.6 bundled in Firefox ...
Yes, even though the bugs share a common ancestor, we're technically not patching the same thing and will have to assign a Firefox-specific CVE to the fix.
Updated•5 months ago
|
Updated•5 months ago
|
| Reporter | ||
Comment 7•5 months ago
|
||
lets us know once the CVE assigned ?
| Assignee | ||
Comment 8•5 months ago
|
||
From what I can tell this bug report is only claiming denial of service.
(In reply to Securin Disclose from comment #0)
Expected vs Actual
Input Expected Actual finding07_len0.png(fdAT data_length = 0)libpng rejects with "invalid fcTL or fdAT chunk found" before reading any chunk data libpng reads 4 CRC bytes as the sequence number, then errors with "PNG unsigned integer out of range" finding07_len1/2/3.png(data_length = 1/2/3)Same rejection libpng reads (data + CRC bytes) as sequence number, then errors with "fcTL or fdAT chunk with out-of-order sequence number found" finding07_safe.png(valid APNG)Decoded without error Decoded without error That all four malformed inputs reach errors inside the secondary checks (not the intended
length < 4rejection) proves the guard atpngrutil.c:2874is bypassed.If both secondary checks happen to pass for a specific attacker-chosen CRC value, control reaches:
png_ptr->idat_size = png_ptr->push_length - 4; // underflow…and subsequent data is consumed as IDAT.
In both cases the reporter is saying that we hit error and reject the png. But let's say that the attacker crafted the file to avoid those errors. The decoder then would be tricked into recording 0xFFFFFFFC as the length of the fdAT chunk (we read the chunk length from the file, so we never trust it, and still don't in this scenario) and we attempt to read that many bytes of our input stream. The code to read from the input stream does not use the attacker controlled value of 0xFFFFFFFC for this; it is still limited to the actual stream length (otherwise there would be a trivial security error that the chunk length from the file controls what memory we read). So attempting to read 0xFFFFFFFC bytes from the input stream isn't a security issue. Can the decoder believing 0xFFFFFFFC is chunk length be turned into a worse problem? Perhaps, but nothing has been shown to that effect.
When it is said "proceeds and reads 4 bytes — past the fdAT chunk boundary into the CRC and the start of the next chunk" the 'fdAT chunk boundary' is just a mental construct we humans have in trying to understand the stream of bytes, it is not a delimited region of memory. The stream of bytes that we are decoding is the delimited region of memory that we don't want to read outside of, but the mechanism that we use for that is not touched by this: the length of a fdAT chunk comes from the untrusted input, so the code could never rely on that value. If you are able to make the decoder read memory that it does not own then please provide details.
As Cosmin pointed out, there was a different but related issue fixed in libpng upstream. The CVE https://github.com/pnggroup/libpng/security/advisories/GHSA-c4v6-gxrq-6g2x for that says:
This is a medium-severity vulnerability. The practical impact depends on the application's CRC error handling configuration.
Default configuration. libpng's default behavior is to call png_error on CRC mismatches and sequence number violations. Under default settings, the smuggled data triggers a fatal parse error before it reaches the application. The image fails to load. Impact is limited to denial of service: a crafted APNG that refuses to render.
Relaxed CRC configuration. Applications that explicitly relax CRC error handling via png_set_crc_action (e.g. PNG_CRC_QUIET_USE) would silently accept the smuggled data....
We don't use png_set_crc_action so we should be in the default configuration, and that one also is a denial of service.
Of course, we still want to fix these issues and I will make a patch for that.
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 9•5 months ago
|
||
Updated•5 months ago
|
Comment 10•5 months ago
|
||
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 11•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Comment 12•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Comment 13•5 months ago
|
||
Updated•5 months ago
|
Comment 14•5 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: sec bug
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: rejects invalid pngs only
- String changes made/needed?: none
- Is Android affected?: yes
Comment 15•5 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: sec bug
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: rejects invalid pngs only
- String changes made/needed?: none
- Is Android affected?: yes
Comment 16•5 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined/Reason for urgency: sec bug
- Code covered by automated testing?: yes
- Fix verified in Nightly?: no
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: rejects invalid pngs only
- String changes made/needed?: none
- Is Android affected?: yes
Comment 17•5 months ago
|
||
Updated•5 months ago
|
Updated•5 months ago
|
Comment 18•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Comment 19•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Comment 20•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Comment 21•4 months ago
•
|
||
This is an automated comment. To help with triage and priority, we are experimenting with an automated exploitation analysis.
A proof-of-concept was attempted, and controlled exploitation is not feasible based on this analysis. The issue was considered an out of bounds read, and the offset or location of the read was attempted, however sufficient control could not be demonstrated. The 4-byte sequence-number read is always guarded so it stays within the allocated push buffer, and the underflowed length counter is clamped to the actual buffer size before any consumption and never drives a copy length, allocation size, or pointer. The only reachable effect is a clean decoder error, making this a denial-of-service with no memory-safety event.
If this bug was submitted for a bounty, it is unlikely to be awarded one unless some control can be demonstrated. If this analysis is demonstrably incorrect, please needinfo :tjr.
Updated•4 months ago
|
Updated•1 month ago
|
Description
•