Skip to content

gif/png: size findings to their real terminator, not to EOF (#58) - #59

Merged
nmatt0 merged 1 commit into
masterfrom
fix/gif-png-size
Oct 17, 2026
Merged

nmatt0 merged 1 commit into
masterfrom
fix/gif-png-size

Conversation

@nmatt0

@nmatt0 nmatt0 commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #58. GIF and PNG were magic-only signatures with no size expression and no validator, so a finding's size stayed unknown and resolve() back-filled it to the next finding or EOF. A small image embedded in a larger file was reported as spanning everything after it (GIF over-reported ~2860x, PNG ~1450x on the issue's repro). That mislabels trailing bytes as image in the map, lets a few-byte asset dominate the whole-file assessment, and makes -c/-e carve unrelated data. jpeg and bmp already compute their real extent; gif and png were the outliers.

Fix

Two validators, mirroring the jpeg one:

  • png: walk length | type | data | crc chunks from the IHDR to IEND and set the size. Each chunk's CRC-32 is checked; a full walk with every CRC valid reports verified, otherwise consistent.
  • gif: walk image-descriptor / extension blocks and their sub-block chains to the 0x3B trailer and set the size (consistent — GIF has no checksum to reach verified).

Both only size, they never drop a real image. The PNG signature+IHDR magic (16 bytes) and the GIF magic are decisive, so a truncated-but-valid image keeps its finding and falls back to size inference.

The gif validator additionally rejects clear false positives of the weak 6-byte magic: after the header and global color table, a real GIF must begin with a block introducer (0x2C/0x21/0x3B), and anything else is not a GIF. A truncated real GIF still has a valid first introducer, so it survives the gate. This kills the same "a stray GIF89a in binary data swallows the rest of the file" symptom from the false-positive direction.

Verification

  • Issue repro fixed: GIF 100035 -> 28, PNG 100069 -> 69, correct tiers.
  • Across a firmware corpus (403 gif/png findings): 103 GIF and 295 PNG now size exactly to their terminator (carved bytes end at 0x3B / IEND), and the gif gate drops 12 bogus magic-only matches.
  • The 5 that still fall back are genuinely unresolvable and were mis-sized before this change too: 1 corrupt PNG whose IDAT CRC fails with no reachable IEND, and 4 GIF false positives whose LZW image data does not decode to the declared pixel count. Resolving those would need a full LZW/zlib decoder and risks dropping truncated real images, so it is out of scope for a sizing fix.
  • Full test suite green. ASan/UBSan clean on the new test, the fixtures, and every corpus file containing a gif/png, for both identify and extract.

Regression test

tests/test_image_size.py is a hard gate (self-contained, stdlib zlib/struct, no external tool). It reproduces the over-sizing (fails without the validators), and covers embedded-at-offset sizing, a false-positive GIF rejection, and that a truncated real image is still identified rather than dropped.

GIF and PNG were magic-only signatures with no size expression and no
validator, so a finding's size stayed unknown and resolve() back-filled it
to the next finding or EOF. A small image embedded in a larger file was
therefore reported as spanning everything after it (GIF over-reported
~2860x, PNG ~1450x on the issue's repro), mislabeling the trailing bytes as
image in the map, letting a few-byte asset dominate the whole-file
assessment, and making -c/-e carve unrelated data. jpeg and bmp already
compute their real extent; gif and png were the outliers.

Add a validator for each, mirroring jpeg:

- png: walk length|type|data|crc chunks from the IHDR to IEND and set the
  size. Verify each chunk's CRC-32; a full walk with every CRC valid
  reports verified, otherwise consistent.
- gif: walk image-descriptor / extension blocks and their sub-block chains
  to the 0x3B trailer and set the size (consistent; GIF has no checksum).

Both only size, they never drop a real image: the PNG signature+IHDR magic
and the GIF magic are decisive, so a truncated-but-valid image keeps its
finding and falls back to size inference. The gif validator additionally
rejects clear false positives of the weak 6-byte magic: after the header
and global color table a real GIF must begin with a block introducer
(0x2C/0x21/0x3B), and anything else is not a GIF. A truncated real GIF
still has a valid first introducer, so it survives the gate.

Verification: the issue repro is fixed (GIF 100035 -> 28, PNG 100069 -> 69).
Across a firmware corpus, 103 GIF and 295 PNG findings now size exactly to
their terminator (carved bytes end at 0x3B / IEND), and the gif gate drops
12 bogus magic-only matches. The handful that still fall back are genuinely
unresolvable (a corrupt PNG whose IDAT CRC fails with no reachable IEND, and
GIF false positives whose LZW image data does not decode) and were mis-sized
before this change too. Full test suite green; ASan/UBSan clean on the new
test, the fixtures, and every corpus file containing a gif/png, for both
identify and extract. New tests/test_image_size.py is a hard gate: it
reproduces the over-sizing (fails without the validators) and also covers
embedded-at-offset sizing, a false-positive GIF rejection, and that a
truncated real image is still identified rather than dropped.
@nmatt0
nmatt0 merged commit 46e4e28 into master Oct 17, 2026
4 checks passed
@nmatt0
nmatt0 deleted the fix/gif-png-size branch October 17, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GIF/PNG findings are sized to EOF (or next finding) instead of to their real end

1 participant