python-pillow / python-pillow/Pillow
Exporting an image created by Image.fromarrow with __arrow_c_array__ crashes with a segmentation fault
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 13.8k
- Forks
- 2.5k
- Avg merge
- 2d 8h
- Merged PRs (30d)
- 89
Description
What did you do?
Created an image with Image.fromarrow and passed it to pyarrow.array.
from PIL import Image
import pyarrow as pa
im = Image.fromarrow(pa.array([0] * 100, type=pa.uint8()), "RGBA", (5, 5))
im.getpixel((0, 0)) # fine
pa.array(im) # segmentation fault
The same happens with a single band image ("L" from 25 values), with im.__arrow_c_array__() called directly, and on Pillow 11.3.0, 12.3.0 and current main (098de8a6e). After any change to the image (im.putpixel((0, 0), (1, 2, 3, 4)), which copies the storage) the export works.
What did you expect to happen?
An Arrow array with the same values, as for any other image, or a ValueError if a borrowed image cannot be exported. The docstring of fromarrow shows both directions (Image.fromarrow(...) and pa.array(im)), so a round trip is the documented usage.
What actually happened?
The process dies with SIGSEGV (exit code 139), nothing is printed.
Why
ImagingBorrowArrow (src/libImaging/Storage.c) fills only the row pointers im->image[y] of the borrowed image and leaves im->block and im->blocks NULL.
export_single_channel_array and export_fixed_pixel_array (src/libImaging/Arrow.c) take the data pointer from im->block, or from im->blocks[0].ptr when block is NULL:
if (im->block) {
array->buffers[1] = im->block;
} else {
array->buffers[1] = im->blocks[0].ptr;
}
For a borrowed image blocks is NULL, so this dereferences NULL. The im->blocks_count > 1 check above it does not catch this case, blocks_count is 0.
Tests/test_pyarrow.py compares fromarrow images with getpixel, so the round trip is not covered.
Possible fix
A borrowed array is accepted only when its length matches the image exactly, so its rows are contiguous and the export can use the first row. Adding a branch at both places:
if (im->block) {
array->buffers[1] = im->block;
} else if (im->arrow_array_capsule) {
array->buffers[1] = im->image[0];
} else {
array->buffers[1] = im->blocks[0].ptr;
}
With this change (built from main) the example returns fixed_size_list<RGBA: uint8 not null>[4] with the original values, and the "L" case returns the 25 values.
Context
pillow-heif is moving its plugin to Image.fromarrow, so every HEIF image opened with Image.open would be a borrowed image, and pa.array(im) on such an image would crash. We are holding that release until this is fixed in Pillow.
What are your OS, Python and Pillow versions?
- OS: macOS 26.5 arm64
- Python: 3.14.5
- Pillow: 11.3.0, 12.3.0, main (098de8a6e)
- pyarrow: 25.0.1
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The crash path is in src/libImaging/Storage.c and src/libImaging/Arrow.c; start by reading ImagingBorrowArrow and the two export functions. Add regression coverage in Tests/test_pyarrow.py for RGBA and single-band fromarrow images, then verify pa.array(im) preserves values without crashing or raises ValueError.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, python
- Domain
- computer-vision
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100