@@ -19,6 +19,10 @@ namespace SixLabors.Fonts.Tables.Cff;
1919/// </remarks>
2020internal ref struct CffEvaluationEngine
2121{
22+ // Appendix B of both Type 2 and CFF2 CharString specifications limits nested local and global
23+ // subroutine calls to 10, which also provides a fixed stack bound for malformed cyclic programs.
24+ private const int MaxSubroutineNesting = 10 ;
25+
2226 private static readonly Random Random = new ( ) ;
2327 private float ? width ;
2428 private int nStems ;
@@ -108,7 +112,7 @@ public Bounds GetBounds()
108112 this . transforming = new ( finder , Vector2 . Zero , new Vector2 ( 1 , - 1 ) , Vector2 . Zero , Matrix3x2 . Identity ) ;
109113
110114 // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller.
111- this . Parse ( this . charStrings ) ;
115+ this . Parse ( this . charStrings , 0 ) ;
112116
113117 // Some CFF end without closing the latest contour.
114118 if ( this . transforming . IsOpen )
@@ -134,7 +138,7 @@ public void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vec
134138 this . transforming = new ( renderer , origin , scale , offset , transform ) ;
135139
136140 // Boolean IGlyphRenderer.BeginGlyph(..) is handled by the caller.
137- this . Parse ( this . charStrings ) ;
141+ this . Parse ( this . charStrings , 0 ) ;
138142
139143 // Some CFF end without closing the latest contour.
140144 if ( this . transforming . IsOpen )
@@ -147,7 +151,8 @@ public void RenderTo(IGlyphRenderer renderer, Vector2 origin, Vector2 scale, Vec
147151 /// Parses and interprets a Type 2 charstring byte buffer, executing operators and accumulating operands.
148152 /// </summary>
149153 /// <param name="buffer">The charstring byte data to parse.</param>
150- private void Parse ( ReadOnlySpan < byte > buffer )
154+ /// <param name="subroutineDepth">The number of active local and global subroutine calls.</param>
155+ private void Parse ( ReadOnlySpan < byte > buffer , int subroutineDepth )
151156 {
152157 SimpleBinaryReader reader = new ( buffer ) ;
153158 bool endCharEncountered = false ;
@@ -239,9 +244,11 @@ private void Parse(ReadOnlySpan<byte> buffer)
239244 index = ( int ) this . stack . Pop ( ) + this . localBias ;
240245 subr = this . localSubrBuffers [ index ] ;
241246
242- if ( subr . Length > 0 )
247+ // The over-limit call contributes no outline, matching how cyclic TrueType components
248+ // degrade to empty while allowing the enclosing charstring to continue normally.
249+ if ( subr . Length > 0 && subroutineDepth < MaxSubroutineNesting )
243250 {
244- this . Parse ( subr ) ;
251+ this . Parse ( subr , subroutineDepth + 1 ) ;
245252 }
246253
247254 break ;
@@ -452,9 +459,11 @@ private void Parse(ReadOnlySpan<byte> buffer)
452459 index = ( int ) this . stack . Pop ( ) + this . globalBias ;
453460 subr = this . globalSubrBuffers [ index ] ;
454461
455- if ( subr . Length > 0 )
462+ // Local and global subroutines share the same nesting stack and therefore the same
463+ // format limit and empty-outline fallback behavior.
464+ if ( subr . Length > 0 && subroutineDepth < MaxSubroutineNesting )
456465 {
457- this . Parse ( subr ) ;
466+ this . Parse ( subr , subroutineDepth + 1 ) ;
458467 }
459468
460469 break ;
0 commit comments