Skip to content
Heist/ feed
Back to feed
HighFix available

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

Type: advisorySources: OSV

### 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 source

Published 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"
}