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
This commit is contained in:
Kovid Goyal 2026-08-23 07:52:17 +05:30
parent 5f04193347
commit 7d2fbaadcc
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 22 additions and 2 deletions

View file

@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

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

View file

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