Skip to content

Raise a truncated chunk error for a short gAMA chunk - #9880

Open
VenishPaneliya wants to merge 1 commit into
python-pillow:mainfrom
VenishPaneliya:fix-truncated-gama-chunk
Open

Raise a truncated chunk error for a short gAMA chunk#9880
VenishPaneliya wants to merge 1 commit into
python-pillow:mainfrom
VenishPaneliya:fix-truncated-gama-chunk

Conversation

@VenishPaneliya

Copy link
Copy Markdown

Summary

chunk_gAMA reads the 4-byte gamma value with i32(s) without first checking the chunk length. A PNG whose gAMA chunk is shorter than 4 bytes raises struct.error, which ImageFile.__init__ converts to SyntaxError, which Image.open() treats as "this isn't a PNG" — so the user sees UnidentifiedImageError: cannot identify image file for a file that is otherwise a perfectly readable PNG.

LOAD_TRUNCATED_IMAGES can't rescue it either, because the failure happens before any truncation handling runs.

The sibling chunk handlers already do this correctly. IHDR, sRGB, pHYs, acTL, fcTL and fdAT all check the length first and either return the short chunk when LOAD_TRUNCATED_IMAGES is set, or raise ValueError("Truncated <cid> chunk"). gAMA is the one fixed-size chunk missing that guard — and it's also the one missing from the existing test_truncated_chunks parametrisation.

Reproduction

import io, struct, zlib
from PIL import Image, ImageFile

def chunk(tag, data):
    return (struct.pack(">I", len(data)) + tag + data
            + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))

ihdr = struct.pack(">IIBBBBB", 1, 1, 8, 0, 0, 0, 0)
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr)
        + chunk(b"gAMA", b"\x00\x01")                     # 2 bytes instead of 4
        + chunk(b"IDAT", zlib.compress(b"\x00\x00")) + chunk(b"IEND", b""))

ImageFile.LOAD_TRUNCATED_IMAGES = True
Image.open(io.BytesIO(data)).load()

Before:

UnidentifiedImageError: cannot identify image file <_io.BytesIO object ...>

(underlying cause: struct.error: unpack_from requires a buffer of at least 4 bytes)

After, matching sRGB exactly:

  • LOAD_TRUNCATED_IMAGES = FalseValueError: Truncated gAMA chunk
  • LOAD_TRUNCATED_IMAGES = True → loads fine

Changes

         s = ImageFile._safe_read(self.fp, length)
+        if length < 4:
+            if ImageFile.LOAD_TRUNCATED_IMAGES:
+                return s
+            msg = "Truncated gAMA chunk"
+            raise ValueError(msg)
         self.im_info["gamma"] = i32(s) / 100000.0

and b"gAMA" added to the existing test_truncated_chunks parametrisation, which already covers the other guarded chunks.

Tests

Tests/test_file_png.py::TestFilePng::test_truncated_chunks[gAMA] fails on current main with struct.error and passes with the guard. Full Tests/test_file_png.py: 65 passed, 1 skipped (the skip is Unix-only). ruff and black clean on both changed files.

One question

chunk_cHRM has the same crash — struct.unpack(f">{len(s) // 4}I", s) raises struct.error when the length isn't a multiple of 4. I left it out of this PR because the right guard there is a judgement call: strict (length < 32, per spec) would reject short-but-parseable chromaticity chunks that currently decode into a partial tuple. Happy to add whichever you prefer, here or separately.

gAMA carries a single 4-byte gamma value, but chunk_gAMA read it with
i32() without first checking the chunk length. A PNG whose gAMA chunk is
shorter than 4 bytes raised struct.error, which ImageFile turns into a
SyntaxError, so Image.open() reported "cannot identify image file" for a
file that is otherwise a perfectly readable PNG.

LOAD_TRUNCATED_IMAGES could not rescue it either: sRGB, pHYs, IHDR, acTL,
fcTL and fdAT all return the short chunk when it is set and otherwise
raise ValueError("Truncated <cid> chunk"), but gAMA failed before
reaching that logic. Guard it the same way and add gAMA to the existing
test_truncated_chunks parametrisation, which already covers the others.
@radarhere

Copy link
Copy Markdown
Member

Hi. Thanks for this. Curious question - did you actually find an image in the wild where this was truncated, or is this a theoretical concern?

Regarding cHRM, I think check for 32 length. Looking at the other chunks that check for length, they are checking for the full specified length. Feel free to add it in this PR.

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.

2 participants