---
id: "CVE-2026-112395"
kind: "vulnerability"
aliases: ["CVE-2026-112395", "GHSA-462x-pgg6-55fp"]
title: "Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)"
severity: "medium"
cvss: 6.5
epss: 0.0026400000788271427
exploited: false
status: "open"
category: "package"
products: [{"name": "pillow", "fixed": ["13.0.0"], "affected": ">= 8.2.0, < 13.0.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-122", "image-processing", "package", "pillow", "python"]
not_affected_if: ["no-untrusted-tiff-input", "pillow-13-or-later"]
published_at: "2026-10-19T21:17:07+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:57:19+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-462x-pgg6-55fp", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112395", "first_seen_at": "2026-10-23T21:57:19+00:00"}]
---

# Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)

Pillow's tiled-TIFF decoder can read past the end of a heap buffer when libtiff reports a smaller tile size than expected, so a crafted TIFF makes the process crash. The advisory also says adjacent heap memory may be copied into the decoded image.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:pillow affected >= 8.2.0, < 13.0.0 fixed 13.0.0

## How to check

Run pip show pillow and confirm the version is >= 8.2.0 and < 13.0.0.
Search the codebase for Image.open or PIL.Image usage on untrusted TIFF uploads.

## Fix

Upgrade pillow to 13.0.0 or later.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-462x-pgg6-55fp
- 2026-10-23T21:57:19+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112395

## From the sources

### Summary

Pillow's libImaging `_decodeTile` decoder, used to decode tiled TIFF images, allocates a per-tile buffer using `realloc(state->buffer, TIFFTileSize(tiff))`. After each `TIFFReadTile` it then iterates `tile_y` from 0 up to `current_tile_length` and calls the configured row unpacker (here `copy4`) with the source pointer `state->buffer + tile_y * row_byte_size`. When libtiff returns a `tile_bytes_size` smaller than the worst-case `row_byte_size * tile_length`, the inner stride-and-copy loop walks past the realloc'd region and `copy4` performs a 1024-byte `memcpy` that reads up to `(tile_length-1) * row_byte_size + row_byte_size` bytes from a buffer sized only to `tile_bytes_size`. The supplied PoC TIFF triggers a 1024-byte over-read exactly 0 bytes past a 196608-byte allocation. An attacker who can convince a Pillow-using application to decode a crafted TIFF can trigger a process crash (DoS) and may cause adjacent heap memory to be copied into the decoded pixel buffer.

### Details

Pillow's TIFF decoder, when libtiff reports the file is tiled, takes the `_decodeTile` path. The function:

1. Asks libtiff for `TIFFTileSize(tiff)` (the bytes-per-tile) and `TIFFTileRowSize(tiff)` (the bytes per row *as stored by libtiff*).
2. Reallocates `state->buffer` to exactly `tile_bytes_size`.
3. For each tile origin `(x, y)` and each plane, calls `TIFFReadTile` into `state->buffer`, then walks the tile row by row, calling the configured `ImagingShuffler` (here `copy4`) on each row's worth of source bytes.

The validation done before the loop is:

```c
if (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) {
    state->errcode = IMAGING_CODEC_BROKEN;
    return -1;
}
```

This rejects oversized libtiff tiles, but it does **not** reject *undersized* ones — i.e. the case where libtiff returns a `tile_bytes_size` strictly **less** than `row_byte_size * tile_length`. The loop body, however, indexes into the buffer assuming the buffer is at least `row_byte_size * tile_length` bytes long.

### Vulnerable Code

```c
// src/libImaging/TiffDecode.c:446-490 — _decodeTile
new_data = realloc(state->buffer, state->bytes);   // bytes = tile_bytes_size
if (!new_data) { ... }
state->buffer = new_data;

for (y = state->yoff; y < state->ysize; y += tile_length) {
    for (plane = 0; plane < planes; plane++) {
        ImagingShuffler shuffler = unpackers[plane];
        for (x = state->xoff; x < state->xsize; x += tile_width) {
            if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { ... }

            current_tile_width  = min((INT32)tile_width,  state->xsize - x);
            current_tile_length = min((INT32)tile_length, state->ysize - y);
            for (tile_y = 0; tile_y < current_tile_length; tile_y++) {
                shuffler(
                    (UINT8 *)im->image[tile_y + y] + x * im->pixelsize,
                    state->buffer + tile_y * row_byte_size,   // <-- OOB stride
                    current_tile_width
                );
            }
        }
    }
}
```

```c
// src/libImaging/Unpack.c:1288-1291 — copy4 (the crash site)
static void
copy4(UINT8 *out, const UINT8 *in, int pixels) {
    /* RGBA, CMYK quadruples */
    memcpy(out, in, 4 * pixels);   // <-- ASAN-detected READ here
}
```

The PoC's tile metadata yields `tile_width = tile_length = 256`, `row_byte_size = 1024` (4 bytes/pixel × 256 pixels), and `tile_bytes_size = 196608` (which is 256·256·3 = three-channel-byte storage). The realloc'd buffer is 196608 bytes. The unpacker iterates `tile_y` from 0 to 255 and for the highest rows the source pointer `state->buffer + tile_y * 1024` already lies at or past offset 196608 — the very last row's `memcpy(in=buffer+255·1024, 1024)` reads 1024 bytes starting `260096 - 196608 = 63488` bytes past the allocation; ASAN flags the very first byte past the heap region (`READ of size 1024 at … 0 bytes after 196608-byte region`).

The mismatch is real: libtiff's `TIFFTileSize` returns the *packed* per-tile storage (which depends on the file's photometric/separation layout — it can legitimately be smaller than `row_byte_size × tile_length`), while the inner loop strides as if the buffer always equals `row_byte_size × tile_length`.


## Attribution

This issue was found by Anthropic from using Claude to study security of open source projects, and I am from Ada Logics helping validate the found issues and report to maintainers. The data in this report has been reviewed manually.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
