FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

getlist()/_putdata(): OOB read when __len__ overstates materialized items · Issue #9892 · python-pillow/Pillow · GitHub

getlist()/_putdata(): OOB read when __len__ overstates materialized items #9892

Description

What did you do?

I passed a custom sequence to Image.point() and Image.putdata() whose
__len__ reports more items than iteration actually materializes. This is
independent of the free-threading race in #9852 / #9853: it is deterministic,
single-threaded, and reproduces with the GIL enabled.

Image.point() reproducer:

from PIL import Image


class OverstatedLengthSequence:
    def __len__(self):
        return 256

    def __getitem__(self, index):
        if index >= 8:
            raise IndexError
        return float(index)


Image.new("L", (4, 4)).point(OverstatedLengthSequence(), "F")

Image.putdata() reproducer:

from PIL import Image


class OverstatedLengthSequence:
    def __len__(self):
        return 16

    def __getitem__(self, index):
        if index >= 2:
            raise IndexError
        return float(index + 1)


Image.new("L", (4, 4)).putdata(OverstatedLengthSequence())

What did you expect to happen?

Pillow should use the length of the sequence returned by PySequence_Fast():

  • fixed-size consumers such as Image.point() should raise their existing
    wrong-length error;
  • Image.putdata() should consume only the items that were actually
    materialized.

It should not index past the end of the materialized list.

What actually happened?

On current main at
4e5f09f533dbd0d87d39951f48b62f4d1a421cba, each script terminated with
SIGSEGV (exit 139) in 5/5 fresh processes on CPython 3.14.6 with the GIL
enabled.

The two C paths first obtain n from the caller-controlled __len__, then
materialize by iteration, but continue indexing to the earlier value of n:

/* getlist() */
n = PySequence_Size(arg);
seq = PySequence_Fast(arg, must_be_sequence);
for (i = 0; i < n; i++) {
    op = PySequence_Fast_GET_ITEM(seq, i);

/* _putdata() */
n = PyObject_Length(data);
seq = PySequence_Fast(data, must_be_sequence);
for (i = 0; i < n; i++) {
    op = PySequence_Fast_GET_ITEM(seq, i);

PySequence_Fast_GET_ITEM() is unchecked, so when n is 256 but seq
contains 8 items (or 16 versus 2), the loop reads beyond the materialized
list.

The same getlist() path is reachable through Image.transform() and
ImageFilter.Kernel. JPEG qtables are not included: that path normalizes
each table through array.array and list before the C code sees it.

I prepared an independent candidate at
41b675109723dcd0fab6f935518501992e0b26c6.
It materializes once, then derives n from PySequence_Fast_GET_SIZE(seq).
Against that exact commit:

  • the Image.point() reproducer raises ValueError: wrong number of lut entries;
  • the Image.putdata() reproducer writes (1, 2) followed by 14 zeroes;
  • the affected-file suite passes 332/332 on CPython 3.14.6 (GIL enabled);
  • the same suite passes 332/332 on CPython 3.14.0rc1t (GIL disabled).

This is reported as a regular robustness bug. The trigger is a Python object
supplied by the caller; I am not assigning a security severity.

What are your OS, Python and Pillow versions?

  • OS: macOS 26.6.1 (25G76), arm64
  • Python: CPython 3.14.6, GIL enabled
  • Pillow: 13.0.0.dev0, main at
    4e5f09f533dbd0d87d39951f48b62f4d1a421cba

(Issue text and candidate developed with AI assistance; reproductions and
measurements run by me.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL