# Pillow: TiffDecode returns uninitialized memory as pixels for short JPEG strips

Pillow's TiffDecode reallocs an uninitialized strip/tile buffer and copies all RowsPerStrip rows even when libtiff's JPEG/G4 codec decoded fewer, leaking freed heap memory as pixels.

- Payout: Research
- Severity: high
- Program: Pillow
- Found by: carlini
- Disclosed: 2026-10-20
- CVE: CVE-2026-112897
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-q45j-58j2-45rg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-q45j-58j2-45rg
- Tags: area:oss domain:oss impact:data-leak impact:pii-leak kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:memory-corruption

**Target:** libImaging/TiffDecode.c _decodeStrip/_decodeTile via libtiff JPEG and Group 4 codecs
**Root cause:** state->buffer is realloc()ed to TIFFStripSize()/TIFFTileSize() without initialization, and TIFFReadEncodedStrip/TIFFReadTile return success (not -1) when a codec decodes fewer rows than the strip, so the untouched remainder is copied into the image.
**Steps:**
1. Build a little-endian 256x256 8-bit greyscale TIFF with Compression=7 (JPEG), RowsPerStrip=256, whose single strip is a baseline JPEG of 256x8 pixels (`Image.new("L",(256,8),200).save(...,"JPEG")`).
2. Open with `Image.open(BytesIO(data)).load()`; it succeeds with no error (compressed TIFFs take the libtiff path).
3. Free buffers containing `b"LEAK"` just before load() to seed the heap.
4. Read `im.tobytes()`: rows 0-7 are 200, rows 8-255 contain 15872 copies of `LEAK` (or all 0x55 under `MALLOC_PERTURB_=170`).
5. Confirm library side: `TIFFReadEncodedStrip(t,0,buf,65536)` warns `Improper JPEG strip/tile size, expected 256x256, got 256x8`, returns 65536, leaves 63488 bytes untouched; Pillow silences warnings via `TIFFSetWarningHandler(NULL)`.
6. Same pattern with Compression=4 Group 4 fax: a 512x16 T.6 stream in a 512x256 strip returns 16384 with only 17 rows written.
**How found:** Code review of _decodeStrip/_decodeTile: noticed realloc without init and that the only check is `== -1`, while libtiff's JPEG codec only warns on short strips and returns full strip size. Verified by feeding a short-JPEG strip directly to libtiff and observing untouched buffer bytes, then built a 478-byte TIFF and seeded freed heap with LEAK to prove disclosure.
**Exploitation:** Attacker uploads a small crafted TIFF to any app that opens images with Pillow in default config and returns pixel-derived output (thumbnail, preview, conversion, OCR). Each strip discloses up to width x RowsPerStrip x SamplesPerPixel bytes of freed heap (recent request bodies, other users' decoded buffers, Python objects), tens of MB per image within MAX_IMAGE_PIXELS, with no error signal.
**Insight:** When a decoder wrapper trusts a library's return value as 'bytes produced' rather than 'rows decoded', short-input codecs that only warn (JPEG SOF height mismatch, premature EOL in G4) leave the tail of a reused realloc buffer uninitialized; seed freed chunks with a marker and check MALLOC_PERTURB_ fill bytes to prove it.

