Skip to content

Oversized JPEG 2000 box length leaks OverflowError from Image.open() #9817

Description

@nevercodecorrect

What did you do?

Open a malformed JP2 file whose first box declares an extended 64-bit length (0xffffffffff000004) that exceeds the platform's signed seek range (sys.maxsize on 64-bit). The issue is identified via fuzzing and manually reproduced to verify.

What did you expect to happen?

Pillow should reject the impossible box geometry through its normal corrupt-image path. A box that is too small (lbox < hlen) already raises SyntaxError("Invalid header length"), which Image.open() normalizes into UnidentifiedImageError. A box that is impossibly large should also be rejected the same way.

What actually happened?

Jpeg2KImagePlugin._parse_jp2_header() builds a top-level BoxReader with no known stream length. next_box_type() reads the extended 64-bit length, and because the reader has no length bound, _can_read(lbox - hlen) returns True and the huge value is stored in remaining_in_box. The parser does not recognize the box type and iterates; on the next next_box_type() call, the stored length is passed straight to fp.seek(...).
Because it exceeds C ssize_t, BytesIO.seek() raises OverflowError and it escapes to the caller:

Traceback (most recent call last):
  File "repro.py", line 6, in <module>
    Image.open(io.BytesIO(data))
  File ".../PIL/Image.py", line 3687, in open
    im = _open_core(
  File ".../PIL/Image.py", line 3667, in _open_core
    im = factory(fp, filename)
  File ".../PIL/ImageFile.py", line 152, in __init__
    self._open()
  File ".../PIL/Jpeg2KImagePlugin.py", line 281, in _open
    header = _parse_jp2_header(self.fp)
  File ".../PIL/Jpeg2KImagePlugin.py", line 166, in _parse_jp2_header
    tbox = reader.next_box_type()
  File ".../PIL/Jpeg2KImagePlugin.py", line 86, in next_box_type
    self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
OverflowError: Python int too large to convert to C ssize_t

The relevant code in BoxReader.next_box_type() is:

def next_box_type(self) -> bytes:
    # Skip the rest of the box if it has not been read
    if self.remaining_in_box > 0:
        self.fp.seek(self.remaining_in_box, os.SEEK_CUR)   # <-- OverflowError here
    self.remaining_in_box = -1

    lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s"))
    if lbox == 1:
        lbox = cast(int, self.read_fields(">Q")[0])
        hlen = 16
    else:
        hlen = 8

    if lbox < hlen or not self._can_read(lbox - hlen):
        msg = "Invalid header length"
        raise SyntaxError(msg)

    self.remaining_in_box = lbox - hlen
    return tbox

The _can_read() guard does not catch this for the top-level reader, because that reader is created with no length (has_length is False and remaining_in_box is -1 at the check), so _can_read() takes its return True # No length known, just read branch and accepts the oversized box.

These JP2 bytes, therefore, raise an unexpected OverflowError which becomes an uncaught exception. The failure is in the pure-Python side, so I assume it isnot a memory-safety vulnerability.

This is the JPEG 2000 counterpart of #9811

What are your OS, Python and Pillow versions?

Reproduced on the current main branch (commit 9e282f5) and confirmed on the
latest release 12.3.0.

  • OS: Ubuntu 22.04.5 LTS, x86_64
  • Python: 3.14.4
  • Pillow: 13.0.0.dev0 (git main)
--------------------------------------------------------------------
Pillow 13.0.0.dev0
Python 3.14.4 (main, Jun 12 2026, 00:08:58) [GCC 11.4.0]
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 13.0.0.dev0
--- JPEG support ok, compiled for libjpeg-turbo 2.1.2
--- OPENJPEG (JPEG2000) support ok, loaded 2.4.0
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.11
--- LIBTIFF support ok, loaded 4.3.0
--------------------------------------------------------------------
import base64, io
from PIL import Image
data = base64.b64decode("AAAADGpQICANCocKAAAAAQAAAAD//////wAABA==")
Image.open(io.BytesIO(data))   # OverflowError: Python int too large to convert to C ssize_t

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions