diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index a5d5a15dbab..0d1c8dae4d3 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -2,6 +2,8 @@ import pytest +from PIL import Image + from .helper import assert_image_equal, hopper @@ -62,3 +64,36 @@ def test_f_mode() -> None: im = hopper("F") with pytest.raises(ValueError): 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) -> float: + if index >= 8: + raise IndexError + return float(index) + + im = Image.new("L", (4, 4)) + with pytest.raises(ValueError): + im.point(OverstatedLengthSequence(), "F") # type: ignore[arg-type] + + +def test_unsized_sequence() -> None: + # a table that only implements __getitem__ (no __len__) is a common shape + # for custom point tables; PySequence_Size() fails for it, so the cheap + # pre-check must fall back to materializing instead of erroring out + class UnsizedSequence: + def __getitem__(self, index: int) -> float: + if index >= 256: + raise IndexError + return float(index) + + im = Image.new("L", (4, 4)) + out = im.point(UnsizedSequence(), "F") # type: ignore[arg-type] + assert_image_equal(out, im.point(list(range(256)), "F")) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 1e727316d1d..601b2eb7e2f 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -99,6 +99,47 @@ def test_array_F() -> None: assert len(im.get_flattened_data()) == len(arr) +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) -> float: + if index >= 2: + raise IndexError + return float(index + 1) + + im = Image.new("L", (4, 4)) + im.putdata(OverstatedLengthSequence()) # type: ignore[arg-type] + assert im.get_flattened_data()[:2] == (1, 2) + + +def test_too_many_entries() -> None: + # an honest, correctly-reported sequence that is simply longer than the + # image still has to be rejected, before or after materialization + im = Image.new("L", (4, 4)) + with pytest.raises(TypeError): + im.putdata(list(range(17))) + + +def test_unsized_sequence() -> None: + # a sequence that only implements __getitem__ (no __len__) is common for + # custom point-table-like objects; PySequence_Size() fails for it, so the + # cheap pre-check must fall back to materializing instead of erroring out + class UnsizedSequence: + def __getitem__(self, index: int) -> int: + if index >= 4: + raise IndexError + return index + + im = Image.new("L", (2, 2)) + im.putdata(UnsizedSequence()) # type: ignore[arg-type] + assert im.get_flattened_data() == (0, 1, 2, 3) + + def test_not_flattened() -> None: im = Image.new("L", (1, 1)) with pytest.raises(TypeError): diff --git a/src/_imaging.c b/src/_imaging.c index 9bdb6328782..8f0cb99d9d5 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -442,9 +442,25 @@ getlist(PyObject *arg, Py_ssize_t *length, const char *wrong_length, int type) { return NULL; } - n = PySequence_Size(arg); + if (length && wrong_length) { + Py_ssize_t reported = PySequence_Size(arg); + if (reported < 0) { + PyErr_Clear(); + } else if (reported != *length) { + PyErr_SetString(PyExc_ValueError, wrong_length); + return NULL; + } + } + + seq = PySequence_Fast(arg, must_be_sequence); + if (!seq) { + return NULL; + } + + n = PySequence_Fast_GET_SIZE(seq); if (length && wrong_length && n != *length) { PyErr_SetString(PyExc_ValueError, wrong_length); + Py_DECREF(seq); return NULL; } @@ -452,15 +468,10 @@ getlist(PyObject *arg, Py_ssize_t *length, const char *wrong_length, int type) { calloc checks for overflow */ list = calloc(n, type & 0xff); if (!list) { + Py_DECREF(seq); return ImagingError_MemoryError(); } - seq = PySequence_Fast(arg, must_be_sequence); - if (!seq) { - free(list); - return NULL; - } - for (i = 0; i < n; i++) { op = PySequence_Fast_GET_ITEM(seq, i); // DRY, branch prediction is going to work _really_ well @@ -1625,8 +1636,26 @@ _putdata(ImagingObject *self, PyObject *args) { image = self->image; - n = PyObject_Length(data); + if (image->image8 && PyBytes_Check(data)) { + n = PyBytes_GET_SIZE(data); + } else { + Py_ssize_t reported = PySequence_Size(data); + if (reported < 0) { + PyErr_Clear(); + } 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) { + PyErr_SetString(PyExc_TypeError, must_be_sequence); + return NULL; + } + n = PySequence_Fast_GET_SIZE(seq); + } 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; } @@ -1667,11 +1696,6 @@ _putdata(ImagingObject *self, PyObject *args) { } } } else { - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } double value; int bigendian = 0; if (image->type == IMAGING_TYPE_SPECIAL) { @@ -1705,11 +1729,6 @@ _putdata(ImagingObject *self, PyObject *args) { } } else { /* 32-bit images */ - seq = PySequence_Fast(data, must_be_sequence); - if (!seq) { - PyErr_SetString(PyExc_TypeError, must_be_sequence); - return NULL; - } switch (image->type) { case IMAGING_TYPE_INT32: for (i = x = y = 0; i < n; i++) {