@@ -886,6 +886,7 @@ impl<'a> Table<'a> {
886886 let mut recursion_stack = RecursionStack {
887887 stack : [ 0 ; 64 ] ,
888888 len : 0 ,
889+ visits_left : MAX_PAINT_VISITS ,
889890 } ;
890891
891892 self . paint_impl (
@@ -1004,6 +1005,10 @@ impl<'a> Table<'a> {
10041005 return None ;
10051006 }
10061007
1008+ // Total-visit budget exceeded (a DAG can revisit the same offset via different sibling
1009+ // branches without ever cycling on the active path -- see `RecursionStack::visits_left`).
1010+ recursion_stack. consume_visit ( ) . ok ( ) ?;
1011+
10071012 recursion_stack. push ( offset) . ok ( ) ?;
10081013 let result = self . parse_paint_impl (
10091014 offset,
@@ -1836,8 +1841,21 @@ struct RecursionStack {
18361841 // The limit of 64 is chosen arbitrarily and not from the spec. But we have to stop somewhere...
18371842 stack : [ usize ; 64 ] ,
18381843 len : usize ,
1844+ // `stack`/`contains` only detect a cycle on the CURRENT root-to-leaf path (an entry is popped
1845+ // as soon as its call returns), so a paint graph shaped as a DAG -- the same offset reachable
1846+ // through multiple sibling branches, none of which individually cycles -- is not caught by it.
1847+ // A chain of `PaintColrLayers` records where every layer slot at each level points at one
1848+ // shared next-level record forces `branching^depth` calls while never re-entering the active
1849+ // path. This budget bounds the total number of `parse_paint` calls for one top-level `paint`
1850+ // call, independent of the DAG's branching factor.
1851+ visits_left : u32 ,
18391852}
18401853
1854+ // The limit of 100_000 total paint-graph node visits is chosen arbitrarily, mirroring
1855+ // `glyf::MAX_COMPONENT_VISITS` -- real color-font paint graphs are expected to have at most a few
1856+ // hundred nodes.
1857+ const MAX_PAINT_VISITS : u32 = 100_000 ;
1858+
18411859impl RecursionStack {
18421860 #[ inline]
18431861 pub fn is_empty ( & self ) -> bool {
@@ -1869,6 +1887,17 @@ impl RecursionStack {
18691887 debug_assert ! ( !self . is_empty( ) ) ;
18701888 self . len -= 1 ;
18711889 }
1890+
1891+ #[ inline]
1892+ pub fn consume_visit ( & mut self ) -> Result < ( ) , ( ) > {
1893+ match self . visits_left . checked_sub ( 1 ) {
1894+ Some ( left) => {
1895+ self . visits_left = left;
1896+ Ok ( ( ) )
1897+ }
1898+ None => Err ( ( ) ) ,
1899+ }
1900+ }
18721901}
18731902
18741903#[ cfg( feature = "variable-fonts" ) ]
0 commit comments