From 4bada07dc6c24319edd1eb76f1dd28d968d58207 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 Feb 2026 22:24:03 +1100 Subject: [PATCH] Avoid overflow by not adding extents together Reference : https://security-tracker.debian.org/tracker/DSA-6357-1 CVE: CVE-2026-42311 Upstream-Status: Backport [https://github.com/python-pillow/Pillow/commit/58f9a1d166dcb0c274807d4423522d205b0c35ea] Signed-off-by: Hitendra Prajapati --- Tests/test_imagefile.py | 70 +++++++++++++++++++++++++++++++++++++++-- src/PIL/Image.py | 4 +-- src/PIL/ImageFile.py | 18 ++++------- src/decode.c | 61 +++++++++++++++++++++++------------ src/encode.c | 60 ++++++++++++++++++++++------------- 5 files changed, 156 insertions(+), 57 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 8aa102729..a00f111e1 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -142,6 +142,27 @@ class TestImageFile: with pytest.raises(SystemError, match="tile cannot extend outside image"): ImageFile._save(im, fp, [ImageFile._Tile("raw", xy + (1, 1), 0, "1")]) + def test_extents_none(self) -> None: + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=None)] + im.load() + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + with Image.open("Tests/images/hopper.jpg") as im: + im.tile = [im.tile[0]._replace(extents=extents)] # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + im.load() + + im2 = Image.new("L", (1, 1)) + fp = BytesIO() + tile = ImageFile._Tile("jpeg", None, 0, "L") + ImageFile._save(im2, fp, [tile]) + + for extents in ("invalid", (0,), ("0", "0", "0", "0")): + tile = tile._replace(extents=extents) # type: ignore[arg-type] + with pytest.raises(ValueError, match="invalid extents"): + ImageFile._save(im2, fp, [tile]) + def test_no_format(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -279,7 +300,20 @@ class TestPyDecoder(CodecsTest): assert MockPyDecoder.last.state.xsize == 200 assert MockPyDecoder.last.state.ysize == 200 - def test_negsize(self) -> None: + def test_negative_offset(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [ImageFile._Tile("MOCK", (-10, yoff, xsize, ysize), 32, None)] + + with pytest.raises(ValueError): + im.load() + + im.tile = [ImageFile._Tile("MOCK", (xoff, -10, xsize, ysize), 32, None)] + with pytest.raises(ValueError): + im.load() + + def test_negative_size(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) @@ -341,7 +375,39 @@ class TestPyEncoder(CodecsTest): assert MockPyEncoder.last.state.xsize == 200 assert MockPyEncoder.last.state.ysize == 200 - def test_negsize(self) -> None: + def test_negative_offset(self) -> None: + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + MockPyEncoder.last = None + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [ + ImageFile._Tile( + "MOCK", (-10, yoff, xoff + xsize, yoff + ysize), 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, -10, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], + ) + + def test_negative_size(self) -> None: buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index baef0aa11..94e021a48 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -759,7 +759,7 @@ class Image: # unpack data e = _getencoder(self.mode, encoder_name, args) - e.setimage(self.im) + e.setimage(self.im, (0, 0) + self.size) bufsize = max(65536, self.size[0] * 4) # see RawEncode.c @@ -822,7 +822,7 @@ class Image: # unpack data d = _getdecoder(self.mode, decoder_name, args) - d.setimage(self.im) + d.setimage(self.im, (0, 0) + self.size) s = d.decode(data) if s[0] >= 0: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 0283fa2fd..ac14d53ed 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -666,28 +666,22 @@ class PyCodec: if extents: (x0, y0, x1, y1) = extents - else: - (x0, y0, x1, y1) = (0, 0, 0, 0) - if x0 == 0 and x1 == 0: - self.state.xsize, self.state.ysize = self.im.size - else: + 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 self.state.ysize = y1 - y0 + else: + self.state.xsize, self.state.ysize = self.im.size if self.state.xsize <= 0 or self.state.ysize <= 0: msg = "Size cannot be negative" 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 43fa0ae3e..2b12a29bf 100644 --- a/src/decode.c +++ b/src/decode.c @@ -154,44 +154,65 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingDecoderObject *decoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; int x0, y0, x1, y1; - x0 = y0 = x1 = y1 = 0; - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(iiii)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } - - decoder->im = im; - - state = &decoder->state; - - /* Setup decoding tile extent */ - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + int e = (int)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } } - 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) { + 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; + + /* Setup decoding tile extent */ + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + /* 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 87426cdec..360f26f97 100644 --- a/src/encode.c +++ b/src/encode.c @@ -218,45 +218,63 @@ PyImaging_AsImaging(PyObject *op); static PyObject * _setimage(ImagingEncoderObject *encoder, PyObject *args) { - PyObject *op; + PyObject *op, *extents; Imaging im; ImagingCodecState state; Py_ssize_t x0, y0, x1, y1; - /* Define where image data should be stored */ - - x0 = y0 = x1 = y1 = 0; - /* FIXME: should publish the ImagingType descriptor */ - if (!PyArg_ParseTuple(args, "O|(nnnn)", &op, &x0, &y0, &x1, &y1)) { + if (!PyArg_ParseTuple(args, "OO", &op, &extents)) { return NULL; } im = PyImaging_AsImaging(op); if (!im) { return NULL; } - - encoder->im = im; - - state = &encoder->state; - - if (x0 == 0 && x1 == 0) { - state->xsize = im->xsize; - state->ysize = im->ysize; + if (extents == Py_None) { + x0 = 0; + y0 = 0; + x1 = im->xsize; + y1 = im->ysize; } else { - state->xoff = x0; - state->yoff = y0; - state->xsize = x1 - x0; - state->ysize = y1 - y0; + if (!PyTuple_Check(extents) || PyTuple_GET_SIZE(extents) != 4) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + for (int i = 0; i < 4; i++) { + PyObject *extent = PyTuple_GetItem(extents, i); + if (!PyLong_Check(extent)) { + PyErr_SetString(PyExc_ValueError, "invalid extents"); + return NULL; + } + Py_ssize_t e = (Py_ssize_t)PyLong_AsLong(extent); + + if (i == 0) { + x0 = e; + } else if (i == 1) { + y0 = e; + } else if (i == 2) { + x1 = e; + } else { + y1 = e; + } + } } - 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) { + 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; + + state->xoff = x0; + state->yoff = y0; + state->xsize = x1 - x0; + state->ysize = y1 - y0; + /* Allocate memory buffer (if bits field is set) */ if (state->bits > 0) { if (state->xsize > ((INT_MAX / state->bits) - 7)) { -- 2.50.1