From 52470ec6afda3efa67b509c4f330cb18902ef7b1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Oct 2026 16:48:02 +1100 Subject: [PATCH] Restrict data types --- Tests/test_image_point.py | 22 +++-------------- Tests/test_image_putdata.py | 20 +--------------- src/PIL/Image.py | 48 +++++++++++++++++++++++++++++++++++++ src/PIL/ImageFilter.py | 23 ++++++++++-------- src/_imaging.c | 35 +++++++-------------------- 5 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 6c3cf927de9..b688dcddf38 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -66,24 +66,6 @@ def test_f_mode() -> None: im.point([]) -def test_overstated_length() -> None: - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/9892 - - class OverstatedLengthSequence: - def __len__(self) -> int: - return 256 - - def __getitem__(self, index: int) -> int: - if index >= 8: - raise IndexError - return index - - im = Image.new("L", (4, 4)) - with pytest.raises(ValueError): - im.point(OverstatedLengthSequence(), "F") # type: ignore[arg-type] - - def test_raising_length() -> None: class RaisingLengthSequence: def __len__(self) -> int: @@ -93,5 +75,7 @@ def __getitem__(self, index: int) -> float: return index im = Image.new("L", (1, 1)) - with pytest.raises(RuntimeError): + with pytest.raises( + TypeError, match="argument must be a sequence, callable or ImagePointHandler" + ): im.point(RaisingLengthSequence(), "F") # type: ignore[arg-type] diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 81ab8cc4a13..65125293061 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -105,24 +105,6 @@ def test_too_many_entries() -> None: im.putdata(list(range(17))) -def test_overstated_length() -> None: - # shouldn't segfault - # see https://github.com/python-pillow/Pillow/issues/9892 - - class OverstatedLengthSequence: - def __len__(self) -> int: - return 16 - - def __getitem__(self, index: int) -> int: - if index >= 2: - raise IndexError - return index + 1 - - im = Image.new("L", (4, 4)) - im.putdata(OverstatedLengthSequence()) # type: ignore[arg-type] - assert im.get_flattened_data() == (1, 2, *(0,) * 14) - - def test_raising_length() -> None: class RaisingLengthSequence: def __len__(self) -> int: @@ -132,7 +114,7 @@ def __getitem__(self, index: int) -> int: return index im = Image.new("L", (1, 1)) - with pytest.raises(RuntimeError): + with pytest.raises(TypeError, match="argument must be a sequence"): im.putdata(RaisingLengthSequence()) # type: ignore[arg-type] diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a55cdccd710..5f8897e8676 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -68,6 +68,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence + from types import ModuleType from typing import Any, Literal, Self @@ -614,6 +615,17 @@ def getdata( ) -> tuple[Transform, Sequence[int]]: ... +def _is_numpy_array(data: object) -> bool: + numpy: ModuleType | None = None + if hasattr(data, "shape"): + try: + import numpy + except ImportError: + pass + + return numpy is not None and isinstance(data, numpy.ndarray) + + class Image: """ This class represents an image object. To create @@ -2045,6 +2057,14 @@ def point(self, im: Image) -> Image: # for other modes, convert the function to a table flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] else: + if type(lut) in (list, tuple): + for value in lut: + if type(value) not in (int, float): + msg = "sequence must be flattened" + raise TypeError(msg) + elif not _is_numpy_array(lut): + msg = "argument must be a sequence, callable or ImagePointHandler" + raise TypeError(msg) flatLut = lut if self.mode == "F": @@ -2130,6 +2150,34 @@ def putdata( :param scale: An optional scale value. The default is 1.0. :param offset: An optional offset value. The default is 0.0. """ + if type(data) in (list, tuple): + singleChannel = getmodebands(self.mode) == 1 + for value in data: + if singleChannel: + if type(value) not in (int, float): + msg = "sequence must be flattened" + raise TypeError(msg) + elif type(value) in (list, tuple): + assert not isinstance(value, float) + if not all(type(value2) is int for value2 in value): + msg = ( + "color must be int, or tuple of one, three or four elements" + ) + raise TypeError(msg) + elif type(value) is not int: + msg = "color must be int or tuple" + raise TypeError(msg) + else: + import array + + if ( + not isinstance(data, array.array) + and not _is_numpy_array(data) + # ImagingCore is only to support deprecated getdata() + and data.__class__.__name__ != "ImagingCore" + ): + msg = "argument must be a sequence" + raise TypeError(msg) self._ensure_mutable() diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 2a2499caef8..0debcafd8cc 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -19,10 +19,11 @@ import abc from typing import cast +from PIL import Image + TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Callable, Sequence - from types import ModuleType from typing import Any from . import _imaging @@ -73,6 +74,15 @@ def __init__( scale: float | None = None, offset: float = 0, ) -> None: + if type(kernel) in (list, tuple): + for value in kernel: + if type(value) not in (int, float): + msg = "sequence must be flattened" + raise TypeError(msg) + else: + msg = "argument must be a sequence" + raise TypeError(msg) + if scale is None: # default scale is sum of kernel scale = sum(kernel) @@ -407,15 +417,8 @@ def __init__( items = size[0] * size[1] * size[2] wrong_size = False - numpy: ModuleType | None = None - if hasattr(table, "shape"): - try: - import numpy - except ImportError: - pass - - if numpy and isinstance(table, numpy.ndarray): - numpy_table: NumpyArray = table + if Image._is_numpy_array(table): + numpy_table = cast("NumpyArray", table) if copy_table: numpy_table = numpy_table.copy() diff --git a/src/_imaging.c b/src/_imaging.c index bd4231f46c9..a764c86c94f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -428,14 +428,6 @@ getlist_impl(PyObject *arg, Py_ssize_t length, const char *wrong_length, int typ return NULL; } - Py_ssize_t reported = PySequence_Size(arg); - if (reported < 0) { - return NULL; - } else if (reported != length) { - PyErr_SetString(PyExc_ValueError, wrong_length); - return NULL; - } - seq = PySequence_Fast(arg, must_be_sequence); if (!seq) { return NULL; @@ -1657,25 +1649,8 @@ _putdata(ImagingObject *self, PyObject *args) { image = self->image; - if (image->image8 && PyBytes_Check(data)) { - n = PyBytes_GET_SIZE(data); - } else { - Py_ssize_t reported = PySequence_Size(data); - if (reported < 0) { - return NULL; - } else if (reported > (Py_ssize_t)image->xsize * (Py_ssize_t)image->ysize) { - PyErr_SetString(PyExc_TypeError, "too many data entries"); - return NULL; - } - - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - return NULL; - } - n = PySequence_Fast_GET_SIZE(seq); - } + n = PyObject_Length(data); if (n > (Py_ssize_t)image->xsize * (Py_ssize_t)image->ysize) { - Py_XDECREF(seq); PyErr_SetString(PyExc_TypeError, "too many data entries"); return NULL; } @@ -1716,6 +1691,10 @@ _putdata(ImagingObject *self, PyObject *args) { } } } else { + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + return NULL; + } double value; int bigendian = 0; if (image->type == IMAGING_TYPE_I16) { @@ -1749,6 +1728,10 @@ _putdata(ImagingObject *self, PyObject *args) { } } else { /* 32-bit images */ + seq = PySequence_Fast(data, must_be_sequence); + if (!seq) { + return NULL; + } switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) {