@@ -47,6 +47,18 @@ pub const Font = struct {
4747 /// Limit cached glyph textures to avoid unbounded GPU/heap growth.
4848 const MAX_GLYPH_CACHE_ENTRIES : usize = 4096 ;
4949
50+ /// Maximum byte length for a single glyph string to prevent abuse from
51+ /// malicious or malformed terminal output. 256 bytes allows for reasonable
52+ /// grapheme clusters including emoji sequences and combining characters
53+ /// while protecting against memory exhaustion attacks.
54+ const MAX_GLYPH_BYTE_LENGTH : usize = 256 ;
55+
56+ /// Maximum codepoints in a single grapheme cluster before chunking.
57+ /// Chosen to balance rendering performance with memory usage. Values above
58+ /// this threshold are split into smaller segments to avoid creating
59+ /// excessively large textures (e.g., cursor trail effects with 120+ chars).
60+ const MAX_CLUSTER_SIZE : usize = 32 ;
61+
5062 const CacheEntry = struct {
5163 texture : * c.SDL_Texture ,
5264 seq : u64 ,
@@ -197,6 +209,22 @@ pub const Font = struct {
197209 if (codepoints .len == 0 ) return ;
198210 if (codepoints .len == 1 and codepoints [0 ] == 0 ) return ;
199211
212+ if (codepoints .len > MAX_CLUSTER_SIZE ) {
213+ const chars_per_chunk = MAX_CLUSTER_SIZE ;
214+ const cell_width = @max (1 , @divTrunc (target_width , @as (c_int , @intCast (codepoints .len ))));
215+
216+ var offset : usize = 0 ;
217+ while (offset < codepoints .len ) {
218+ const chunk_end = @min (offset + chars_per_chunk , codepoints .len );
219+ const chunk = codepoints [offset .. chunk_end ];
220+ const chunk_x = x + @as (c_int , @intCast (offset )) * cell_width ;
221+ const chunk_width = @as (c_int , @intCast (chunk .len )) * cell_width ;
222+ try self .renderCluster (chunk , chunk_x , y , chunk_width , target_height , fg_color , variant );
223+ offset = chunk_end ;
224+ }
225+ return ;
226+ }
227+
200228 const effective_variant = self .effectiveVariant (variant , codepoints );
201229
202230 var total_bytes : usize = 0 ;
@@ -265,6 +293,22 @@ pub const Font = struct {
265293 if (codepoints .len == 0 ) return ;
266294 if (codepoints .len == 1 and codepoints [0 ] == 0 ) return ;
267295
296+ if (codepoints .len > MAX_CLUSTER_SIZE ) {
297+ const chars_per_chunk = MAX_CLUSTER_SIZE ;
298+ const cell_width = @max (1 , @divTrunc (target_width , @as (c_int , @intCast (codepoints .len ))));
299+
300+ var offset : usize = 0 ;
301+ while (offset < codepoints .len ) {
302+ const chunk_end = @min (offset + chars_per_chunk , codepoints .len );
303+ const chunk = codepoints [offset .. chunk_end ];
304+ const chunk_x = x + @as (c_int , @intCast (offset )) * cell_width ;
305+ const chunk_width = @as (c_int , @intCast (chunk .len )) * cell_width ;
306+ try self .renderClusterFill (chunk , chunk_x , y , chunk_width , target_height , fg_color , variant );
307+ offset = chunk_end ;
308+ }
309+ return ;
310+ }
311+
268312 const effective_variant = self .effectiveVariant (variant , codepoints );
269313
270314 var total_bytes : usize = 0 ;
@@ -350,6 +394,11 @@ pub const Font = struct {
350394 }
351395
352396 fn getGlyphTexture (self : * Font , utf8 : []const u8 , fg_color : c.SDL_Color , fallback : Fallback , variant : Variant ) RenderGlyphError ! * c.SDL_Texture {
397+ if (utf8 .len > MAX_GLYPH_BYTE_LENGTH ) {
398+ log .warn ("Refusing to render excessively long glyph string: {d} bytes" , .{utf8 .len });
399+ return error .GlyphRenderFailed ;
400+ }
401+
353402 const key = GlyphKey {
354403 .hash = std .hash .Wyhash .hash (0 , utf8 ),
355404 .color = packColor (if (fallback == .emoji ) WHITE else fg_color ),
0 commit comments