Skip to content

Composite glyphs referencing a shared component subtree cause unbounded CPU cost in glyf/gvar outlining (exponential in branching factor, not bounded by MAX_COMPONENTS) #220

Description

@scadastrangelove

glyf::outline_impl's MAX_COMPONENTS = 32 bounds the depth of a composite-glyph chain, but
nothing bounds how many times a shared component-glyph subtree gets re-visited via sibling
components at the same level. A composite glyph whose components all reference one shared
child glyph — rather than MAX_COMPONENTS distinct ones — forces branching^depth calls to
outline_impl while the depth cap is never reached, and needs only depth + 1 distinct glyph
records (not branching^depth of them) to construct.

The identical pattern exists in gvar::outline_var_impl (used for variable-font glyph
outlining), which has the same depth >= glyf::MAX_COMPONENTS guard and no visit-count bound
either.

How bad is this, measured against real data

Rather than assert a severity, I measured it against a real 6253-glyph font (DejaVu Sans, 759 KB,
genuine composite glyphs for accented Latin/Cyrillic/Greek) outlining every glyph: 8.66ms
total, worst single real glyph 1.23ms.
Against that baseline, the 920-byte crafted glyph below
(16.8s) is ~13,600× slower than the single slowest glyph in a large, real, widely-deployed
font
— for less than 2× the byte count of that glyph. This is a genuine, large disproportion
between crafted-input cost and legitimate-input cost, not just "technically unbounded in theory."

Reproduction (ttf-parser 0.25.1, and current main @ 6e75b3c)

A chain of depth composite glyphs, each with branching components that all reference the
SAME single next-level glyph, terminating in one real leaf glyph:

depth branching font size outline_glyph time (before fix)
10 2 536 B 55 µs
15 2 686 B 2.2 ms
18 3 920 B 16.8 s
20 3 996 B > 20 s (did not complete)

Each timing was measured as an isolated OS process (to safely bound the run) on current main
@ 6e75b3c. Self-contained reproducer available (builds the sfnt/head/hhea/maxp/loca/glyf
tables byte-by-byte; ~150 lines).

Fix

PR incoming: adds a total component-visit budget (100,000 — real composite glyphs have at most a
few hundred components, so this leaves generous headroom while bounding worst-case cost to a
small constant regardless of the crafted branching factor), threaded through both
glyf::outline_impl and gvar::outline_var_impl. Happy to adjust the exact threshold if you'd
rather pick a different number — this part is a policy call, the mechanism (a total-visit
counter, independent of depth) is the part I'd consider load-bearing.


Discovered by the rust-in-peace security pipeline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions