From 876b700525567d6fe1d7374218caaca3371f301f Mon Sep 17 00:00:00 2001 From: Altafk960 Date: Sun, 25 Oct 2026 23:07:50 +0530 Subject: [PATCH 1/3] Check planar line size in ImagingPcxDecode ImagingPcxDecode validated its input line against the packed-pixel formula (xsize * bits + 7) / 8, but the P;2L/P;4L unpackers consume planes * ((xsize + 7) / 8) bytes per line, with each bit plane padded to a byte (#10186). A caller-supplied stride equal to the packed size passed the check while unpackP2L/unpackP4L read up to three bytes past the decode buffer. Validate the line against the same planes-aware, overflow-guarded expression now used by decode.c:_setimage and ImagingRawDecode, and key the band-interleave collapse on state->planes instead of the "bits == 2 || bits == 4" heuristic, so that packed L;2/L;4/P;2/P;4 rows are no longer de-interleaved when the stride carries padding. Tests extend #10186's test_pXl_decoder_buffer_size matrix to the pcx decoder and pin the packed low-bit row layout with padded strides. --- Tests/test_lib_pack.py | 85 ++++++++++++++++++++++++++++++++++++++ src/libImaging/PcxDecode.c | 30 +++++++++++--- 2 files changed, 110 insertions(+), 5 deletions(-) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 3165ef1c330..88c28a11c1c 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -858,3 +858,88 @@ 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 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; From 242b849fe2a7196a2e6c00af510640ed6c5ccb9b Mon Sep 17 00:00:00 2001 From: Altafk960 Date: Sun, 25 Oct 2026 23:07:57 +0530 Subject: [PATCH 2/3] Reject planar raw modes for interlaced ZIP data The interlaced bits < 8 path of ImagingZipDecode stages one bit-pixel at a time in a single byte and calls the shuffle with that one-byte buffer, while the P;2L/P;4L unpackers read planes padded bytes per call and would read past it. Reject the combination at decoder construction with "unrecognized raw mode", mirroring the putpalette rejection in #10186. Non-interlaced planar raw modes keep working and are covered by regression tests. --- Tests/test_lib_pack.py | 14 ++++++++++++++ src/decode.c | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 88c28a11c1c..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 @@ -943,3 +944,16 @@ def test_pcx_packed_low_bit_padded_row( 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/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; From ee5e92393435fb00b548264c650f4d489e380376 Mon Sep 17 00:00:00 2001 From: Altafk960 Date: Mon, 26 Oct 2026 15:55:19 +0530 Subject: [PATCH 3/3] Add release note --- docs/releasenotes/13.1.0.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 5edc0d28693..6e145655941 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^