Skip to content

APNG: progressive (push) reader accepts a leading fcTL with incorrect frame size that the sequential reader rejects — pull-vs-push frame-count divergence (sibling of #854) #891

Description

@arielkoren

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

Image

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions