diff --git a/Tests/images/psd-oob-write-overflow.psd b/Tests/images/psd-oob-write-overflow.psd new file mode 100644 index 00000000000..c2bb10d614e Binary files /dev/null and b/Tests/images/psd-oob-write-overflow.psd differ diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 3b145b13983..538b1406b36 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,12 +1,18 @@ from __future__ import annotations +import sys import warnings import pytest from PIL import Image, PsdImagePlugin -from .helper import assert_image_equal_tofile, assert_image_similar, hopper, is_pypy +from .helper import ( + assert_image_equal_tofile, + assert_image_similar, + hopper, + is_pypy, +) test_file = "Tests/images/hopper.psd" @@ -204,3 +210,17 @@ def test_bounds_crash(test_file: str) -> None: with pytest.raises(ValueError): im.load() + + +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() + if sys.maxsize <= 2**32: + with pytest.raises(OverflowError): + im.seek(im.n_frames) + else: + im.seek(im.n_frames) + + with pytest.raises(ValueError): + im.load() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 6cb0d36a3f8..5177059055a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -316,52 +316,38 @@ def test_setimage(self) -> None: with pytest.raises(ValueError): MockPyDecoder.last.set_as_raw(b"\x00") - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - - im.load() - - assert MockPyDecoder.last.state.xoff == 0 - assert MockPyDecoder.last.state.yoff == 0 - assert MockPyDecoder.last.state.xsize == 200 - assert MockPyDecoder.last.state.ysize == 200 - - def test_negsize(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] + im.tile = [ImageFile._Tile("MOCK", extents, 32, None)] with pytest.raises(ValueError): im.load() - im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)] - with pytest.raises(ValueError): - im.load() - - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - im.tile = [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None - ) - ] + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - with pytest.raises(ValueError): - im.load() + im.load() - im.tile = [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None - ) - ] - with pytest.raises(ValueError): - im.load() + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_decode(self) -> None: decoder = ImageFile.PyDecoder("") @@ -392,22 +378,18 @@ def test_setimage(self) -> None: assert MockPyEncoder.last.state.xsize == xsize assert MockPyEncoder.last.state.ysize == ysize - def test_extents_none(self) -> None: - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - im.tile = [ImageFile._Tile("MOCK", None, 32, None)] - - fp = BytesIO() - ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) - - assert MockPyEncoder.last - assert MockPyEncoder.last.state.xoff == 0 - assert MockPyEncoder.last.state.yoff == 0 - assert MockPyEncoder.last.state.xsize == 200 - assert MockPyEncoder.last.state.ysize == 200 - - def test_negsize(self) -> None: + @pytest.mark.parametrize( + "extents", + ( + (-10, yoff, xoff + xsize, yoff + ysize), + (xoff, -10, xoff + xsize, yoff + ysize), + (xoff, yoff, -10, yoff + ysize), + (xoff, yoff, xoff + xsize, -10), + (xoff, yoff, xoff + xsize + 100, yoff + ysize), + (xoff, yoff, xoff + xsize, yoff + ysize + 100), + ), + ) + def test_extents(self, extents: tuple[int, int, int, int]) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -415,49 +397,28 @@ def test_negsize(self) -> None: fp = BytesIO() MockPyEncoder.last = None with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) last: MockPyEncoder | None = MockPyEncoder.last assert last assert last.cleanup_called with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", extents, 0, "RGB")]) - def test_oversize(self) -> None: + def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) + im.tile = [ImageFile._Tile("MOCK", None, 32, None)] fp = BytesIO() - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" - ) - ], - ) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) - with pytest.raises(ValueError): - ImageFile._save( - im, - fp, - [ - ImageFile._Tile( - "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" - ) - ], - ) + assert MockPyEncoder.last + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_encode(self) -> None: encoder = ImageFile.PyEncoder("") diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 9aab1744ccb..c70d93f3c97 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -806,6 +806,10 @@ def setimage( if extents: x0, y0, x1, y1 = extents + if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]: + msg = "Tile cannot extend outside image" + raise ValueError(msg) + self.state.xoff = x0 self.state.yoff = y0 self.state.xsize = x1 - x0 @@ -817,13 +821,6 @@ def setimage( msg = "Size must be positive" raise ValueError(msg) - if ( - self.state.xsize + self.state.xoff > self.im.size[0] - or self.state.ysize + self.state.yoff > self.im.size[1] - ): - msg = "Tile cannot extend outside image" - raise ValueError(msg) - class PyDecoder(PyCodec): """ diff --git a/src/decode.c b/src/decode.c index 71f8d73d29a..5c6c250987d 100644 --- a/src/decode.c +++ b/src/decode.c @@ -198,6 +198,12 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { } } + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > (int)im->xsize || + y1 > (int)im->ysize) { + PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); + return NULL; + } + decoder->im = im; state = &decoder->state; @@ -208,13 +214,6 @@ _setimage(ImagingDecoderObject *decoder, PyObject *args) { state->xsize = x1 - x0; state->ysize = y1 - y0; - if (state->xoff < 0 || state->xsize <= 0 || - state->xsize + state->xoff > (int)im->xsize || state->yoff < 0 || - state->ysize <= 0 || state->ysize + state->yoff > (int)im->ysize) { - PyErr_SetString(PyExc_ValueError, "tile cannot extend outside image"); - return NULL; - } - /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (!state->bytes) { diff --git a/src/encode.c b/src/encode.c index 26b744935d3..4b149653484 100644 --- a/src/encode.c +++ b/src/encode.c @@ -269,6 +269,11 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { return NULL; } + if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0 || x1 > im->xsize || y1 > im->ysize) { + PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); + return NULL; + } + encoder->im = im; state = &encoder->state; @@ -278,13 +283,6 @@ _setimage(ImagingEncoderObject *encoder, PyObject *args) { state->xsize = x1 - x0; state->ysize = y1 - y0; - if (state->xoff < 0 || state->xsize <= 0 || - state->xsize + state->xoff > im->xsize || state->yoff < 0 || - state->ysize <= 0 || state->ysize + state->yoff > im->ysize) { - PyErr_SetString(PyExc_SystemError, "tile cannot extend outside image"); - return NULL; - } - /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (state->xsize > ((INT_MAX / state->bits) - 7)) {