Closed Bug 2039443 (CVE-2026-12325) Opened 5 months ago Closed 5 months ago

FINDING-07 — APNG fdAT integer underflow in progressive PNG reader

Categories

(Core :: Graphics: ImageLib, defect)

defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox-esr115 152+ fixed
firefox-esr140 152+ fixed
firefox151 --- wontfix
firefox152 + fixed
firefox153 + fixed

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
Attached file bug-07.zip —

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:

  1. png_crc_read consumes 4 bytes past the fdAT data boundary.
  2. png_ptr->idat_size = push_length - 4 underflows to ~0xFFFFFFFC.
  3. The decoder proceeds with a corrupt sequence-number value and a multi-GB
    idat_size. Subsequent stream bytes are misclassified as IDAT data.
  4. 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.c lines 219-242, 568-578
  • Source: media/libpng/pngrutil.c lines 2855-2885
  • Patch: media/libpng/apng.patch
Flags: sec-bounty?
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: ImageLib
Keywords: ai-involved
Product: Firefox → Core
Flags: needinfo?(ctruta)

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: nobody → tnikkel

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.

Flags: needinfo?(ctruta)

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

Flags: needinfo?(dveditz)

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.

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

... 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.

Whiteboard: [client-bounty-form] → [client-bounty-form][adv-esr115.37+]
Whiteboard: [client-bounty-form][adv-esr115.37+] → [client-bounty-form]

lets us know once the CVE assigned ?

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 < 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.

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.

Keywords: sec-moderate → sec-low
Attached file (secure) —
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-esr140.12+]
Severity: -- → S3
Keywords: csectype-dos
Attached file (secure) —
Attachment #9595339 - Flags: approval-mozilla-beta?
Attached file (secure) —
Attachment #9595340 - Flags: approval-mozilla-esr140?
Attached file (secure) —
Attachment #9595341 - Flags: approval-mozilla-esr115?

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

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

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
Group: gfx-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
Attachment #9595341 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [client-bounty-form][adv-esr140.12+] → [client-bounty-form][adv-esr140.12+][adv-esr115.37+]
Attachment #9595339 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9595340 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c153/b152]
Whiteboard: [client-bounty-form][adv-esr140.12+][adv-esr115.37+] → [client-bounty-form][adv-esr140.12+][adv-esr115.37+][adv-main152+]
Alias: CVE-2026-12325
Flags: sec-bounty? → sec-bounty-

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.

Flags: sec-bounty-hof+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: