| Version | Supported |
|---|---|
| 1.x | ✅ Yes |
Please report security vulnerabilities by opening a private GitHub Security Advisory at: https://github.com/CrossApplication/glyph_path/security/advisories/new
Do not open a public issue for security vulnerabilities.
| Package | Version | Status |
|---|---|---|
cacherine |
2.4.0 | ✅ No known advisories |
synchronized |
3.4.0+1 (transitive via cacherine) | ✅ No known advisories |
Dev dependencies (test, analyzer, etc.) are not included in published builds and are therefore not in scope for end-user security.
Advisories were verified manually against pub.dev. To re-verify at each dependency update, search for each package at osv.dev, check the package changelog on pub.dev, and run dart pub outdated to identify stale versions. Enable GitHub Dependabot on the repository for automatic advisory notifications.
This package parses binary font data (.ttf / .otf). Treat all font bytes as untrusted.
ResourceLimits defines hard static const caps enforced during font parsing and during
text-shaping API calls (generateGlyphPaths, measureText, resolveGlyphIdsForLayout,
TextLayout.layout). Exceeding a limit tied to the font's own binary structure raises
FontParseException (fatal tables) or silently disables the associated feature (non-fatal
tables: kern, GPOS, GSUB) — but this Fatal/Non-Fatal split describes parse-time
behavior only, i.e. what happens while Font.parse() reads the table itself. A small
number of limits are instead evaluated lazily, once per text-shaping call, well after
Font.parse() has already returned successfully; these always raise FontParseException
to the caller of that call regardless of whether the table they belong to is otherwise
classified Non-Fatal — see maxLigatureEvalOps below and
Exception model for the one currently affected case. Exceeding a limit
on the caller-supplied input itself (maxInputTextLength, maxFontSize) raises
ArgumentError instead, since these validate the public API contract rather than the font
data:
| Limit | Value | Scope |
|---|---|---|
maxTables |
64 | Font table directory |
maxGlyphs |
65535 | cmap, hmtx, glyf/CFF |
maxCompositeDepth |
8 | Composite glyph nesting |
maxCompositeComponents |
64 | Components per composite glyph |
maxContoursPerGlyph |
256 | TrueType glyph outlines |
maxPointsPerGlyph |
32768 | TrueType glyph outlines |
maxCmapIterations |
65536 | cmap format 4 segment expansion |
maxCmapGroups |
10000 | cmap format 12 group count |
maxCmapFormat12Span |
1114112 | cmap format 12 cumulative code-point span |
maxKernPairs |
65535 | Cumulative across all kern subtables |
maxGposPairs |
65535 | Cumulative across all GPOS PairPos subtables |
maxGposMatrixCells |
65535 | Per GPOS PairPos Format 2 subtable |
maxGposFeatureLookupRefs |
65535 | Cumulative kern lookup-index references across the GPOS FeatureList |
maxCoverageGlyphs |
65535 | GPOS Coverage Format 2 expansion |
maxClassDefEntries |
65535 | GPOS ClassDef Format 2 expansion |
maxLigaRules |
10000 | Cumulative GSUB ligature rules |
maxLigatureComponentCount |
16 | Components per GSUB ligature record |
maxLigatureEvalOps |
1000000 | GSUB ligature rule visits and component comparisons per text call |
maxCffSubrDepth |
10 | CFF Type 2 subroutine call nesting |
maxCffTotalSubrCalls |
10000 | Cumulative callsubr/callgsubr invocations per CFF glyph |
maxCffStackDepth |
513 | CFF Type 2 charstring operand stack, and CFF DICT operand stacks |
maxCffStemCount |
96 | CFF Type 2 cumulative hstem/vstem/hintmask pairs per glyph |
maxCffOutputCommands |
32768 | PathCommands emitted per CFF glyph interpretation |
maxCompositeOutputCommands |
65535 | PathCommands per composite glyph expansion level |
maxPathCommandsPerGlyph |
131072 | Cumulative PathCommands across all composite levels |
maxCoverageRangeRecords |
20000 | Per-call range-record scan in GSUB Coverage Format 2 |
maxGsubCoverageScanSteps |
1000000 | Cumulative GSUB ligature coverage scan cost per parse |
maxGsubLigSetVisits |
1000000 | Cumulative LigatureSet visits across all GSUB liga lookups/subtables per parse |
maxGsubSubtablesPerLookup |
64 | Subtables per GSUB Lookup table |
maxGsubFeatureLookupRefs |
65535 | Cumulative liga lookup-index references across the FeatureList |
maxInputTextLength |
4096 | UTF-16 code units per API call |
maxFontSize |
1e6 | fontSize value accepted by generateGlyphPaths, measureText, TextLayout.layout |
Every table parser receives a BinaryReader.slice() scoped to the table's declared byte
range. This makes cross-table reads structurally impossible — a truncated or malformed
table cannot bleed into adjacent table bytes. BinaryReader.slice() rejects negative
offset or length arguments with RangeError, preventing callers from accidentally
referencing memory before the start of a table slice.
head — minimum 54-byte length validated before any field read. The magicNumber
field (offset 8+4 = 12) is validated against 0x5F0F3CF5. The checkSumAdjustment
field (offset 8) is intentionally not validated: many freely distributed fonts and
subsetting-tool outputs contain incorrect checksum values, so rejecting on checksum
mismatch would break compatibility with fonts that render correctly in every major
browser and OS.
hhea — minimum 36-byte length validated; numberOfHMetrics checked against the
hmtx table size.
cmap — subtable offsets are validated against the cmap table's own declared size
(not the full font buffer), catching subtables that point into adjacent tables. Within
format 4, segCountX2 is validated to be even and length is checked against
16 + 8 × segCount before any array read. Format 12 rejects tables whose group count
exceeds maxCmapGroups before allocating any group data.
kern — at least 4 bytes are required before reading the table header; each
subtable requires at least 6 bytes; any subtable with length < 6 is rejected (prevents
backward seeks). The cumulative pair count is capped at maxKernPairs.
loca — every offset is validated to fall within the glyf table's byte range;
descending offsets (which would imply a negative glyph size) are rejected.
glyf — composite glyph nesting is checked against maxCompositeDepth and
maxCompositeComponents before recursion; cycle detection prevents infinite loops.
The cumulative PathCommand output per expansion level is capped at
maxCompositeOutputCommands, and the total across all recursive levels at
maxPathCommandsPerGlyph, preventing memory exhaustion from deeply nested composites.
Simple glyph flag repeat expansion is validated before allocation: if the declared
repeatCount would push the total flag count past numPoints, a FontParseException
is raised immediately rather than after the over-expanded list is built.
endPtsOfContours is validated to be strictly increasing immediately after it is
read, before numPoints (derived from its last entry) is used to size the
coordinate and flag arrays — a non-monotonic array would otherwise let the
contour-building loop index past those arrays.
GPOS — Coverage Format 2 range expansion is capped at maxCoverageGlyphs;
ClassDef Format 1 and Format 2 expansion are capped at maxClassDefEntries; PairPos
Format 2 matrix size (class1Count × class2Count) is checked against maxGposMatrixCells
before the nested loop executes. PairPos Format 1 counts every visited pair record
(not only those with a non-zero XAdvance) against maxGposPairs, so a crafted font
whose pairs all encode a zero XAdvance still trips the cumulative cap instead of
iterating unbounded. The FeatureList's kern-feature collection loop counts every
lookup-index reference read (featureCount × lookupIndexCount, both independent
uint16 fields) against maxGposFeatureLookupRefs, preventing a crafted font from
sharing one small FeatureTable offset across thousands of kern features to force
hundreds of millions of reads from a buffer of a few hundred kilobytes.
GSUB — ligature compCount values of 0 (spec violation) or greater than
maxLigatureComponentCount are rejected. Cumulative rule count is capped at maxLigaRules.
All limits described so far in this section are checked inside parseGsub() itself, so a
violation is caught and swallowed by the Non-Fatal boundary described under
Exception model below — Font.parse() succeeds and ligatures are
simply disabled.
maxLigatureEvalOps is different: it is not checked during parseGsub(). It is checked
inside Font._resolveGlyphIdsWithSource, which runs on every call to
generateGlyphPaths, generateRawGlyphPaths, measureText, resolveGlyphIdsForLayout,
and TextLayout.layout — i.e. after Font.parse() has already returned successfully, once
per text-shaping call. The total number of ligature rule visits and component comparisons
per call is capped at maxLigatureEvalOps — counted once per candidate rule considered
in addition to once per component compared, so a rule that never reaches the component
loop (e.g. a degenerate 0-component rule) cannot evade the budget — preventing CPU
exhaustion from fonts with large rule sets combined with long input strings. Because the
check fires mid-evaluation rather than while parseGsub() builds the table's data
structures, there is no Non-Fatal boundary to swallow it at: exceeding this limit raises
FontParseException directly to the caller of that text-shaping call, even though GSUB
itself is otherwise a Non-Fatal table. Applications that call these APIs with long,
caller-supplied strings against untrusted fonts should be prepared to catch
FontParseException from them, not only from Font.parse().
Lookup subtable counts are capped at maxGsubSubtablesPerLookup, and the cumulative
liga lookup-index references scanned across the FeatureList are capped at
maxGsubFeatureLookupRefs — both independent uint16 fields whose product would
otherwise be unbounded. Coverage Format 2 range-record scans are capped per call at
maxCoverageRangeRecords, and the cumulative scan cost across all LigatureSet
first-glyph lookups in a single parse is capped at maxGsubCoverageScanSteps,
preventing the ligSetCount × rangeCount combination from driving billions of
comparisons even though neither factor alone exceeds its uint16 range.
Independently of that per-visit coverage-scan cost, the cumulative number of
LigatureSet visits itself — across every subtable of every liga lookup in a
single parse — is capped at maxGsubLigSetVisits. Without this cap, a
Coverage Format 1 table (or a sparse Format 2 table) makes each visit O(1),
so maxGsubCoverageScanSteps never fires; a crafted font could still reuse
the same physical subtable and LigatureSet bytes across every offset entry to
declare hundreds of billions of visits (the product of the number of distinct
liga lookup indices, subtableCount, and ligSetCount — three
independently uint16-bounded fields) from a binary of only a few tens of
kilobytes.
CFF — _skipIndex validates that offSize is in [1, 4] before reading any INDEX
offsets. The charstring interpreter verifies that the byte position advances each
iteration (stalled iteration → FontParseException). Multi-byte number encodings
(byte 28 shortint, byte 255 fixed-point, bytes 247–254) are checked for sufficient
remaining bytes before reading. Subroutine call depth is capped at maxCffSubrDepth; total invocations per glyph at
maxCffTotalSubrCalls; charstring operand stack depth at maxCffStackDepth; cumulative
hint stem pairs at maxCffStemCount (bounding the hintmask/cntrmask mask-byte skip
distance); PathCommand output per glyph at maxCffOutputCommands. FDSelect Format 3
sentinel handling rejects malformed range boundaries. The Top DICT, Private DICT, and
Font DICT operand stacks (built while parsing DICT structures, independent of the
charstring interpreter) are also capped at maxCffStackDepth, and the Top DICT's escape
opcode (byte 12) is bounds-checked before reading its second byte. _readIndexEntries
rejects INDEX offset arrays that are not non-decreasing, rather than relying on
Uint8List.sublist to reject the resulting negative-length entry with a raw
RangeError. _readIndexEntries and _parsePrivateDict both validate that the
byte range they are about to read (derived from attacker-controlled DICT operands)
fits within the table buffer before calling Uint8List.sublist, raising
FontParseException directly at the point of failure rather than letting
Uint8List.sublist throw a raw RangeError that the caller would otherwise see
(there is no global handler downstream that would convert it). The Font DICT parser (used for CID-keyed FDArray entries) handles the
real-number operand (byte 30) the same way the Top DICT and Private DICT parsers do —
without it, a real number's continuation bytes could be misread as a premature
Private operator or a truncated multi-byte integer, corrupting the Private DICT
size/offset resolved for that Font DICT.
Two exception types cross the public API boundary, and they are deliberately not interchangeable:
FontParseException— the font's own binary data is structurally invalid: a missing or truncated table, an out-of-range offset, a cyclic composite glyph, or a resource limit tied to the font's structure being exceeded. Most such limits (e.g.maxCffSubrDepth) are checked by a Fatal-table parser (head,hhea,maxp,cmap,hmtx,glyf/loca,CFF) duringFont.parse(), which validates bounds before reading and raisesFontParseExceptiondirectly at the point of failure rather than depending on a global handler to catch aRangeErrorafter the fact — there is no such global handler for fatal tables. One limit,maxLigatureEvalOps, is checked later instead: during a text-shaping call (generateGlyphPaths,generateRawGlyphPaths,measureText,resolveGlyphIdsForLayout, orTextLayout.layout), not duringFont.parse()— see theGSUBentry under Per-table bounds checks above. It still raisesFontParseException, but to the caller of that later call rather than to the caller ofFont.parse(), even though GSUB itself is a Non-Fatal table for every other kind of failure. EveryFontParseExceptioncarries a descriptivemessageand an optionaloffsetpointing to the problematic byte position (offsetisnullfor themaxLigatureEvalOpscase, since it is not tied to a specific byte).ArgumentError— the caller violated the public API's own contract, independent of the font's contents:maxGlyphCacheSize/maxShapeCacheSize≤ 0 passed toFont.parse,textlonger thanmaxInputTextLength, orfontSizenon-finite, non-positive, or greater thanmaxFontSize. These are argument-validation failures on the API surface, not font-parsing failures, and are never converted toFontParseException.
Non-fatal table failures (kern, GPOS, GSUB, OS/2) are swallowed silently at the
point each table is parsed: both FontParseException and RangeError are caught locally
(the GPOS and GSUB parsers additionally catch internal ArgumentErrors raised while
parsing the table's own bytes, as defensive best-effort — unrelated to the public-API-boundary
ArgumentError described above, which is never caught or converted), the associated
feature is disabled, and parsing continues. Callers have no
direct notification when these tables are rejected; a malformed font will simply produce
output without kerning or ligature substitution.
The one exception is maxLigatureEvalOps (see above): because it fires during a
text-shaping call rather than while GSUB is parsed, it is not covered by this swallow —
FontParseException propagates out of that call normally.