Pillow: Invalid free in FreeType glyph rendering when a stroker is active
### 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
- Products
- pillow
- Affected versions
- 10.0.0, 10.0.1, 10.1.0, 10.2.0, 10.3.0, 10.4.0, 11.0.0, 11.1.0, 11.2.1, 11.3.0, 12.0.0, 12.1.0, 12.1.1, 12.2.0, 12.3.0
- Fixed versions
- 13.0.0
- CVEs
- CVE-2026-112893
- Confirmed fixed CVEs
- Not specified
- Updated in this feed
- 1 Nov 2026, 10:37 UTC · Revision 1
Source evidence
Pillow: Invalid free in FreeType glyph rendering when a stroker is active
core-osv · Original sourcePublished 21 Oct 2026, 20:06 UTC
### 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
osv:GHSA-j4m9-6w36-pw47
pillow
Fixed versions: 13.0.0
Complete source record
{
"id": "77f84cd446c2f1e253c7366a418ab566b8e16ce4d0266c5b277c7bbf94abb30e",
"url": "https://osv.dev/vulnerability/GHSA-j4m9-6w36-pw47",
"method": "structured",
"payload": {
"kind": "advisory",
"title": "Pillow: Invalid free in FreeType glyph rendering when a stroker is active",
"summary": "### 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",
"evidence": "osv:GHSA-j4m9-6w36-pw47",
"products": [
{
"name": {
"value": "pillow",
"evidence": "/affected/0/package/name"
},
"prerequisites": [],
"fixed_versions": [
{
"value": "13.0.0",
"evidence": "/affected/0/ranges"
}
],
"affected_versions": [
{
"value": "10.0.0",
"evidence": "/affected/0/versions"
},
{
"value": "10.0.1",
"evidence": "/affected/0/versions"
},
{
"value": "10.1.0",
"evidence": "/affected/0/versions"
},
{
"value": "10.2.0",
"evidence": "/affected/0/versions"
},
{
"value": "10.3.0",
"evidence": "/affected/0/versions"
},
{
"value": "10.4.0",
"evidence": "/affected/0/versions"
},
{
"value": "11.0.0",
"evidence": "/affected/0/versions"
},
{
"value": "11.1.0",
"evidence": "/affected/0/versions"
},
{
"value": "11.2.1",
"evidence": "/affected/0/versions"
},
{
"value": "11.3.0",
"evidence": "/affected/0/versions"
},
{
"value": "12.0.0",
"evidence": "/affected/0/versions"
},
{
"value": "12.1.0",
"evidence": "/affected/0/versions"
},
{
"value": "12.1.1",
"evidence": "/affected/0/versions"
},
{
"value": "12.2.0",
"evidence": "/affected/0/versions"
},
{
"value": "12.3.0",
"evidence": "/affected/0/versions"
}
]
}
],
"severity": "high",
"structured": {
"id": "GHSA-j4m9-6w36-pw47",
"aliases": [
"BIT-pillow-2026-112893",
"CVE-2026-112893",
"GHSA-j4m9-6w36-pw47"
],
"metrics": [
{
"type": "CVSS_V4",
"score": "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"
}
],
"affected": [
{
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "10.0.0"
},
{
"fixed": "13.0.0"
}
]
}
],
"package": {
"name": "pillow",
"purl": "pkg:pypi/pillow",
"ecosystem": "PyPI"
},
"versions": [
"10.0.0",
"10.0.1",
"10.1.0",
"10.2.0",
"10.3.0",
"10.4.0",
"11.0.0",
"11.1.0",
"11.2.1",
"11.3.0",
"12.0.0",
"12.1.0",
"12.1.1",
"12.2.0",
"12.3.0"
],
"database_specific": {
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-j4m9-6w36-pw47/GHSA-j4m9-6w36-pw47.json"
}
}
],
"modified": "2026-10-26T08:55:43.891551666Z",
"provider": "osv",
"references": [
"https://github.com/python-pillow/Pillow/security/advisories/GHSA-j4m9-6w36-pw47",
"https://nvd.nist.gov/vuln/detail/CVE-2026-112893",
"https://github.com/python-pillow/Pillow/commit/a7416de7f3a43128179d2cf68df3240ea8dafe13",
"https://github.com/python-pillow/Pillow",
"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"
],
"provider_metadata": {
"cwe_ids": [
"CWE-457",
"CWE-763"
],
"severity": "HIGH",
"github_reviewed": true,
"nvd_published_at": "2026-10-19T22:17:09Z",
"github_reviewed_at": "2026-10-21T20:06:34Z"
}
},
"related_cves": [
{
"value": "CVE-2026-112893",
"evidence": "/aliases"
}
],
"security_relevant": true,
"severity_evidence": "/database_specific/severity"
},
"source_id": "core-osv",
"document_id": 446804,
"published_at": "2026-10-21T20:06:34+00:00"
}