# Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files)

McIdas AREA header words set a tiny row stride, so Pillow's mmap path builds row pointers 1 byte apart while reading xsize bytes, leaking adjacent process memory or crashing with SIGBUS.

- Payout: Research
- Severity: high
- Program: Pillow
- Found by: GameZoneHacker
- Disclosed: 2026-07-20
- CVE: CVE-2026-54058
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-62p4-gmf7-7g93
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-62p4-gmf7-7g93
- Tags: area:oss domain:oss impact:data-leak impact:dos kind:research platform:ghsa sev:high tech:python technique:source-review vuln:dos vuln:info-disclosure vuln:memory-corruption

**Target:** Pillow image decoder (McIdas AREA plugin, src/map.c mmap path)
**Root cause:** PyImaging_MapBuffer validates offset + ysize*stride <= buffer_len but never checks stride >= im->linesize (xsize*pixelsize), so row pointers spaced by attacker-controlled stride are read at full natural row width. McIdasImagePlugin derives stride/offset/size from unvalidated header words.
**Steps:**
1. Craft a 256-byte McIdas AREA header with prefix `\x00\x00\x00\x00\x00\x00\x00\x04` and set w[11]=1 (mode L), w[10]=xsize large (e.g. 200000), w[9]=1 (ysize), w[14]=0, w[15]=1 so `stride = w[15]+w[10]*w[11]*w[14] = 1`, and w[34] chosen so offset is small.
2. Save the file to disk and call `Image.open(path)` so use_mmap is true (filename set, single tile).
3. ImageFile.load mmap branch checks only `offset + ysize*stride <= map.size()` (2 <= 256) and calls `core.map_buffer(map, (xsize,1), 'raw', offset, ('L',1,1))`.
4. PyImaging_MapBuffer sets `im->image[0] = view.buf + offset` with linesize = xsize = 200000, no stride>=linesize check.
5. Call `img.tobytes()` (or getpixel/convert/save); the raw 'L' encoder reads 200000 bytes from a 256-byte mapping, leaking adjacent memory or raising SIGBUS.
**How found:** Code-reading of the mmap fast path: noticed map.c validates offset+ysize*stride against buffer length but never reconciles stride with the natural row width im->linesize, then traced which plugins feed attacker-controlled stride/offset/size into that path and found McIdas derives all of them from raw header words.
**Exploitation:** Attacker supplies a crafted McIdas AREA file to any app that saves uploads to disk and opens them by path with default plugins, then decodes/thumbnails/serves pixels. The decoded image bytes contain adjacent worker-process memory (secrets, other users' data) which the app returns or stores; a larger xsize reliably crashes the worker with SIGBUS.
**Escalation:** Leaked heap/mapped memory served back through the app's normal image output path turns a decoder bug into cross-tenant data disclosure; oversized xsize converts it to reliable worker DoS.
**Insight:** When a decoder hands a stride to a shared mmap/buffer helper, check whether the helper validates stride against the mode's natural linesize, not just total buffer length; format plugins that compute stride from header fields (McIdas w[14]/w[15]) are prime sources of a stride smaller than row width.

