VYPR
Moderate severityNVD Advisory· Published Feb 25, 2026· Updated Feb 26, 2026

psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps

CVE-2026-27809

Description

psd-tools is a Python package for working with Adobe Photoshop PSD files. Prior to version 1.12.2, 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. The fix in version 1.12.2 wraps the decode_rle() call in a try/except so the existing fallback handles the error gracefully.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

psd-tools before 1.12.2 crashes on malformed RLE-compressed PSD data because an unhandled ValueError bypasses the existing black-pixel fallback.

Root

Cause

psd-tools is a Python package for parsing Adobe Photoshop PSD files. Prior to version 1.12.2, the decode_rle() function raised a ValueError when processing malformed RLE-compressed image data (for example, a literal run that extends past the expected row size) [1]. Although decompress() contained a fallback that would replace failed channels with black pixels, this fallback relied on result being None and never triggered because the ValueError from decode_rle() propagated uncaught, crashing psd.composite() and export operations [1].

Exploitation

An attacker can craft a PSD file with deliberately invalid RLE-compressed channel data. No authentication or special network position is required; the file only needs to be opened or processed by psd-tools’ composite or export functions [1]. The bug is entirely in the packet‑decoding layer, so any code path that calls into the compression module is affected.

Impact

Successfully exploiting this vulnerability causes a denial‑of‑service condition: the application using psd-tools terminates with an unhandled ValueError exception. No sensitive data is leaked, but availability is lost for any workflow that automatically processes untrusted PSD files [1]. The same advisory notes additional hardening gaps (unguarded zlib.decompress and missing dimension validation) that are addressed in the same release [2].

Mitigation

The fix was released in psd-tools version 1.12.2 [4]. The commit wraps the decode_rle() call in a try/except block, allowing the existing black‑pixel fallback to apply gracefully [1][3]. Users should upgrade to 1.12.2 or later; no workaround is available for older versions [4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
psd-toolsPyPI
< 1.12.21.12.2

Affected products

2

Patches

1
6c0a78f195b5

Fix compression security issues (GHSA-24p2-j2jr-386w) (#549)

https://github.com/psd-tools/psd-toolsKota YamaguchiFeb 24, 2026via ghsa
8 files changed · +326 65
  • pyproject.toml+2 0 modified
    @@ -62,6 +62,8 @@ dev = [
         "aggdraw>=1.4.1;sys_platform=='win32' and python_version>='3.11'",
         "scipy",
         "scikit-image",
    +    "cython>=3.2.4",
    +    "setuptools>=82.0.0",
     ]
     docs = ["sphinx", "sphinx_rtd_theme"]
     
    
  • src/psd_tools/compression/__init__.py+92 10 modified
    @@ -64,6 +64,7 @@
     import array
     import io
     import logging
    +import warnings
     import zlib
     from typing import Iterator, Union
     
    @@ -85,6 +86,60 @@
     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,
    @@ -129,34 +184,61 @@ 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: bytes | None = 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:
                 mode = "L" if depth == 8 else "RGB" if depth == 24 else "RGBA"
                 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)
    -
    -    assert result is not None
    +            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
     
     
    
  • src/psd_tools/compression/rle.py+19 19 modified
    @@ -58,36 +58,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()
    -
    -    if length == 1:
    -        if data[0] != 128:
    -            raise ValueError("Invalid RLE compression")
    -        return result
    +    result = bytearray(size)  # pre-allocated and zero-filled
     
    -    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)
     
    
  • src/psd_tools/compression/_rle.pyx+27 21 modified
    @@ -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])
    
  • src/psd_tools/__init__.py+2 1 modified
    @@ -30,6 +30,7 @@
     """
     
     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__"]
    
  • tests/psd_tools/compression/test_compression.py+113 0 modified
    @@ -1,14 +1,18 @@
     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(
         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(
    @@ -99,3 +151,64 @@ def test_compress_decompress_fail(
         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
    +# ---------------------------------------------------------------------------
    +
    +
    +@pytest.mark.parametrize(
    +    "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
    +
    +
    +@pytest.mark.parametrize(
    +    "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)
    +
    +
    +@pytest.mark.parametrize(
    +    "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)
    
  • tests/psd_tools/compression/test_rle.py+20 14 modified
    @@ -29,20 +29,26 @@ def test_identical() -> None:
     
     
     @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: Any, data: bytes, size: int) -> None:
    -    with pytest.raises(ValueError):
    -        mod.decode(data, size)
    +def test_tolerant_decode(mod: Any, data: bytes, size: int, expected: bytes) -> 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
    
  • uv.lock+51 0 modified
    @@ -370,6 +370,44 @@ toml = [
         { name = "tomli", marker = "python_full_version <= '3.11'" },
     ]
     
    +[[package]]
    +name = "cython"
    +version = "3.2.4"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" },
    +    { url = "https://files.pythonhosted.org/packages/7d/3d/b26f29092c71c36e0462752885bdfb18c23c176af4de953fdae2772a8941/cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845", size = 3370134, upload-time = "2026-01-04T14:14:53.627Z" },
    +    { url = "https://files.pythonhosted.org/packages/56/9e/539fb0d09e4f5251b5b14f8daf77e71fee021527f1013791038234618b6b/cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02", size = 3537552, upload-time = "2026-01-04T14:14:56.852Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/c6/82d19a451c050d1be0f05b1a3302267463d391db548f013ee88b5348a8e9/cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86", size = 2766191, upload-time = "2026-01-04T14:14:58.709Z" },
    +    { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" },
    +    { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" },
    +    { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" },
    +    { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" },
    +    { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" },
    +    { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" },
    +    { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" },
    +    { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" },
    +    { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" },
    +    { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" },
    +    { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" },
    +    { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" },
    +    { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" },
    +    { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" },
    +    { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" },
    +    { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" },
    +    { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" },
    +    { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" },
    +    { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" },
    +    { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" },
    +    { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" },
    +    { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
    +]
    +
     [[package]]
     name = "debugpy"
     version = "1.8.17"
    @@ -1139,6 +1177,7 @@ composite = [
     dev = [
         { name = "aggdraw", version = "1.3.19", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' or sys_platform != 'win32'" },
         { name = "aggdraw", version = "1.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
    +    { name = "cython" },
         { name = "ipykernel" },
         { name = "mypy" },
         { name = "pytest" },
    @@ -1147,6 +1186,7 @@ dev = [
         { name = "scikit-image" },
         { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
         { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
    +    { name = "setuptools" },
     ]
     docs = [
         { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
    @@ -1177,13 +1217,15 @@ dev = [
         { name = "aggdraw", marker = "sys_platform != 'win32'", specifier = ">=1.3.16" },
         { name = "aggdraw", marker = "python_full_version < '3.11' and sys_platform == 'win32'", specifier = ">=1.3.16,<1.4.1" },
         { name = "aggdraw", marker = "python_full_version >= '3.11' and sys_platform == 'win32'", specifier = ">=1.4.1" },
    +    { name = "cython", specifier = ">=3.2.4" },
         { name = "ipykernel" },
         { name = "mypy" },
         { name = "pytest" },
         { name = "pytest-cov" },
         { name = "ruff" },
         { name = "scikit-image" },
         { name = "scipy" },
    +    { name = "setuptools", specifier = ">=82.0.0" },
     ]
     docs = [
         { name = "sphinx" },
    @@ -1607,6 +1649,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" },
     ]
     
    +[[package]]
    +name = "setuptools"
    +version = "82.0.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" },
    +]
    +
     [[package]]
     name = "six"
     version = "1.17.0"
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.