Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions Tests/test_lib_pack.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
import zlib

import pytest

Expand Down Expand Up @@ -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)
9 changes: 9 additions & 0 deletions docs/releasenotes/13.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
10 changes: 10 additions & 0 deletions src/decode.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
30 changes: 25 additions & 5 deletions src/libImaging/PcxDecode.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,34 @@

#include "Imaging.h"

#include <limits.h>

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;
Expand Down Expand Up @@ -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;
Expand Down
Loading