Raise ValueError for truncated gAMA and cHRM PNG chunks - #9880
Conversation
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.
|
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 holds 8 unsigned ints, so check for the full 32 bytes the same way
the other fixed-length chunks do.
Unpacking exactly 8 ints from the first 32 bytes also fixes two further
cases: a chunk longer than 32 bytes but not a multiple of 4 raised
struct.error, because ">{len(s) // 4}I" asked for fewer bytes than the
buffer held, and a 36-byte chunk quietly produced 9 chromaticity values
instead of 8.
|
Theoretical — I haven't got a real-world file for this. I found it by comparing the chunk handlers against each other: So it's a robustness/consistency fix rather than a response to a file someone hit in the wild. The part I'd argue is worth having is that
One thing I ran into while doing it: the length check alone doesn't cover everything, because the existing unpack derives its format from the buffer size: raw_vals = struct.unpack(f">{len(s) // 4}I", s)With So I unpack exactly 8 ints from the first 32 bytes: raw_vals = struct.unpack(">8I", s[:32])Behaviour now: That does change one existing behaviour: an over-length cHRM used to return the extra values, and now they're ignored. Since the chunk is specified as exactly 8 unsigned ints I think ignoring them is right, but say the word and I'll drop that half and leave just the
|
Summary
chunk_gAMAreads the 4-byte gamma value withi32(s)without first checking the chunk length. A PNG whosegAMAchunk is shorter than 4 bytes raisesstruct.error, whichImageFile.__init__converts toSyntaxError, whichImage.open()treats as "this isn't a PNG" — so the user seesUnidentifiedImageError: cannot identify image filefor a file that is otherwise a perfectly readable PNG.LOAD_TRUNCATED_IMAGEScan'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,fcTLandfdATall check the length first and either return the short chunk whenLOAD_TRUNCATED_IMAGESis set, or raiseValueError("Truncated <cid> chunk").gAMAis the one fixed-size chunk missing that guard — and it's also the one missing from the existingtest_truncated_chunksparametrisation.Reproduction
Before:
(underlying cause:
struct.error: unpack_from requires a buffer of at least 4 bytes)After, matching
sRGBexactly:LOAD_TRUNCATED_IMAGES = False→ValueError: Truncated gAMA chunkLOAD_TRUNCATED_IMAGES = True→ loads fineChanges
and
b"gAMA"added to the existingtest_truncated_chunksparametrisation, which already covers the other guarded chunks.Tests
Tests/test_file_png.py::TestFilePng::test_truncated_chunks[gAMA]fails on currentmainwithstruct.errorand passes with the guard. FullTests/test_file_png.py: 65 passed, 1 skipped (the skip is Unix-only).ruffandblackclean on both changed files.One question
chunk_cHRMhas the same crash —struct.unpack(f">{len(s) // 4}I", s)raisesstruct.errorwhen 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.