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.)
What did you do?
I passed a custom sequence to
Image.point()andImage.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:Image.putdata()reproducer:What did you expect to happen?
Pillow should use the length of the sequence returned by
PySequence_Fast():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
mainat4e5f09f533dbd0d87d39951f48b62f4d1a421cba, each script terminated withSIGSEGV(exit 139) in 5/5 fresh processes on CPython 3.14.6 with the GIL enabled.The two C paths first obtain
nfrom the caller-controlled__len__, then materialize by iteration, but continue indexing to the earlier value ofn:PySequence_Fast_GET_ITEM()is unchecked, so whennis 256 butseqcontains 8 items (or 16 versus 2), the loop reads beyond the materialized list.The same
getlist()path is reachable throughImage.transform()andImageFilter.Kernel. JPEG qtables are not included: that path normalizes each table througharray.arrayandlistbefore the C code sees it.I prepared an independent candidate at
41b675109723dcd0fab6f935518501992e0b26c6.It materializes once, then derives
nfromPySequence_Fast_GET_SIZE(seq).Against that exact commit:
Image.point()reproducer raisesValueError: wrong number of lut entries;Image.putdata()reproducer writes(1, 2)followed by 14 zeroes;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?
mainat4e5f09f533dbd0d87d39951f48b62f4d1a421cba(Issue text and candidate developed with AI assistance; reproductions and measurements run by me.)