GHSA-462x-pgg6-55fp

Suggest an improvement
Source
https://github.com/advisories/GHSA-462x-pgg6-55fp
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-462x-pgg6-55fp/GHSA-462x-pgg6-55fp.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-462x-pgg6-55fp
Aliases
Published
2026-10-20T20:49:26Z
Modified
2026-10-20T21:00:04Z
Severity
  • 6.5 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L CVSS Calculator
Summary
Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)
Details

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:

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

// 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
                );
            }
        }
    }
}
// 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.

Database specific
{
    "cwe_ids":  [
        "CWE-122"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-20T20:49:26Z",
    "nvd_published_at":  "2026-10-19T21:17:07Z",
    "severity":  "MODERATE"
}
References

Affected packages

PyPI / pillow

Package

Affected ranges

Type
ECOSYSTEM
Events
Introduced
8.2.0
Fixed
13.0.0

Affected versions

8.*
8.2.0
8.3.0
8.3.1
8.3.2
8.4.0
9.*
9.0.0
9.0.1
9.1.0
9.1.1
9.2.0
9.3.0
9.4.0
9.5.0
10.*
10.0.0
10.0.1
10.1.0
10.2.0
10.3.0
10.4.0
11.*
11.0.0
11.1.0
11.2.1
11.3.0
12.*
12.0.0
12.1.0
12.1.1
12.2.0
12.3.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-462x-pgg6-55fp/GHSA-462x-pgg6-55fp.json"