diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 3165ef1c330..f66be9333f1 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import zlib import pytest @@ -858,3 +859,101 @@ def test_pXl_decoder_buffer_size( im = Image.new("P", (9, 1)) with pytest.raises(ValueError, match="not enough image data"): im.frombytes(data, decoder_name, rawmode) + + @pytest.mark.parametrize( + "size, rawmode, data, stride", + ( + ((9, 1), "P;2L", b"\x01\x02\x03", 3), # packed size 3, planar size 4 + ((17, 1), "P;4L", bytes(range(1, 10)), 9), # packed 9, planar 12 + ), + ) + def test_pcx_pXl_decoder_buffer_size( + self, size: tuple[int, int], rawmode: str, data: bytes, stride: int + ) -> None: + # The PCX decoder takes its buffer size from the stride argument, so it + # must check against the planar row size itself + im = Image.new("P", size) + with pytest.raises(ValueError, match="cannot decode image data"): + im.frombytes(data, "pcx", rawmode, stride) + + @pytest.mark.parametrize( + "size, rawmode, data, stride", + ( + ((9, 1), "P;2L", b"\x01\x02\x03\x04", 4), + ((17, 1), "P;4L", bytes(range(1, 13)), 12), + ), + ) + def test_pcx_pXl_decoder_buffer_size_ok( + self, size: tuple[int, int], rawmode: str, data: bytes, stride: int + ) -> None: + im = Image.new("P", size) + im.frombytes(data, "pcx", rawmode, stride) + + @pytest.mark.parametrize( + "mode, rawmode, size, data, stride, expected", + ( + # Packed 2/4-bit rows keep their packed layout when the row is + # padded: only the planar raw modes (P;2L/P;4L) store padded bit + # planes, so the row collapse must not de-interleave these rows + ( + "L", + "L;2", + (9, 1), + b"\x94\x60\x80\x40\x00\x00", + 6, + (170, 85, 85, 0, 85, 170, 0, 0, 170), + ), + ( + "L", + "L;4", + (5, 1), + b"\x12\x34\x50\x00\x00\x00\x00\x00", + 8, + (17, 34, 51, 68, 85), + ), + ( + "P", + "P;2", + (9, 1), + b"\x94\x60\x80\x40\x00\x00", + 6, + (2, 1, 1, 0, 1, 2, 0, 0, 2), + ), + ( + "P", + "P;4", + (5, 1), + b"\x12\x34\x50\x00\x00\x00\x00\x00", + 8, + (1, 2, 3, 4, 5), + ), + ), + ) + def test_pcx_packed_low_bit_padded_row( + self, + mode: str, + rawmode: str, + size: tuple[int, int], + data: bytes, + stride: int, + expected: tuple[int, ...], + ) -> None: + # Packed L;2/L;4/P;2/P;4 rows are not bit planes; a padded stride must + # not change the decoded pixels (the rows are de-interleaved only for + # the planar raw modes) + im = Image.new(mode, size) + im.frombytes(data, "pcx", rawmode, stride) + assert tuple(im.get_flattened_data()) == expected + + def test_zip_pXl_interlaced_rejected(self) -> None: + # The interlaced unpacker stages a single byte per pixel; planar raw + # modes would read past it + im = Image.new("P", (16, 16)) + with pytest.raises(ValueError, match="unrecognized raw mode"): + im.frombytes(zlib.compress(b"\x00" * 32), "zip", "P;2L", 1) + + @pytest.mark.parametrize("rawmode", ("P;2L", "P;4L")) + def test_zip_pXl_not_interlaced(self, rawmode: str) -> None: + planes = 2 if rawmode == "P;2L" else 4 + im = Image.new("P", (9, 1)) + im.frombytes(zlib.compress(b"\x00" * (planes * 2 + 1)), "zip", rawmode) diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 380154ae109..d9b525ce0ca 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -49,6 +49,15 @@ TODO Other changes ============= +PCX and ZIP decoder row sizes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When using :py:meth:`~PIL.Image.Image.frombytes()` with the ``pcx`` decoder and a +caller-supplied stride, packed 2- and 4-bit rows padded to a byte boundary were +collapsed incorrectly, and the row-size arithmetic could overflow for very wide +images. The ZIP decoder no longer accepts planar raw modes when interlacing is +requested, since it cannot decode them correctly. + Resizing I;16B and I;16N images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/decode.c b/src/decode.c index 213c104309e..8a4ed7c43b9 100644 --- a/src/decode.c +++ b/src/decode.c @@ -834,6 +834,16 @@ PyImaging_ZipDecoderNew(PyObject *self, PyObject *args) { return NULL; } + /* The interlaced path feeds the unpacker one pixel at a time through a + single byte staging slot, while planar raw modes (P;2L/P;4L) read a + whole padded bit plane per call (#10186). */ + if (interlaced && + (rawmode == IMAGING_RAWMODE_P_2L || rawmode == IMAGING_RAWMODE_P_4L)) { + Py_DECREF(decoder); + PyErr_SetString(PyExc_ValueError, "unrecognized raw mode"); + return NULL; + } + decoder->decode = ImagingZipDecode; decoder->cleanup = ImagingZipDecodeCleanup; diff --git a/src/libImaging/PcxDecode.c b/src/libImaging/PcxDecode.c index d060be3e7bc..682d372323f 100644 --- a/src/libImaging/PcxDecode.c +++ b/src/libImaging/PcxDecode.c @@ -15,14 +15,34 @@ #include "Imaging.h" +#include + int ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { UINT8 n; UINT8 *ptr; - if ((state->xsize * state->bits + 7) / 8 > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; + /* Check that the buffer holds one line of input. Planar raw modes + (P;2L/P;4L) store each line as state->planes padded bit planes, + not as packed pixels (#10186). */ + if (state->planes) { + if (state->xsize > INT_MAX - 7) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + if (state->planes * ((state->xsize + 7) / 8) > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } + } else { + if (state->xsize > ((INT_MAX / state->bits) - 7)) { + state->errcode = IMAGING_CODEC_MEMORY; + return -1; + } + if ((state->bits * state->xsize + 7) / 8 > state->bytes) { + state->errcode = IMAGING_CODEC_OVERRUN; + return -1; + } } ptr = buf; @@ -62,9 +82,9 @@ ImagingPcxDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt if (state->x >= state->bytes) { int bands; int xsize = 0; - if (state->bits == 2 || state->bits == 4) { + if (state->planes) { xsize = (state->xsize + 7) / 8; - bands = state->bits; + bands = state->planes; } else { xsize = state->xsize; bands = im->bands;