Summary
libpng's two read APIs disagree on how many frames the same APNG contains. For a
stream carrying a leading fcTL (the fcTL that describes the default image) whose
width/height do not match IHDR, the sequential reader
(png_read_frame_head / png_read_image) completes 0 frames, while the
progressive / push reader (png_process_data + frame callbacks) completes 1.
Same bytes, two different results — a pull-vs-push decode inconsistency.
This is the same class as #854 / CVE-2026-40930 (the short-fdAT push-mode smuggle),
but a distinct code path: it originates in png_handle_fcTL's leading-fcTL
size-mismatch handling, not in png_push_read_IDAT. It therefore survives the fix
for #854 (commit 9ec49c2d5) — I reproduced it on libpng18 HEAD (post-9ec49c2d5,
d1d0abeff).
Severity is low. There is no memory-safety issue here — no OOB, no underflow, no
sanitizer trip. The observable is purely behavioural: the two readers render a
different frame set. Filing it as a decode-consistency sibling of #854 in case libpng
wants the two read paths to agree (a malformed APNG rendering as an animation in a
streaming/push consumer but as nothing in a sequential one is a content-smuggling
surface for anything that relies on one path to validate what the other renders).
Root cause
A leading fcTL whose frame size differs from IHDR is warned-and-ignored:
pngrutil.c:2807 png_warning(png_ptr, "Ignoring leading fcTL with incorrect frame size");
Both readers hit this warning, but they then infer different frame sets for the
un-fcTL'd default image, so the sequential path errors out with 0 completed frames
while the progressive path emits 1. The fix for #854 (png_ensure_sequence_number(png_ptr, push_length) in png_push_read_IDAT) does not touch this path.
Reproduction
234-byte APNG, all CRCs and the zlib stream valid. Chunk layout:
| Offset |
Chunk |
Fields |
| 8 |
IHDR |
8×8, bit depth 8, colour type 2 (RGB) |
| 33 |
acTL |
num_frames=1, num_plays=0 |
| 53 |
fcTL |
seq=0, 8×1 ← leading fcTL, size ≠ IHDR (8×8) |
| 91 |
IDAT |
default image (31 bytes) |
| 134 |
fcTL |
seq=1, 1×8 |
| 172 |
fdAT |
seq=2, 34 bytes |
| 222 |
IEND |
|
Decode the same bytes through both APIs and compare the number of fully-completed
frames:
- Sequential (
png_read_info → png_read_frame_head / png_read_image): 0 frames
- Progressive (
png_process_data with png_set_progressive_read_fn +
png_set_progressive_frame_fn): 1 frame
Observed libpng diagnostics on the progressive path:
"Ignoring leading fcTL with incorrect frame size" (warning, then a frame is emitted).
The exact 234-byte input is attached to this issue as poc.apng.
Version
Reproduced on libpng18 @ d1d0abeff (post-9ec49c2d5, so this is not #854). Not
present as a memory issue at any version — it is a frame-count/consistency divergence
only.
Notes
I'm reporting this as a low-severity consistency issue, not a security vulnerability —
happy to close as wontfix if the project's position is that malformed-fcTL APNGs may
render differently between the two read paths. Reported it because it's a direct
sibling of #854 and lives one function over.

Summary
libpng's two read APIs disagree on how many frames the same APNG contains. For a
stream carrying a leading
fcTL(the fcTL that describes the default image) whosewidth/height do not match
IHDR, the sequential reader(
png_read_frame_head/png_read_image) completes 0 frames, while theprogressive / push reader (
png_process_data+ frame callbacks) completes 1.Same bytes, two different results — a pull-vs-push decode inconsistency.
This is the same class as #854 / CVE-2026-40930 (the short-fdAT push-mode smuggle),
but a distinct code path: it originates in
png_handle_fcTL's leading-fcTLsize-mismatch handling, not in
png_push_read_IDAT. It therefore survives the fixfor #854 (commit
9ec49c2d5) — I reproduced it onlibpng18HEAD (post-9ec49c2d5,d1d0abeff).Severity is low. There is no memory-safety issue here — no OOB, no underflow, no
sanitizer trip. The observable is purely behavioural: the two readers render a
different frame set. Filing it as a decode-consistency sibling of #854 in case libpng
wants the two read paths to agree (a malformed APNG rendering as an animation in a
streaming/push consumer but as nothing in a sequential one is a content-smuggling
surface for anything that relies on one path to validate what the other renders).
Root cause
A leading
fcTLwhose frame size differs fromIHDRis warned-and-ignored:Both readers hit this warning, but they then infer different frame sets for the
un-
fcTL'd default image, so the sequential path errors out with 0 completed frameswhile the progressive path emits 1. The fix for #854 (
png_ensure_sequence_number(png_ptr, push_length)inpng_push_read_IDAT) does not touch this path.Reproduction
234-byte APNG, all CRCs and the zlib stream valid. Chunk layout:
Decode the same bytes through both APIs and compare the number of fully-completed
frames:
png_read_info→png_read_frame_head/png_read_image): 0 framespng_process_datawithpng_set_progressive_read_fn+png_set_progressive_frame_fn): 1 frameObserved libpng diagnostics on the progressive path:
"Ignoring leading fcTL with incorrect frame size"(warning, then a frame is emitted).The exact 234-byte input is attached to this issue as
poc.apng.Version
Reproduced on
libpng18@d1d0abeff(post-9ec49c2d5, so this is not #854). Notpresent as a memory issue at any version — it is a frame-count/consistency divergence
only.
Notes
I'm reporting this as a low-severity consistency issue, not a security vulnerability —
happy to close as wontfix if the project's position is that malformed-fcTL APNGs may
render differently between the two read paths. Reported it because it's a direct
sibling of #854 and lives one function over.