# Pillow: TIFF encoder ColorMap plane-size mismatch causes a heap out-of-bounds read

Pillow's TIFF encoder allocates a fixed 768-entry ColorMap but libtiff reads 65536 entries per plane at 16-bit depth, causing a heap out-of-bounds read on save.

- Payout: Research
- Severity: high
- Program: Pillow
- Found by: Tomer-PL
- Disclosed: 2026-10-20
- CVE: CVE-2026-112896
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-pchr-8j38-3x4p
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pchr-8j38-3x4p
- 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:** TIFF encoder (PyImaging_LibTiffEncoderNew / libtiff TIFFTAG_COLORMAP)
**Root cause:** Pillow's encoder requires exactly 768 ColorMap entries and allocates 1536 bytes with a fixed 256-entry stride, but libtiff computes per-plane count as 1<<BitsPerSample (65536 for 16-bit) and memcpy's that many SHORTs from each plane pointer.
**Steps:**
1. Craft a 1x1, 16-bit grayscale, PackBits-compressed TIFF with tag 320 (ColorMap) containing 768 SHORT values (three zero-filled 256-entry planes).
2. Open and load it: `Image.open(BytesIO(data)); image.load()` — loads as `I;16` without error.
3. Save back to TIFF retaining metadata: `image.save(BytesIO(), format="TIFF")`.
4. Encoder allocates 1536 bytes and passes three plane pointers with 256-entry stride to libtiff.
5. libtiff's `TIFFVSetField` for TIFFTAG_COLORMAP does `_TIFFsetShortArrayExt` copying 1<<16 SHORTs per plane, reading 131072 bytes past the 1536-byte allocation — ASAN heap-buffer-overflow READ.
**How found:** Noticed the encoder's ColorMap validation enforces a fixed 768-entry count independent of BitsPerSample, then compared against libtiff's tif_dir.c which sizes the copy by 1<<td_bitspersample; built a 16-bit grayscale TIFF with a 768-entry ColorMap and confirmed the mismatch on save.
**Exploitation:** Requires an application that accepts attacker TIFF input, retains ColorMap metadata, and re-saves to TIFF via the libtiff-backed encoder. Observed effect is an OOB read and ASAN termination; process-memory exposure during encoding is potential but not demonstrated.
**Insight:** When a library forwards caller-supplied metadata to a native API, check whether the native side derives buffer sizes from other metadata fields (here BitsPerSample) rather than the fixed count the wrapper validates — a valid-looking 768-entry ColorMap becomes an OOB read once sample depth is 16-bit.

