python-pillow / python-pillow/Pillow

Exporting an image created by Image.fromarrow with __arrow_c_array__ crashes with a segmentation fault

Open
#9,896 0 comments 0 reactions 0 assignees View on GitHub

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.