Skip to content

Commit c140e23

Browse files
authored
Merge pull request #571 from treeform/fix/opentype-composite-recursion
Limit OpenType composite glyph recursion
2 parents 7f2bebd + eaac21d commit c140e23

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

src/pixie/fontformats/opentype.nim

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,8 @@ type
394394
when defined(release):
395395
{.push checks: off.}
396396

397+
const maxCompositeGlyphRecursion = 64
398+
397399
template eofCheck(buf: string, readTo: int) =
398400
if readTo > buf.len:
399401
raise newException(PixieError, "Unexpected error reading font data, EOF")
@@ -2180,7 +2182,7 @@ proc hasGlyph*(opentype: OpenType, rune: Rune): bool =
21802182
rune in opentype.cmap.runeToGlyphId
21812183

21822184
proc parseGlyfGlyph(
2183-
opentype: OpenType, glyphId: uint16
2185+
opentype: OpenType, glyphId: uint16, recursionDepth = 0
21842186
): Path {.raises: [PixieError], gcsafe.}
21852187

21862188
proc parseGlyphPath(
@@ -2318,7 +2320,9 @@ proc parseGlyphPath(
23182320

23192321
result.closePath()
23202322

2321-
proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
2323+
proc parseCompositeGlyph(
2324+
opentype: OpenType, offset, recursionDepth: int
2325+
): Path =
23222326
result = newPath()
23232327

23242328
var
@@ -2402,7 +2406,10 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
24022406
# elif (flags and 0b1000000000000) != 0: # UNSCALED_COMPONENT_OFFSET
24032407
# discard
24042408

2405-
var subPath = opentype.parseGlyfGlyph(component.glyphId)
2409+
var subPath = opentype.parseGlyfGlyph(
2410+
component.glyphId,
2411+
recursionDepth + 1
2412+
)
24062413
subPath.transform(mat3(
24072414
component.xScale, component.scale10, 0.0,
24082415
component.scale01, component.yScale, 0.0,
@@ -2413,7 +2420,11 @@ proc parseCompositeGlyph(opentype: OpenType, offset: int): Path =
24132420

24142421
moreComponents = (flags and 0b100000) != 0
24152422

2416-
proc parseGlyfGlyph(opentype: OpenType, glyphId: uint16): Path =
2423+
proc parseGlyfGlyph(
2424+
opentype: OpenType, glyphId: uint16, recursionDepth: int
2425+
): Path =
2426+
if recursionDepth > maxCompositeGlyphRecursion:
2427+
raise newException(PixieError, "Invalid composite glyph recursion")
24172428

24182429
if glyphId.int >= opentype.glyf.offsets.len:
24192430
raise newException(PixieError, "Invalid glyph ID " & $glyphId)
@@ -2434,7 +2445,7 @@ proc parseGlyfGlyph(opentype: OpenType, glyphId: uint16): Path =
24342445
i += 10
24352446

24362447
if numberOfContours < 0:
2437-
opentype.parseCompositeGlyph(i)
2448+
opentype.parseCompositeGlyph(i, recursionDepth)
24382449
else:
24392450
parseGlyphPath(opentype.buf, i, numberOfContours)
24402451

tests/test_fonts.nim

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os, pixie, strformat, unicode, xrays
1+
import os, pixie, pixie/fontformats/opentype, strformat, tables, unicode, xrays
22

33
proc wh(image: Image): Vec2 =
44
## Return with and height as a size vector.
@@ -1048,6 +1048,52 @@ block:
10481048
var typeface = readTypeface("tests/fonts/Roboto-Regular_1.ttf")
10491049
doAssert typeface.getKerningAdjustment('T'.Rune, 'e'.Rune) == -99.0
10501050

1051+
block:
1052+
proc writeBe16(data: var string, offset, value: int) =
1053+
data[offset] = char((value shr 8) and 0xff)
1054+
data[offset + 1] = char(value and 0xff)
1055+
1056+
let originalData = readFile("tests/fonts/Roboto-Regular_1.ttf")
1057+
let original = parseOpenType(originalData)
1058+
1059+
var
1060+
targetRune: Rune
1061+
targetGlyphId = -1
1062+
glyphOffset = -1
1063+
1064+
for rune, glyphId in original.cmap.runeToGlyphId.pairs:
1065+
let glyphIndex = glyphId.int
1066+
if glyphIndex == 0 or glyphIndex + 1 >= original.glyf.offsets.len:
1067+
continue
1068+
1069+
let
1070+
startOffset = original.glyf.offsets[glyphIndex].int
1071+
endOffset = original.glyf.offsets[glyphIndex + 1].int
1072+
glyphLen = endOffset - startOffset
1073+
1074+
if glyphLen >= 16:
1075+
targetRune = rune
1076+
targetGlyphId = glyphIndex
1077+
glyphOffset = startOffset
1078+
break
1079+
1080+
doAssert targetGlyphId >= 0
1081+
1082+
var mutated = originalData
1083+
writeBe16(mutated, glyphOffset + 0, 0xffff) # Composite glyph.
1084+
writeBe16(mutated, glyphOffset + 2, 0)
1085+
writeBe16(mutated, glyphOffset + 4, 0)
1086+
writeBe16(mutated, glyphOffset + 6, 0)
1087+
writeBe16(mutated, glyphOffset + 8, 0)
1088+
writeBe16(mutated, glyphOffset + 10, 0x0002) # ARGS_ARE_XY_VALUES.
1089+
writeBe16(mutated, glyphOffset + 12, targetGlyphId)
1090+
mutated[glyphOffset + 14] = '\0'
1091+
mutated[glyphOffset + 15] = '\0'
1092+
1093+
let font = parseOpenType(mutated)
1094+
doAssertRaises PixieError:
1095+
discard font.getGlyphPath(targetRune)
1096+
10511097
block:
10521098
var font = readFont("tests/fonts/Inter-Regular.ttf")
10531099
font.size = 26

0 commit comments

Comments
 (0)