{"id":"CVE-2026-112395","kind":"vulnerability","aliases":["CVE-2026-112395","GHSA-462x-pgg6-55fp"],"related":[],"title":"Pillow tiled-TIFF decoder reads past tile buffer in copy4 (Unpack.c:1290)","description":"### 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.","summary":"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.","severity":"medium","cvss":6.5,"epss":0.0026400000788271427,"exploited":false,"exploited_at":null,"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"],"how_to_check":"Run pip show pillow and confirm the version is >= 8.2.0 and < 13.0.0.\nSearch the codebase for Image.open or PIL.Image usage on untrusted TIFF uploads.","fix":"Upgrade pillow to 13.0.0 or later.","status":"open","published_at":"2026-10-19T21:17:07+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:57:45+00:00","updated_at":"2026-10-23T21:57:19+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:57:45Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade pillow to 13.0.0 or later.\nunknown","summary":"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.","category":"package","who_cares":["python","pillow","image-processing"],"confidence":0.8,"analyst_note":"","how_to_check":"Run pip show pillow and confirm the version is >= 8.2.0 and < 13.0.0.\nSearch the codebase for Image.open or PIL.Image usage on untrusted TIFF uploads.\nunknown","needs_analyst":false,"not_affected_if":["no-untrusted-tiff-input","pillow-13-or-later"],"severity_if_unknown":"unknown"},"cost_usd":0.000538,"input_tokens":3855,"output_tokens":305,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'+255':646C '-1':182C,365C,483C '-112395':16A,24A '-122':27B '-1291':540C '-2026':15A,23A '-490':422C '0':113C,211C,454C,481C,507C,615C,675C '1':271C '1024':173C,205C,582C,630C,647C,648C,650C,673C '1288':539C '1290':13A '196608':215C,591C,607C,637C,654C,678C '2':293C '255':617C '256':578C,585C,594C,595C '260096':653C '3':302C,596C '4':561C,583C '446':421C '462x':19A '55fp':21A '63488':655C '7':355C '8':356C 'adjac':70B,242C 'advisori':67B 'alloc':92C,217C,659C 'alreadi':631C 'also':68B 'applic':227C 'asan':564C,660C 'asan-detect':563C 'ask':272C 'assum':404C 'attack':219C 'bit':353C 'bodi':398C 'broken':363C 'buffer':9A,48B,97C,101C,132C,192C,252C,296C,316C,403C,406C,428C,439C,478C,526C,605C,627C,645C 'byte':136C,143C,152C,174C,184C,187C,189C,197C,206C,212C,216C,279C,286C,300C,337C,348C,386C,392C,411C,415C,430C,431C,433C,530C,580C,589C,600C,608C,651C,656C,665C,676C,679C 'bytes-per-til':278C 'bytes/pixel':584C 'c':345C,419C,537C 'call':120C,312C,324C 'case':150C,380C 'caus':241C 'channel':599C 'cmyk':556C 'code':418C 'codec':362C 'configur':122C,326C 'const':550C 'convinc':222C 'copi':75B,161C,247C 'copy4':11A,126C,170C,329C,541C,547C 'craft':60B,231C 'crash':65B,237C,543C 'current':116C,484C,494C,510C,534C 'cve':14A,22A 'cwe':26B 'd':167C,604C 'data':425C,437C,441C 'decod':5A,39B,78B,85C,88C,229C,250C,257C 'decodetil':84C,267C,423C 'detail':253C 'detect':565C 'done':340C 'dos':238C 'end':44B 'errcod':360C 'exact':210C,298C 'expect':57B 'file':262C 'first':664C 'flag':661C 'function':270C 'ghsa':18A 'ghsa-462x-pgg6-55fp':17A 'heap':47B,71B,243C,668C 'highest':621C 'howev':399C 'i.e':378C 'im':517C,523C 'imag':29B,79B,91C,361C,518C 'image-process':28B 'imagingshuffl':327C,458C 'index':400C 'inner':157C 'int':553C 'int32':488C,498C 'iter':109C,611C 'last':640C 'least':409C 'length':118C,155C,181C,351C,395C,414C,451C,496C,500C,512C,577C 'less':389C 'libimag':83C 'libtiff':50B,139C,259C,273C,292C,369C,382C,685C 'lie':632C 'long':416C 'loop':162C,343C,397C 'make':62B 'may':73B,240C 'memcpi':175C,558C,643C 'memori':72B,244C 'metadata':572C 'min':487C,497C 'mismatch':682C 'new':424C,436C,440C 'offset':636C 'one':377C 'oob':532C 'origin':306C 'over-read':207C 'overs':368C 'packag':31B 'past':7A,42B,164C,213C,635C,657C,666C 'path':268C 'per':95C,280C,287C 'per-til':94C 'perform':171C 'pgg6':20A 'pillow':1A,25A,32B,34B,81C,225C,254C 'pillow-us':224C 'pixel':251C,554C,562C,586C 'pixels':524C 'plane':311C,354C,453C,455C,456C,457C,461C,482C 'poc':201C,569C 'pointer':130C,625C 'process':30B,64B,236C 'python':33B 'quadrupl':557C 'read':6A,41B,177C,209C,566C,649C,670C 'real':684C 'realloc':99C,166C,294C,426C,603C 'region':168C,669C,680C 'reject':367C,375C 'report':51B,260C 'return':140C,364C,383C,688C 'rgba':555C 'row':123C,135C,151C,183C,186C,288C,321C,323C,332C,391C,410C,529C,579C,622C,641C 'say':69B 'shuffler':459C,515C 'site':544C 'size':55B,137C,144C,153C,185C,188C,193C,198C,301C,349C,387C,393C,412C,434C,531C,581C,590C,672C 'smaller':53B,145C 'sourc':129C,336C,624C 'src/libimaging/tiffdecode.c':420C 'src/libimaging/unpack.c':538C 'start':652C 'state':100C,131C,295C,315C,352C,359C,427C,429C,438C,444C,447C,464C,467C,477C,491C,501C,525C,626C 'static':545C 'storag':601C 'store':290C 'strict':388C 'stride':159C,533C 'stride-and-copi':158C 'summari':80C 'suppli':200C 'take':265C 'tdata':475C 'three':598C 'three-channel-byt':597C 'tiff':4A,38B,61B,90C,103C,202C,232C,256C,276C,284C,474C 'tiffreadtil':106C,313C,473C 'tifftilerows':283C 'tifftiles':102C,275C,687C 'tile':3A,8A,37B,54B,89C,96C,110C,117C,133C,142C,154C,180C,196C,264C,281C,299C,305C,320C,347C,350C,357C,370C,385C,394C,413C,432C,450C,470C,485C,489C,495C,499C,505C,508C,511C,513C,519C,527C,535C,571C,574C,576C,588C,612C,628C 'tiled-tiff':2A,36B 'trigger':203C,234C 'uint8':516C,548C,551C 'unders':376C 'unpack':124C,460C,610C 'unpack.c':12A 'use':86C,98C,226C 'valid':339C 'void':546C 'vulner':417C 'walk':163C,318C 'width':358C,471C,486C,490C,536C,575C 'worst':149C 'worst-cas':148C 'worth':334C 'x':307C,463C,466C,469C,479C,493C,522C 'xoff':465C 'xsize':468C,492C 'y':111C,134C,308C,443C,446C,449C,480C,503C,506C,509C,514C,520C,521C,528C,613C,629C 'yield':573C 'yoff':445C 'ysize':448C,502C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T21:17:07+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:L","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-462x-pgg6-55fp","url":"https://github.com/advisories/GHSA-462x-pgg6-55fp","severity":"medium","published_at":"2026-10-20T20:49:26+00:00","modified_at":"2026-10-20T20:49:27+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-112395","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112395","severity":null,"published_at":"2026-10-19T21:17:07+00:00","modified_at":"2026-10-20T19:48:50+00:00","first_seen_at":"2026-10-23T21:57:19+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}