diff --git a/Tests/images/psd-large-bytecounts.psd b/Tests/images/psd-large-bytecounts.psd new file mode 100644 index 00000000000..6e71809936c Binary files /dev/null and b/Tests/images/psd-large-bytecounts.psd differ diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index f03a1f94601..ff0ed37dc68 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,7 +1,6 @@ from __future__ import annotations import io -import sys import warnings import pytest @@ -192,6 +191,20 @@ def test_unknown_channel_id() -> None: assert im.layers[0][1] == "" +@pytest.mark.parametrize("bbox", ((1, 0, 0, 1), (0, 1, 1, 0))) +def test_negative_tile_size(bbox: tuple[int, int, int, int]) -> None: + fp = io.BytesIO(b"\x00\x00") + with pytest.raises(ValueError, match="negative tile size"): + PsdImagePlugin._maketile(fp, "RGB", bbox, 3) + + +def test_large_bytecounts() -> None: + # The declared height requires a packbits bytecounts table of ~26 GB. + # Reading it must fail cleanly instead of attempting a huge allocation. + with pytest.raises(OSError, match="Truncated File Read"): + Image.open("Tests/images/psd-large-bytecounts.psd") + + def test_combined_larger_than_size() -> None: # The combined size of the individual parts is larger than the # declared 'size' of the extra data field, resulting in a backwards seek. @@ -251,5 +264,5 @@ def test_bounds_crash_overflow() -> None: with Image.open("Tests/images/psd-oob-write-overflow.psd") as im: assert isinstance(im, PsdImagePlugin.PsdImageFile) im.load() - with pytest.raises(OverflowError if sys.maxsize <= 2**32 else ValueError): + with pytest.raises(ValueError, match="negative tile size"): im.seek(im.n_frames) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index dd0afa46973..55395b307de 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -287,16 +287,15 @@ def read(size: int) -> bytes: def _maketile( file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int ) -> list[ImageFile._Tile]: - tiles = [] - read = file.read - - compression = i16(read(2)) - xsize = bbox[2] - bbox[0] ysize = bbox[3] - bbox[1] + if xsize < 0 or ysize < 0: + msg = "negative tile size" + raise ValueError(msg) + tiles = [] + compression = i16(file.read(2)) offset = file.tell() - if compression == 0: # # raw compression @@ -305,13 +304,13 @@ def _maketile( if mode == "CMYK": layer += ";I" tiles.append(ImageFile._Tile("raw", bbox, offset, layer)) - offset = offset + xsize * ysize + offset += xsize * ysize elif compression == 1: # # packbits compression i = 0 - bytecount = read(channels * ysize * 2) + bytecount = ImageFile._safe_read(file, channels * ysize * 2) offset = file.tell() for channel in range(channels): layer = mode[channel] @@ -319,13 +318,13 @@ def _maketile( layer += ";I" tiles.append(ImageFile._Tile("packbits", bbox, offset, layer)) for y in range(ysize): - offset = offset + i16(bytecount, i) + offset += i16(bytecount, i) i += 2 file.seek(offset) if offset & 1: - read(1) # padding + file.read(1) # padding return tiles