{"schema":"threatline.record/2","record":{"id":"CVE-2026-112395","aliases":["GHSA-462x-pgg6-55fp"],"summary":"Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)","details":"### Summary\n\nPillow'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.\n\n### Details\n\nPillow's TIFF decoder, when libtiff reports the file is tiled, takes the `_decodeTile` path. The function:\n\n1. Asks libtiff for `TIFFTileSize(tiff)` (the bytes-per-tile) and `TIFFTileRowSize(tiff)` (the bytes per row *as stored by libtiff*).\n2. Reallocates `state->buffer` to exactly `tile_bytes_size`.\n3. 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.\n\nThe validation done before the loop is:\n\n```c\nif (tile_bytes_size > ((tile_length * state->bits / planes + 7) / 8) * tile_width) {\n    state->errcode = IMAGING_CODEC_BROKEN;\n    return -1;\n}\n```\n\nThis 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.\n\n### Vulnerable Code\n\n```c\n// src/libImaging/TiffDecode.c:446-490 — _decodeTile\nnew_data = realloc(state->buffer, state->bytes);   // bytes = tile_bytes_size\nif (!new_data) { ... }\nstate->buffer = new_data;\n\nfor (y = state->yoff; y < state->ysize; y += tile_length) {\n    for (plane = 0; plane < planes; plane++) {\n        ImagingShuffler shuffler = unpackers[plane];\n        for (x = state->xoff; x < state->xsize; x += tile_width) {\n            if (TIFFReadTile(tiff, (tdata_t)state->buffer, x, y, 0, plane) == -1) { ... }\n\n            current_tile_width  = min((INT32)tile_width,  state->xsize - x);\n            current_tile_length = min((INT32)tile_length, state->ysize - y);\n            for (tile_y = 0; tile_y < current_tile_length; tile_y++) {\n                shuffler(\n                    (UINT8 *)im->image[tile_y + y] + x * im->pixelsize,\n                    state->buffer + tile_y * row_byte_size,   // <-- OOB stride\n                    current_tile_width\n                );\n            }\n        }\n    }\n}\n```\n\n```c\n// src/libImaging/Unpack.c:1288-1291 — copy4 (the crash site)\nstatic void\ncopy4(UINT8 *out, const UINT8 *in, int pixels) {\n    /* RGBA, CMYK quadruples */\n    memcpy(out, in, 4 * pixels);   // <-- ASAN-detected READ here\n}\n```\n\nThe 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`).\n\nThe 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`.\n\n\n## Attribution\n\nThis 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.","categories":["Libraries & packages"],"product":"pillow","vendor":"python-pillow","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-462x-pgg6-55fp","source_modified_at":"2026-10-20T20:49:27Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"pillow"},"range":">= 8.2.0, < 13.0.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-112395","source_modified_at":"2026-10-19T20:59:15.098Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"Pillow","vendor":"python-pillow","versions":[{"version":">= 8.2.0, < 13.0.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-462x-pgg6-55fp","source_modified_at":"2026-10-20T21:00:04.394527086Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"PyPI","name":"pillow","purl":"pkg:pypi/pillow"},"ranges":[{"events":[{"introduced":"8.2.0"},{"fixed":"13.0.0"}],"type":"ECOSYSTEM"}],"versions":["10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-462x-pgg6-55fp","source_modified_at":"2026-10-20T20:49:27Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"pillow"},"version":"13.0.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-462x-pgg6-55fp","source_modified_at":"2026-10-20T21:00:04.394527086Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"PyPI","name":"pillow","purl":"pkg:pypi/pillow"},"version":"13.0.0"}],"cvss":6.5,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00264,"percentile":0.17259,"score_date":"2026-10-23"},"published_at":"2026-10-19T20:59:15.098Z","modified_at":"2026-10-20T21:00:04.394527086Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-462x-pgg6-55fp","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-20T20:49:27Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-112395","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-19T20:59:15.098Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-462x-pgg6-55fp","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-20T21:00:04.394527086Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-112395","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-462x-pgg6-55fp","tags":["x_refsource_CONFIRM"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112395","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/python-pillow/Pillow/commit/76869efdbbf2ad7dccd7536520fb61b7a00ab61e","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-462x-pgg6-55fp","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"product","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-20T21:00:04.394527086Z","content_hash":"5d0f4573797d5e241d6537b7e35a814ff56ccf472419bc40a1ad6b9d1fe6fde1"}}
