From 3b1f70da61df02378594ba6f490e937da127d40f Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Sat, 21 Mar 2026 01:01:20 +1100
Subject: [PATCH] Simplify `setimage()` by always passing extents (#9395)

---
 src/PIL/Image.py |  4 ++--
 src/decode.c     | 15 +++++----------
 src/encode.c     | 15 +++++----------
 3 files changed, 12 insertions(+), 22 deletions(-)

Index: pillow-11.3.0/src/PIL/Image.py
===================================================================
--- pillow-11.3.0.orig/src/PIL/Image.py
+++ pillow-11.3.0/src/PIL/Image.py
@@ -800,7 +800,7 @@ class Image:
 
         # unpack data
         e = _getencoder(self.mode, encoder_name, encoder_args)
-        e.setimage(self.im)
+        e.setimage(self.im, (0, 0) + self.size)
 
         from . import ImageFile
 
@@ -871,7 +871,7 @@ class Image:
 
         # unpack data
         d = _getdecoder(self.mode, decoder_name, decoder_args)
-        d.setimage(self.im)
+        d.setimage(self.im, (0, 0) + self.size)
         s = d.decode(data)
 
         if s[0] >= 0:
Index: pillow-11.3.0/src/decode.c
===================================================================
--- pillow-11.3.0.orig/src/decode.c
+++ pillow-11.3.0/src/decode.c
@@ -163,7 +163,7 @@ _setimage(ImagingDecoderObject *decoder,
     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, "O(iiii)", &op, &x0, &y0, &x1, &y1)) {
         return NULL;
     }
     im = PyImaging_AsImaging(op);
@@ -171,27 +171,21 @@ _setimage(ImagingDecoderObject *decoder,
         return NULL;
     }
 
+    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 */
-    if (x0 == 0 && x1 == 0) {
-        state->xsize = im->xsize;
-        state->ysize = im->ysize;
-    } else {
-        state->xoff = x0;
-        state->yoff = y0;
-        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;
-    }
+    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) {
Index: pillow-11.3.0/src/encode.c
===================================================================
--- pillow-11.3.0.orig/src/encode.c
+++ pillow-11.3.0/src/encode.c
@@ -232,7 +232,7 @@ _setimage(ImagingEncoderObject *encoder,
     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, "O(nnnn)", &op, &x0, &y0, &x1, &y1)) {
         return NULL;
     }
     im = PyImaging_AsImaging(op);
@@ -240,26 +240,19 @@ _setimage(ImagingEncoderObject *encoder,
         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;
 
-    if (x0 == 0 && x1 == 0) {
-        state->xsize = im->xsize;
-        state->ysize = im->ysize;
-    } else {
-        state->xoff = x0;
-        state->yoff = y0;
-        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;
-    }
+    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) {
Index: pillow-11.3.0/Tests/test_file_psd.py
===================================================================
--- pillow-11.3.0.orig/Tests/test_file_psd.py
+++ pillow-11.3.0/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"
 
@@ -184,3 +190,17 @@ def test_layer_crashes(test_file: str) -
             assert isinstance(im, PsdImagePlugin.PsdImageFile)
             with pytest.raises(SyntaxError):
                 im.layers
+
+
+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()
Index: pillow-11.3.0/Tests/test_imagefile.py
===================================================================
--- pillow-11.3.0.orig/Tests/test_imagefile.py
+++ pillow-11.3.0/Tests/test_imagefile.py
@@ -301,52 +301,38 @@ class TestPyDecoder(CodecsTest):
         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("")
@@ -377,22 +363,18 @@ class TestPyEncoder(CodecsTest):
         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)
@@ -400,49 +382,28 @@ class TestPyEncoder(CodecsTest):
         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("")
Index: pillow-11.3.0/src/PIL/ImageFile.py
===================================================================
--- pillow-11.3.0.orig/src/PIL/ImageFile.py
+++ pillow-11.3.0/src/PIL/ImageFile.py
@@ -786,28 +786,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):
     """
