Skip to content

font: avoid panic on malformed composite glyphs - #269

Open
ChrisJr404 wants to merge 1 commit into
go-text:mainfrom
ChrisJr404:fix/glyf-composite-phantom-panic
Open

font: avoid panic on malformed composite glyphs#269
ChrisJr404 wants to merge 1 commit into
go-text:mainfrom
ChrisJr404:fix/glyf-composite-phantom-panic

Conversation

@ChrisJr404

Copy link
Copy Markdown

Problem

Face.GlyphData / Face.GlyphDataOutline panic with slice bounds out of range [:-4] on certain malformed TrueType fonts.

getPointsForGlyph is documented to always return the four phantom points, so glyphDataFromGlyf slices them off unconditionally:

points := f.getPointsForGlyph(glyph)
segments := buildSegments(points[:len(points)-phantomCount])

That invariant does not actually hold for composite glyphs. In getPointsForGlyphRec, when a component resolves to fewer than phantomCount points (e.g. it references an out-of-range glyph index, or nesting exceeds maxCompositeNesting), the function returns early before appending the phantom points:

LC := len(compPoints)
if LC < phantomCount { // in case of max depth reached
    delete(currentGlyphs, item.GlyphIndex)
    return
}

So a composite glyph whose (first) component is unresolvable makes getPointsForGlyph return 0–3 points, and points[:len(points)-phantomCount] slices with a negative bound and panics. Any caller passing untrusted font data to font.ParseTTF and then reading glyph outlines can be crashed by a crafted font.

getGlyfPoints (reached from GlyphExtents on variable fonts) has the same unchecked assumption at allPoints[len(allPoints)-phantomCount:].

Fix

Guard both consumers: if getPointsForGlyph returns fewer than phantomCount points, treat the glyph as empty instead of slicing out of range. This matches the existing behaviour for glyphs with no outline (a blank glyph), and leaves well-formed fonts completely unaffected.

Test

Added TestGlyphDataMalformedComposite, which builds a composite glyph referencing an out-of-range component and checks that the glyph-data accessors return an empty outline rather than panicking. It panics without the fix and passes with it. Found by fuzzing ParseTTF + glyph-outline extraction; the original crashing font and a hand-built minimal reproducer both go through the same code path.

@whereswaldon

Copy link
Copy Markdown
Member

@benoitkugler Is it better to guard the consumers, or to alter getPointsForGlyph to always return four points, even if they are zeroes? I don't love making all callers perform a length-checking ceremony, but maybe that is more desirable that making up phantom points? I really don't know.

@benoitkugler

Copy link
Copy Markdown
Contributor

@ChrisJr404 Thank you for the bug report !

@benoitkugler Is it better to guard the consumers, or to alter getPointsForGlyph to always return four points, even if they are zeroes? I don't love making all callers perform a length-checking ceremony, but maybe that is more desirable that making up phantom points? I really don't know.

I indeed think it would be a bit cleaner to actually enforce the phantom points creation. It will only penalize cases which happen barely in practice, and keep the code simpler.

getPointsForGlyph is assumed to always return at least the phantom
points, so glyphDataFromGlyf and getGlyfPoints slice them off with
points[:len(points)-phantomCount]. That assumption breaks for a
composite glyph whose component cannot be resolved (an out-of-range
glyph index, or nesting beyond maxCompositeNesting): the recursion
returned before appending the phantom points, so the glyph yielded
fewer than phantomCount points and the slice bound went negative,
panicking with 'slice bounds out of range [:-4]'.

Enforce the invariant in the producer instead of the callers: the
early-return path in getPointsForGlyphRec now contributes the phantom
points too, so a missing or too-deeply-nested glyph yields an
empty-but-valid outline. With the guarantee upheld, the redundant
length checks at the call sites and the internal component check are
removed. Well-formed fonts are unaffected.
@ChrisJr404
ChrisJr404 force-pushed the fix/glyf-composite-phantom-panic branch from 4ff0642 to 7233e78 Compare August 18, 2026 19:32
@ChrisJr404

Copy link
Copy Markdown
Author

Thanks, that makes sense to me too. I've reworked it so the guarantee lives in getPointsForGlyphRec itself: the early-return path (missing glyph id, or nesting past maxCompositeNesting) now appends the phantom points before returning, so getPointsForGlyph always yields at least phantomCount points and a malformed composite just produces an empty-but-valid outline. With the invariant enforced by the producer I dropped the two caller-side length checks and the redundant internal one, which keeps the code simpler as you noted. Regression test and the font/shaping/harfbuzz suites still pass.

@benoitkugler benoitkugler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great to me, thank you for this nice fix !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants