From 6dc6d1cc54f5e7518468bf6017492c1efe13c01d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 9 Sep 2026 09:21:38 +0530 Subject: [PATCH] Fix buffer overflow in the shaped run cache key fill_key() sizes its scratch buffer assuming every cell holds at most MAX_NUM_CODEPOINTS_PER_CELL codepoints. The draw paths in screen.c enforce that, but Line.add_combining_char() does not, so a line built via the Python API with more codepoints in a cell overflows the key buffer once the run is long enough. Bail out of caching such runs instead. --- kitty/shaped-run-cache.c | 3 +++ kitty_tests/fonts.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/kitty/shaped-run-cache.c b/kitty/shaped-run-cache.c index b3257c693..36af87ef4 100644 --- a/kitty/shaped-run-cache.c +++ b/kitty/shaped-run-cache.c @@ -130,6 +130,9 @@ fill_key( for (; num_cells; cells++, num_cells--) { if (cells->is_multicell && cells->x) continue; text_in_cell(cells, tc, &lc); + // the scratch buffer is sized assuming this limit, cells that exceed it + // (only possible via the Python API) are simply not cached + if (lc.count > MAX_NUM_CODEPOINTS_PER_CELL) return false; uint16_t advance = 1; if (cells->is_multicell) advance = (uint16_t)(cells->width * cells->scale); uint32_t hdr = ((uint32_t)advance << 16) | (uint32_t)lc.count; diff --git a/kitty_tests/fonts.py b/kitty_tests/fonts.py index c6bb6f089..f0f46e4c9 100644 --- a/kitty_tests/fonts.py +++ b/kitty_tests/fonts.py @@ -14,12 +14,14 @@ from kitty.constants import is_macos, read_kitty_resource from kitty.fast_data_types import ( DECAWM, ParsedFontFeature, + Screen, get_fallback_font, set_allow_use_of_box_fonts, sprite_idx_to_pos, sprite_map_set_layout, sprite_map_set_limits, test_render_line, + test_shape, test_sprite_position_increment, wcwidth, ) @@ -488,6 +490,37 @@ class Rendering(FontBaseTest): self.ae(groups('i\u0332\u0308', font='LiberationMono-Regular.ttf'), [(1, 2)]) self.ae(groups('u\u0332 u\u0332\u0301', font='LiberationMono-Regular.ttf'), [(1, 2), (1, 1), (1, 2)]) + def test_shaped_run_cache(self): + # test_shape() already checks that a cached run matches the HarfBuzz + # output for the same run, here we check that different runs sharing a + # font, and therefore a cache, do not collide with each other + ctx = partial(setup_for_testing, size=self.font_size, dpi=self.dpi, main_face_path=self.path_for_font(self.font_name)) + + def shape(text): + s = Screen(None, 1, max(8, len(text) * 2)) + s.draw(text) + return test_shape(s.line(0), None) + + texts = ('abcd', 'a===b', '->', '<==>', '-' * 18, 'x' * 32, 'x' * 33, 'a