From 7233e78d59ec65e6086d729892787c19cd714afa Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:12:32 -0400 Subject: [PATCH] font: avoid panic on malformed composite glyphs 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. --- font/glyphs.go | 14 +++++++++----- font/renderer_test.go | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/font/glyphs.go b/font/glyphs.go index 29afd394..7564af80 100644 --- a/font/glyphs.go +++ b/font/glyphs.go @@ -55,11 +55,17 @@ const maxCompositeNesting = 20 // protect against malicious fonts // use the `glyf` table to fetch the contour points, // applying variation if needed. -// for composite, recursively calls itself; allPoints includes phantom points and will be at least of length 4 +// for composite, recursively calls itself; allPoints includes phantom points and is +// always at least of length phantomCount, even for a missing, too-deeply-nested or +// otherwise malformed glyph (in which case an empty-but-valid outline is produced). func (f *Face) getPointsForGlyphRec(gid tables.GlyphID, currentDepth int, currentGlyphs glyphSet, allPoints *[]contourPoint /* OUT */) { // adapted from harfbuzz/src/OT/glyf/Glyph.hh if currentDepth > maxCompositeNesting || int(gid) >= len(f.glyf) { + // The glyph is missing or too deeply nested; still contribute the + // phantom points so the invariant documented above holds and callers + // never see a slice shorter than phantomCount. + *allPoints = append(*allPoints, make([]contourPoint, phantomCount)...) return } @@ -104,11 +110,9 @@ func (f *Face) getPointsForGlyphRec(gid tables.GlyphID, currentDepth int, curren f.getPointsForGlyphRec(item.GlyphIndex, currentDepth+1, currentGlyphs, &compPoints) + // getPointsForGlyphRec guarantees at least the phantom points, so a + // component always contributes a well-formed slice here. LC := len(compPoints) - if LC < phantomCount { // in case of max depth reached - delete(currentGlyphs, item.GlyphIndex) - return - } /* Copy phantom points from component if USE_MY_METRICS flag set */ if item.HasUseMyMetrics() { diff --git a/font/renderer_test.go b/font/renderer_test.go index 1a685a56..603c8ef6 100644 --- a/font/renderer_test.go +++ b/font/renderer_test.go @@ -643,3 +643,25 @@ func TestColorGlyphs(t *testing.T) { _, ok = face.GlyphDataColor(0) tu.Assert(t, ok) } + +// A composite glyph whose component references an out-of-range (or otherwise +// unresolvable) glyph produces fewer contour points than the phantom points +// appended for a well-formed glyph. The glyph data accessors must treat this +// as an empty glyph rather than panicking with a negative slice bound. +func TestGlyphDataMalformedComposite(t *testing.T) { + f := &Font{ + glyf: tables.Glyf{ + // glyph 0: composite referencing the out-of-range component 5 + {Data: tables.CompositeGlyph{Glyphs: []tables.CompositeGlyphPart{{GlyphIndex: 5}}}}, + }, + } + face := Face{Font: f} + + // must not panic, and yields an empty outline + out, _ := face.GlyphDataOutline(0) + tu.Assert(t, len(out.Segments) == 0) + + if gd, ok := face.GlyphData(0).(GlyphOutline); ok { + tu.Assert(t, len(gd.Segments) == 0) + } +}