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.
This commit is contained in:
Kovid Goyal 2026-09-09 09:21:38 +05:30
parent fd6008cd99
commit 6dc6d1cc54
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
2 changed files with 36 additions and 0 deletions

View file

@ -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;

View file

@ -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<!--b', '\u00e1\u00e9', 'a>\u2060<b')
cold = {}
for text in texts:
with ctx(): # a fresh font, and therefore an empty cache, for every run
cold[text] = shape(text)
with ctx(): # all runs now share a single cache
for text in texts + tuple(reversed(texts)):
self.ae(cold[text], shape(text), f'cached shaping differs for: {text!r}')
def test_shaping_with_many_combining_chars(self):
# cells can hold more codepoints than MAX_NUM_CODEPOINTS_PER_CELL when
# built via the Python API, which must not overflow the shaped run cache key
s = Screen(None, 1, 64)
s.draw('a' * 32)
line = s.line(0)
for x in range(32):
for i in range(40):
line.add_combining_char(x, chr(0x300 + (i % 16)))
self.ae(sum(g[0] for g in test_shape(line, None)), 32)
def test_emoji_presentation(self):
s = self.create_screen()
s.draw('\u2716\u2716\ufe0f')