Your message dated Sat, 11 Jul 2026 10:32:48 +0000
with message-id <[email protected]>
and subject line Released in 13.6
has caused the Debian Bug report #1140505,
regarding trixie-pu: package psd-tools/1.10.7+dfsg.1-1+deb13u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1140505: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1140505
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected]
Control: affects -1 + src:psd-tools
User: [email protected]
Usertags: pu

  * CVE-2026-27809: Compression module vulnerabilities (Closes: #1129098)
diffstat for psd-tools-1.10.7+dfsg.1 psd-tools-1.10.7+dfsg.1

 changelog                                                               |    7 
 patches/0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch |  596 
++++++++++
 patches/series                                                          |    1 
 3 files changed, 604 insertions(+)

diff -Nru psd-tools-1.10.7+dfsg.1/debian/changelog 
psd-tools-1.10.7+dfsg.1/debian/changelog
--- psd-tools-1.10.7+dfsg.1/debian/changelog    2025-04-14 07:35:59.000000000 
+0300
+++ psd-tools-1.10.7+dfsg.1/debian/changelog    2026-06-21 20:06:58.000000000 
+0300
@@ -1,3 +1,10 @@
+psd-tools (1.10.7+dfsg.1-1+deb13u1) trixie; urgency=medium
+
+  * Non-maintainer upload.
+  * CVE-2026-27809: Compression module vulnerabilities (Closes: #1129098)
+
+ -- Adrian Bunk <[email protected]>  Sun, 21 Jun 2026 20:06:58 +0300
+
 psd-tools (1.10.7+dfsg.1-1) unstable; urgency=low
 
   * New upstream release.
diff -Nru 
psd-tools-1.10.7+dfsg.1/debian/patches/0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch
 
psd-tools-1.10.7+dfsg.1/debian/patches/0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch
--- 
psd-tools-1.10.7+dfsg.1/debian/patches/0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch
      1970-01-01 02:00:00.000000000 +0200
+++ 
psd-tools-1.10.7+dfsg.1/debian/patches/0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch
      2026-06-21 20:06:58.000000000 +0300
@@ -0,0 +1,596 @@
+From 507976f67adf6b4b3315f555098c964f0c00fb9e Mon Sep 17 00:00:00 2001
+From: Kota Yamaguchi <[email protected]>
+Date: Tue, 24 Feb 2026 10:47:06 +0900
+Subject: Fix compression security issues (GHSA-24p2-j2jr-386w) (#549)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+* Fix crash on invalid RLE compression in decompress()
+
+When a PSD file contains malformed RLE-compressed image data (e.g. a
+literal run that extends past the expected row size), decode_rle() raises
+ValueError which propagated all the way to the user, crashing
+psd.composite() and psd-tools export.
+
+decompress() already had a fallback that replaces failed channels with
+black pixels when result is None, but it never triggered because the
+ValueError from decode_rle() was not caught. Wrap the decode_rle() call
+in a try/except so the existing fallback handles the error gracefully.
+
+Fixes #544
+
+Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
+
+* Make RLE decoder tolerant of non-standard PackBits encoding
+
+Investigation of issue #544 revealed four patterns in the wild that
+Photoshop decodes gracefully but the previous strict decoder rejected:
+
+1. Overflow runs (421 rows): a repeat or copy run whose output would
+   exceed row_size.  Decoder now clips to the remaining output space.
+
+2. Truncated copy runs (238 rows): input stream shorter than the
+   declared run length.  Decoder copies available bytes and zero-pads.
+
+3. Lone repeat header at end of row (4 rows): Cython decoder crashed
+   with IndexError (uncaught by the ValueError safety net).  Fixed by
+   guarding data[i] access for both repeat and copy run headers.
+
+4. Short output rows (8 rows): all input consumed before row_size bytes
+   decoded.  Pre-allocated zero-filled buffer handles this for free.
+
+Both rle.py and _rle.pyx are updated with matching logic; the Cython
+extension is rebuilt.  The safety-net except in decompress() now also
+catches IndexError and emits a structured logger.warning instead of the
+previous silent swallow.
+
+Tests: renamed test_malicious → test_tolerant_decode with correct
+expected values; added five new low-level decode() tolerance tests;
+updated test_decompress_invalid_rle_fallback_to_black to assert the
+correct clipped output now that the decoder succeeds instead of falling
+back to black.
+
+Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
+
+* Fix compression security issues (GHSA-24p2-j2jr-386w)
+
+Address five remaining findings from the security review of the
+compression module:
+
+1. ZIP bomb protection: replace bare zlib.decompress() with a new
+   _safe_zlib_decompress() helper that uses zlib.decompressobj with
+   a hard max_length cap.  Decompressed output exceeding the expected
+   channel size now falls back to a black channel (consistent with the
+   existing tolerant RLE behaviour) instead of exhausting memory.
+
+2. Dimension bounds validation: decompress() now validates width,
+   height, and depth against Adobe-spec limits before any allocation
+   (width/height in [1, 300000], depth in {1, 8, 16, 32}).
+
+3. assert -> raise: the 'assert len(result) == length' guard is
+   replaced with an explicit 'if … raise ValueError' so it cannot be
+   silently disabled with python -O.
+
+4. PSDDecompressionWarning: a new UserWarning subclass is emitted
+   (alongside the existing logger.warning) whenever any codec falls
+   back to a black channel.  Re-exported from psd_tools.__init__ so
+   callers can filter or escalate it via warnings.filterwarnings.
+
+5. Cython type fix (_rle.pyx): all loop indices in decode() changed
+   from 'cdef int' to 'cdef Py_ssize_t', eliminating the signed-int /
+   Py_ssize_t mismatch that caused undefined behaviour for row sizes
+   > INT_MAX.  encode() zero-length branch now returns the explicit
+   empty std::string instead of relying on implicit memoryview coercion.
+
+Dev: add cython and setuptools to the dev dependency group so the
+Cython extension can be rebuilt locally.
+
+Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
+
+* style: apply ruff formatting to compression/__init__.py
+
+Wrap long _warn_decompress_failure call for ZIP_WITH_PREDICTION to
+satisfy the line-length rule that failed CI.
+
+Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
+
+---------
+
+Co-authored-by: Claude Sonnet 4.6 <[email protected]>
+---
+ src/psd_tools/__init__.py                     |   3 +-
+ src/psd_tools/compression/__init__.py         | 101 ++++++++++++++--
+ src/psd_tools/compression/_rle.pyx            |  48 ++++----
+ src/psd_tools/compression/rle.py              |  38 +++---
+ .../psd_tools/compression/test_compression.py | 113 ++++++++++++++++++
+ tests/psd_tools/compression/test_rle.py       |  34 +++---
+ 6 files changed, 273 insertions(+), 64 deletions(-)
+
+diff --git a/src/psd_tools/__init__.py b/src/psd_tools/__init__.py
+index ccfdcc5..a556666 100644
+--- a/src/psd_tools/__init__.py
++++ b/src/psd_tools/__init__.py
+@@ -1,4 +1,5 @@
+ from psd_tools.api.psd_image import PSDImage
++from psd_tools.compression import PSDDecompressionWarning
+ from psd_tools.version import __version__
+ 
+-__all__ = ["PSDImage", "__version__"]
++__all__ = ["PSDImage", "PSDDecompressionWarning", "__version__"]
+diff --git a/src/psd_tools/compression/__init__.py 
b/src/psd_tools/compression/__init__.py
+index 8a4bf50..c4415a2 100644
+--- a/src/psd_tools/compression/__init__.py
++++ b/src/psd_tools/compression/__init__.py
+@@ -8,6 +8,7 @@ from typing import Iterator
+ import array
+ import io
+ import logging
++import warnings
+ import zlib
+ 
+ from PIL import Image
+@@ -28,6 +29,60 @@ except ImportError:
+ logger = logging.getLogger(__name__)
+ 
+ 
++class PSDDecompressionWarning(UserWarning):
++    """Issued when channel data cannot be fully decompressed.
++
++    The affected channel is replaced with black pixels.  Catch or filter this
++    warning to detect silently degraded images::
++
++        import warnings
++        from psd_tools.compression import PSDDecompressionWarning
++
++        with warnings.catch_warnings():
++            warnings.simplefilter("error", PSDDecompressionWarning)
++            psd = PSDImage.open("file.psd")
++    """
++
++
++_VALID_DEPTHS: frozenset[int] = frozenset((1, 8, 16, 32))
++_MAX_DIMENSION: int = 300_000  # PSD/PSB hard limit per the Adobe spec
++
++
++def _warn_decompress_failure(
++    codec: str,
++    exc: Exception,
++    width: int,
++    height: int,
++    depth: int,
++    version: int,
++) -> None:
++    """Log and emit a PSDDecompressionWarning for a failed channel decode."""
++    msg = (
++        "%s decode failed (%s: %s); channel replaced with black. "
++        "width=%d height=%d depth=%d version=%d"
++        % (codec, type(exc).__name__, exc, width, height, depth, version)
++    )
++    logger.warning(msg)
++    warnings.warn(msg, PSDDecompressionWarning, stacklevel=3)
++
++
++def _safe_zlib_decompress(data: bytes, max_length: int) -> bytes:
++    """Decompress *data* with a hard upper bound on output size.
++
++    Unlike :func:`zlib.decompress`, this function raises :exc:`ValueError`
++    if the decompressed output would exceed *max_length* bytes, preventing
++    memory exhaustion from crafted ZIP-bomb payloads.
++    """
++    d = zlib.decompressobj()
++    out = d.decompress(data, max_length + 1)
++    if d.unconsumed_tail:
++        raise ValueError(
++            "Decompressed size exceeds expected maximum of %d bytes" % 
max_length
++        )
++    out += d.flush()
++    return out
++
++
+ def compress(
+     data: bytes,
+     compression: Compression,
+@@ -72,24 +127,46 @@ def decompress(
+     :param data: compressed data bytes.
+     :param compression: compression type,
+             see :py:class:`~psd_tools.constants.Compression`.
+-    :param width: width.
+-    :param height: height.
+-    :param depth: bit depth of the pixel.
++    :param width: width in pixels; must be in [1, 300000].
++    :param height: height in pixels; must be in [1, 300000].
++    :param depth: bit depth of the pixel; must be one of 1, 8, 16, 32.
+     :param version: psd file version.
+     :return: decompressed data bytes.
++    :raises ValueError: if *width*, *height*, or *depth* are out of range.
+     """
++    if width < 1 or width > _MAX_DIMENSION:
++        raise ValueError("width %d out of range [1, %d]" % (width, 
_MAX_DIMENSION))
++    if height < 1 or height > _MAX_DIMENSION:
++        raise ValueError("height %d out of range [1, %d]" % (height, 
_MAX_DIMENSION))
++    if depth not in _VALID_DEPTHS:
++        raise ValueError("depth %d not in %s" % (depth, 
sorted(_VALID_DEPTHS)))
++
+     length = width * height * max(1, depth // 8)
+ 
+     result = None
+     if compression == Compression.RAW:
+         result = data[:length]
+     elif compression == Compression.RLE:
+-        result = decode_rle(data, width, height, depth, version)
++        try:
++            result = decode_rle(data, width, height, depth, version)
++        except (ValueError, IndexError) as e:
++            _warn_decompress_failure("RLE", e, width, height, depth, version)
++            result = None
+     elif compression == Compression.ZIP:
+-        result = zlib.decompress(data)
++        try:
++            result = _safe_zlib_decompress(data, length)
++        except (ValueError, zlib.error) as e:
++            _warn_decompress_failure("ZIP", e, width, height, depth, version)
++            result = None
+     else:
+-        decompressed = zlib.decompress(data)
+-        result = decode_prediction(decompressed, width, height, depth)
++        try:
++            decompressed = _safe_zlib_decompress(data, length)
++            result = decode_prediction(decompressed, width, height, depth)
++        except (ValueError, zlib.error) as e:
++            _warn_decompress_failure(
++                "ZIP_WITH_PREDICTION", e, width, height, depth, version
++            )
++            result = None
+ 
+     if depth >= 8:
+         if result is None:
+@@ -97,8 +174,14 @@ def decompress(
+             result = Image.new(mode, (width, height), color=0).tobytes()
+             logger.warning("Failed channel has been replaced by black")
+         else:
+-            assert len(result) == length, "len=%d, expected=%d" % 
(len(result), length)
+-
++            if len(result) != length:
++                raise ValueError(
++                    "Decompressed length mismatch: got %d, expected %d"
++                    % (len(result), length)
++                )
++
++    if result is None:
++        raise RuntimeError("decompress() produced no result for depth=%d" % 
depth)
+     return result
+ 
+ 
+diff --git a/src/psd_tools/compression/_rle.pyx 
b/src/psd_tools/compression/_rle.pyx
+index 6d2207b..18786dc 100644
+--- a/src/psd_tools/compression/_rle.pyx
++++ b/src/psd_tools/compression/_rle.pyx
+@@ -8,39 +8,45 @@ def decode(const unsigned char[:] data, Py_ssize_t size) -> 
string:
+     """decode(data, size) -> bytes
+ 
+     Apple PackBits RLE decoder.
++
++    Tolerant implementation: runs that would exceed *size* are clipped at the
++    row boundary, runs whose input is truncated copy what is available, and 
any
++    remaining bytes are zero-padded (std::string::resize zero-initialises).
++    The function always returns exactly *size* bytes without raising.
+     """
+ 
+-    cdef int i = 0
+-    cdef int j = 0
+-    cdef int length = data.shape[0]
++    cdef Py_ssize_t i = 0
++    cdef Py_ssize_t j = 0
++    cdef Py_ssize_t length = data.shape[0]
++    cdef Py_ssize_t actual, available
+     cdef unsigned char bit
+     cdef string result
+ 
++    result.resize(size)  # zero-initialised by std::string::resize
++
+     if length == 1:
+-        if data[0] != 128:
+-            raise ValueError('Invalid RLE compression')
++        # Single byte: either a no-op (128) or a stray header — return zeros
+         return result
+ 
+-    result.resize(size)
+-
+-    while i < length:
++    while i < length and j < size:
+         i, bit = i+1, data[i]
+         if bit > 128:
+             bit = 256 - bit
+-            if j+1+bit > size:
+-                raise ValueError('Invalid RLE compression')
+-            fill_n(result.begin()+j, 1+bit, <char>data[i])
+-            j += 1+bit
++            if i >= length:  # lone repeat header at end of stream — stop
++                break
++            actual = min(1+bit, size-j)  # clip at remaining output space
++            fill_n(result.begin()+j, actual, <char>data[i])
++            j += actual
+             i += 1
+         elif bit < 128:
+-            if i+1+bit > length or (j+1+bit > size):
+-                raise ValueError('Invalid RLE compression')
+-            copy_n(&data[i], 1+bit, result.begin()+j)
+-            j += 1+bit
+-            i += 1+bit
+-
+-    if size and (j != size):
+-        raise ValueError('Expected %d bytes but decoded %d bytes' % (size, j))
++            if i >= length:  # copy header is the last byte; nothing to copy
++                break
++            available = min(length-i, 1+bit)
++            actual = min(available, size-j)  # clip to input and output
++            copy_n(&data[i], actual, result.begin()+j)
++            j += actual
++            i += available  # advance by declared amount or to end
++        # bit == 128: no-op
+ 
+     return result
+ 
+@@ -58,7 +64,7 @@ def encode(const unsigned char[:] data) -> string:
+     cdef string result
+ 
+     if length == 0:
+-        return data
++        return result
+     if length == 1:
+         result.push_back(0)
+         result.push_back(data[0])
+diff --git a/src/psd_tools/compression/rle.py 
b/src/psd_tools/compression/rle.py
+index 19f07e1..e7822b8 100644
+--- a/src/psd_tools/compression/rle.py
++++ b/src/psd_tools/compression/rle.py
+@@ -4,36 +4,36 @@ def decode(data: bytes, size: int) -> bytes:
+     """decode(data, size) -> bytes
+ 
+     Apple PackBits RLE decoder.
++
++    Tolerant implementation: runs that would exceed *size* are clipped at the
++    row boundary, runs whose input is truncated copy what is available, and 
any
++    remaining bytes are zero-padded.  The function always returns exactly 
*size*
++    bytes without raising.
+     """
+ 
+     i, j = 0, 0
+     length = len(data)
+     data = bytearray(data)
+-    result = bytearray()
++    result = bytearray(size)  # pre-allocated and zero-filled
+ 
+-    if length == 1:
+-        if data[0] != 128:
+-            raise ValueError("Invalid RLE compression")
+-        return result
+-
+-    while i < length:
++    while i < length and j < size:
+         i, bit = i + 1, data[i]
+         if bit > 128:
+             bit = 256 - bit
+-            if j + 1 + bit > size:
+-                raise ValueError("Invalid RLE compression")
+-            result.extend((data[i : i + 1]) * (1 + bit))
+-            j += 1 + bit
++            if i >= length:  # lone repeat header at end of stream — stop
++                break
++            actual = min(1 + bit, size - j)  # clip at remaining output space
++            result[j : j + actual] = bytes([data[i]]) * actual
++            j += actual
+             i += 1
+         elif bit < 128:
+-            if i + 1 + bit > length or (j + 1 + bit > size):
+-                raise ValueError("Invalid RLE compression")
+-            result.extend(data[i : i + 1 + bit])
+-            j += 1 + bit
+-            i += 1 + bit
+-
+-    if size and (len(result) != size):
+-        raise ValueError("Expected %d bytes but decoded %d bytes" % (size, j))
++            copy_count = 1 + bit
++            available = length - i
++            actual = min(copy_count, available, size - j)  # clip to input 
and output
++            result[j : j + actual] = data[i : i + actual]
++            j += actual
++            i += min(copy_count, available)  # advance by declared amount or 
to end
++        # bit == 128: no-op
+ 
+     return bytes(result)
+ 
+diff --git a/tests/psd_tools/compression/test_compression.py 
b/tests/psd_tools/compression/test_compression.py
+index 2b71591..7189b46 100644
+--- a/tests/psd_tools/compression/test_compression.py
++++ b/tests/psd_tools/compression/test_compression.py
+@@ -1,16 +1,20 @@
+ from __future__ import print_function, unicode_literals
+ 
+ import logging
++import warnings
++import zlib
+ 
+ import pytest
+ 
+ from psd_tools.compression import (
++    PSDDecompressionWarning,
+     compress,
+     decode_prediction,
+     decode_rle,
+     decompress,
+     encode_prediction,
+     encode_rle,
++    rle_impl,
+ )
+ from psd_tools.constants import Compression
+ 
+@@ -77,6 +81,54 @@ def test_compress_decompress(data, kind, width, height, 
depth, version):
+     assert output == data, "output=%r, expected=%r" % (output, data)
+ 
+ 
++def test_decompress_rle_overflow_clips() -> None:
++    # Header: 1 row of 4 compressed bytes (\x00\x04).
++    # Row data: literal run header 0x02 = copy 3 bytes, but row_size is 2.
++    # Tolerant decoder clips to 2 bytes → correct decoded output, no fallback.
++    rle_data = b"\x00\x04\x02\x00\x00\x00"
++    width, height, depth = 2, 1, 8
++    result = decompress(rle_data, Compression.RLE, width, height, depth)
++    assert result == b"\x00\x00"
++
++
++# --- Low-level decode() tolerance tests (exercise both Python and Cython 
impls) ---
++
++
++def test_decode_rle_repeat_overflow() -> None:
++    # 0x82 = repeat-run header: 256 - 0x82 = 126, so repeat 127× next byte.
++    # row_size=3 → decoder should clip to 3 repetitions of 0xAA.
++    data = bytes([0x82, 0xAA])
++    assert rle_impl.decode(data, 3) == b"\xaa\xaa\xaa"
++
++
++def test_decode_rle_copy_overflow() -> None:
++    # 0x02 = copy-run header: copy 3 bytes, but row_size=2.
++    # Decoder should clip to 2 bytes.
++    data = bytes([0x02, 0x01, 0x02, 0x03])
++    assert rle_impl.decode(data, 2) == b"\x01\x02"
++
++
++def test_decode_rle_copy_truncated_input() -> None:
++    # 0x04 = copy-run header: copy 5 bytes, but only 3 bytes follow in the 
stream.
++    # Decoder should copy 3 available bytes and zero-pad the remaining 2.
++    data = bytes([0x04, 0x01, 0x02, 0x03])
++    assert rle_impl.decode(data, 5) == b"\x01\x02\x03\x00\x00"
++
++
++def test_decode_rle_lone_repeat_header() -> None:
++    # 0x82 = repeat-run header with no following pixel byte (stream ends).
++    # Should not raise (previously caused IndexError in Cython); returns 
zeros.
++    data = bytes([0x82])
++    assert rle_impl.decode(data, 4) == b"\x00\x00\x00\x00"
++
++
++def test_decode_rle_short_output() -> None:
++    # Stream is valid but encodes fewer bytes than row_size (zero-padded 
remainder).
++    # 0x00 = copy 1 byte (0xFF); row_size=4 → b"\xff\x00\x00\x00"
++    data = bytes([0x00, 0xFF])
++    assert rle_impl.decode(data, 4) == b"\xff\x00\x00\x00"
++
++
+ # This will fail due to irreversible zlib compression.
+ @pytest.mark.xfail
+ @pytest.mark.parametrize(
+@@ -97,3 +149,64 @@ def test_compress_decompress_fail(data, width, height, 
depth):
+     decoded = decompress(data, Compression.ZIP_WITH_PREDICTION, width, 
height, depth)
+     encoded = compress(decoded, Compression.ZIP_WITH_PREDICTION, width, 
height, depth)
+     assert data == encoded
++
++
++# ---------------------------------------------------------------------------
++# Security fix tests
++# ---------------------------------------------------------------------------
++
++
[email protected](
++    "kind",
++    [Compression.ZIP, Compression.ZIP_WITH_PREDICTION],
++)
++def test_decompress_zip_bomb_falls_back_to_black(kind: Compression) -> None:
++    """A zlib payload that expands beyond the declared channel size must not
++    exhaust memory; it should fall back to a black channel."""
++    oversized = zlib.compress(b"\x00" * 10_000)
++    result = decompress(oversized, kind, width=2, height=2, depth=8)
++    assert result == b"\x00" * 4  # black fallback for 2×2 8-bit
++
++
[email protected](
++    "kind",
++    [Compression.ZIP, Compression.ZIP_WITH_PREDICTION],
++)
++def test_decompress_zip_bomb_emits_psd_warning(kind: Compression) -> None:
++    """ZIP bomb fallback must emit PSDDecompressionWarning."""
++    oversized = zlib.compress(b"\x00" * 10_000)
++    with warnings.catch_warnings(record=True) as caught:
++        warnings.simplefilter("always")
++        decompress(oversized, kind, width=2, height=2, depth=8)
++    assert any(issubclass(w.category, PSDDecompressionWarning) for w in 
caught)
++
++
++def test_decompress_rle_failure_emits_psd_warning() -> None:
++    """An undecodable RLE channel must emit PSDDecompressionWarning."""
++    # version=1 row byte-count table needs height*2 bytes (unsigned short 
each).
++    # Providing only 1 byte forces array.frombytes to raise ValueError (not a
++    # multiple of 2), which is the path that triggers the warning.
++    bad_rle = b"\x00"
++    with warnings.catch_warnings(record=True) as caught:
++        warnings.simplefilter("always")
++        decompress(bad_rle, Compression.RLE, width=4, height=1, depth=8, 
version=1)
++    assert any(issubclass(w.category, PSDDecompressionWarning) for w in 
caught)
++
++
[email protected](
++    "bad_kwarg",
++    [
++        {"width": 0},
++        {"width": 300_001},
++        {"height": 0},
++        {"height": 300_001},
++        {"depth": 7},
++        {"depth": 0},
++    ],
++)
++def test_decompress_invalid_dimensions_raises(bad_kwarg: dict) -> None:
++    """Out-of-spec dimensions must raise ValueError immediately."""
++    params: dict = dict(width=4, height=4, depth=8, version=1)
++    params.update(bad_kwarg)
++    with pytest.raises(ValueError):
++        decompress(b"\x00" * 16, Compression.RAW, **params)
+diff --git a/tests/psd_tools/compression/test_rle.py 
b/tests/psd_tools/compression/test_rle.py
+index f7234de..a0b3378 100644
+--- a/tests/psd_tools/compression/test_rle.py
++++ b/tests/psd_tools/compression/test_rle.py
+@@ -26,20 +26,26 @@ def test_identical():
+     assert decoded_c == EDGE_CASE_1
+ 
+ @pytest.mark.parametrize(
+-    ("mod, data, size"),
++    ("mod, data, size, expected"),
+     [
+-        # b'\x01\x01\x01\x01'
+-        (rle, b"\xfd\x01", 3),
+-        (rle, b"\xfd\x01", 5),
+-        (_rle, b"\xfd\x01", 3),
+-        (_rle, b"\xfd\x01", 5),
+-        # b'\x01\x02\x03'
+-        (rle, b"\x02\x01\x02\x03", 2),
+-        (rle, b"\x02\x01\x02\x03", 4),
+-        (_rle, b"\x02\x01\x02\x03", 2),
+-        (_rle, b"\x02\x01\x02\x03", 4),
++        # 0xfd = repeat-run: 256-253=3, so repeat 4× byte 0x01.
++        # size=3 → overflow clipped: b'\x01\x01\x01'
++        (rle, b"\xfd\x01", 3, b"\x01\x01\x01"),
++        (_rle, b"\xfd\x01", 3, b"\x01\x01\x01"),
++        # size=5 → 4 real bytes + 1 zero-padded: b'\x01\x01\x01\x01\x00'
++        (rle, b"\xfd\x01", 5, b"\x01\x01\x01\x01\x00"),
++        (_rle, b"\xfd\x01", 5, b"\x01\x01\x01\x01\x00"),
++        # 0x02 = copy-run: copy 3 bytes (0x01 0x02 0x03).
++        # size=2 → overflow clipped: b'\x01\x02'
++        (rle, b"\x02\x01\x02\x03", 2, b"\x01\x02"),
++        (_rle, b"\x02\x01\x02\x03", 2, b"\x01\x02"),
++        # size=4 → 3 real bytes + 1 zero-padded: b'\x01\x02\x03\x00'
++        (rle, b"\x02\x01\x02\x03", 4, b"\x01\x02\x03\x00"),
++        (_rle, b"\x02\x01\x02\x03", 4, b"\x01\x02\x03\x00"),
+     ],
+ )
+-def test_malicious(mod, data, size):
+-    with pytest.raises(ValueError):
+-        mod.decode(data, size)
++def test_tolerant_decode(mod, data, size, expected) -> None:
++    # The decoder must never raise; it clips overflow runs and zero-pads 
short output.
++    result = mod.decode(data, size)
++    assert result == expected
++    assert len(result) == size
+-- 
+2.47.3
+
diff -Nru psd-tools-1.10.7+dfsg.1/debian/patches/series 
psd-tools-1.10.7+dfsg.1/debian/patches/series
--- psd-tools-1.10.7+dfsg.1/debian/patches/series       1970-01-01 
02:00:00.000000000 +0200
+++ psd-tools-1.10.7+dfsg.1/debian/patches/series       2026-06-21 
20:06:53.000000000 +0300
@@ -0,0 +1 @@
+0001-Fix-compression-security-issues-GHSA-24p2-j2jr-386w-.patch

--- End Message ---
--- Begin Message ---
Version: 13.6

This update was released as part of 13.6.

--- End Message ---

Reply via email to