render: blank glyphs grow the glyph slot map and shape cache without bound #406

Closed
opened 2026-09-25 02:07:44 +09:00 by natsukium · 0 comments
Owner

Summary

Two client-side glyph caches grow without bound on blank glyphs, which PTY output controls.

  • GlyphIndex::slots (crates/felis-render-wgpu/src/glyphs.rs) caches a blank rasterization as None. A None entry takes no atlas space, so it never triggers reset_atlas_full, and that reset is the only thing that clears the map.
  • ShapeCache (crates/felis-shaping/src/lib.rs, get_or_insert_sized) evicts once bytes_used exceeds its cap. A blank bitmap adds 0 bytes, so blank entries never count toward the cap.

A key is (char, cell_height, SizingKey), and a producer chooses both the char and the OSC 66 sizing. The OSC 66 ranges alone give 7 × 8 × 16 × 16 × 3 × 3 ≈ 1.3 × 10^5 sizings per char, before the four font styles. Multiplied by the chars that rasterize blank (the Unicode space characters, U+2800, and anything the font stack renders empty), this leaves on the order of 10^7 distinct keys. Each one becomes resident for the life of the client after being displayed once.

This contradicts docs/explanation/security-model.md "Resource exhaustion": "every unbounded quantity is capped where it is owned".

This finding comes from reading the source. It has not been reproduced or measured.

Found by

Review of the foldhash change to the same maps (#397). Seeding the hasher randomly there addresses collision cost, but not size.

Direction (not decided)

  • Stop caching blank results per sizing: key the blank check on the char alone, or recompute blanks. Or
  • Count each entry, blank or not, against a cap and evict, as shaped_runs already does with SHAPED_RUN_CACHE_MAX.

Either way, a test should pin the bound: display N distinct blank keys and assert that the map size stays under the cap.

## Summary Two client-side glyph caches grow without bound on blank glyphs, which PTY output controls. - `GlyphIndex::slots` (`crates/felis-render-wgpu/src/glyphs.rs`) caches a blank rasterization as `None`. A `None` entry takes no atlas space, so it never triggers `reset_atlas_full`, and that reset is the only thing that clears the map. - `ShapeCache` (`crates/felis-shaping/src/lib.rs`, `get_or_insert_sized`) evicts once `bytes_used` exceeds its cap. A blank bitmap adds 0 bytes, so blank entries never count toward the cap. A key is `(char, cell_height, SizingKey)`, and a producer chooses both the char and the OSC 66 sizing. The OSC 66 ranges alone give 7 × 8 × 16 × 16 × 3 × 3 ≈ 1.3 × 10^5 sizings per char, before the four font styles. Multiplied by the chars that rasterize blank (the Unicode space characters, U+2800, and anything the font stack renders empty), this leaves on the order of 10^7 distinct keys. Each one becomes resident for the life of the client after being displayed once. This contradicts `docs/explanation/security-model.md` "Resource exhaustion": "every unbounded quantity is capped where it is owned". This finding comes from reading the source. It has not been reproduced or measured. ## Found by Review of the foldhash change to the same maps (#397). Seeding the hasher randomly there addresses collision cost, but not size. ## Direction (not decided) - Stop caching blank results per sizing: key the blank check on the char alone, or recompute blanks. Or - Count each entry, blank or not, against a cap and evict, as `shaped_runs` already does with `SHAPED_RUN_CACHE_MAX`. Either way, a test should pin the bound: display N distinct blank keys and assert that the map size stays under the cap.
Commenting is not possible because the repository is archived.
No description provided.