diff --git a/docs/changelog.rst b/docs/changelog.rst index 9ae9cc7bc..83c87bcf9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -253,6 +253,8 @@ Detailed list of changes - Drag and drop protocol: deny requests for drag data made before the user has actually dropped something onto the window +- Text sizing protocol: Fix a buffer overflow when a natural width (no explicit ``w`` key) text sizing escape code contains a grapheme cluster longer than four codepoints + 0.48.2 [2026-07-30] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/screen.c b/kitty/screen.c index ca5368d1c..7b23f1635 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -1619,8 +1619,10 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const char_type ch = self->lc->chars[i]; CharProps cp = char_props_for(ch); if (cp.is_invalid) continue; - if ((s = grapheme_segmentation_step(s, cp)).add_to_current_cell || (wcwidth_std(cp) == 0 && lc.count)) lc.chars[lc.count++] = ch; - else { + if ((s = grapheme_segmentation_step(s, cp)).add_to_current_cell || (wcwidth_std(cp) == 0 && lc.count)) { + ensure_space_for_chars(&lc, lc.count + 1); + lc.chars[lc.count++] = ch; + } else { if (lc.count) handle_variable_width_multicell_command(self, mcd, &lc); switch (wcwidth_std(cp)) { case 0: diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 95c242b87..c9308f7e3 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -150,6 +150,22 @@ def test_multicell(self: TestMulticell) -> None: for x in range(1, 3): comb(x, y) comb(0, 1) + + # Regression test for buffer overflow when a natural width (w=0) grapheme + # cluster is longer than the ListOfChars stack buffer. See the accumulation + # loop in screen_handle_multicell_command(). The cluster is accumulated in + # full (overflowing the buffer before the fix) even though the text stored + # per cell is later capped at MAX_NUM_CODEPOINTS_PER_CELL (24). + capped = 'a' + '́' * 23 + s.reset() + multicell(s, 'a' + '́' * 32) + ac(0, 0, is_multicell=True, width=1, scale=1, x=0, y=0, text=capped, natural_width=True) + # A second, even longer cluster exercises the heap-backed buffer after the first flush. + s.reset() + multicell(s, 'a' + '́' * 32 + 'b' + '́' * 48) + ac(0, 0, is_multicell=True, x=0, y=0, text=capped, natural_width=True) + ac(1, 0, is_multicell=True, x=0, y=0, text='b' + '́' * 23, natural_width=True) + s.reset() multicell(s, 'aüa', scale=2) self.ae(s.cursor.x, 6)