fix(statusline): prevent various buffer overflows with item groups #40219

Problem:

Various out-of-bounds writes inherited from vim (examples assume MAXPATHL==4096):
- truncated item groups where minwid>maxwid:
    nvim --clean +"set stl=%<%{%repeat('x',4096-11)%}%50.5(12🙂345%)"
  leads to fillchars spilling over the end of the group/buffer while trying to
  compensate for truncating at a multicell character because minwid<=maxwid is assumed
- left-aligned item groups with multi-byte fillchar:
    nvim --clean +"set fillchars+=stl:∙ stl=%<%{%repeat('x',4096-3)%}%-2(X%)"
  wrongly leads to padding at the end of the statusline and `out_p-out==4097`
  because the bounds check assumes a 1-byte fillchar
- right-aligned item groups with 1-byte fillchar:
    nvim --clean +"set stl=%<%{%repeat('x',4096-4)%}%4(XY%)"
  leads to "YX" instead of "XY" at the end of the statusline
  because `memmove` is done before adjusting the offset
- right-aligned item groups with multi-byte fillchar:
    nvim --clean +"set fillchars+=stl:∙ stl=%5(X%)"
  leads to "∙∙∙∙<e2>", i.e. the fillchar is being written over the group contents
  and eventually being overwritten itself at the second byte with the final NUL,
  because the padding counter assumes a 1-byte fillchar; to crash Neovim,
    nvim --clean +"set fillchars+=stl:∙ stl=%<%{%repeat('x',4096-149)%}%50(X%)"

Solution:
Clearer variable names and no recycling of variables for different purposes.

(cherry picked from commit e36659c82f)
This commit is contained in:
Sébastien Hoffmann
2026-06-13 19:57:01 +02:00
committed by github-actions[bot]
parent c1318ac348
commit 83d9b9afca
2 changed files with 56 additions and 15 deletions

View File

@@ -1119,16 +1119,19 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
}
}
int minwid = stl_items[stl_groupitems[groupdepth]].minwid;
// If the group is longer than it is allowed to be truncate by removing
// bytes from the start of the group text. Don't truncate when item is a
// 'statuscolumn' fold item to ensure correctness of the mouse clicks.
if (group_len > stl_items[stl_groupitems[groupdepth]].maxwid
&& stl_items[stl_groupitems[groupdepth]].type != HighlightFold) {
// { Determine the number of bytes to remove
int maxwid = stl_items[stl_groupitems[groupdepth]].maxwid;
// Find the first character that should be included.
int n = 0;
while (group_len >= stl_items[stl_groupitems[groupdepth]].maxwid) {
while (group_len >= maxwid) {
group_len -= ptr2cells(t + n);
n += utfc_ptr2len(t + n);
}
@@ -1141,7 +1144,8 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
memmove(t + 1, t + n, (size_t)(out_p - (t + n)));
out_p = out_p - n + 1;
// Fill up space left over by half a double-wide char.
while (++group_len < stl_items[stl_groupitems[groupdepth]].minwid) {
minwid = MIN(minwid, maxwid);
while (++group_len < minwid) {
schar_get_adv(&out_p, fillchar);
}
// }
@@ -1157,33 +1161,36 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op
stl_items[idx].start = MAX(stl_items[idx].start, t);
}
// If the group is shorter than the minimum width, add padding characters.
} else if (abs(stl_items[stl_groupitems[groupdepth]].minwid) > group_len) {
ptrdiff_t min_group_width = stl_items[stl_groupitems[groupdepth]].minwid;
} else if (abs(minwid) > group_len) {
ptrdiff_t fillchar_bytes = (ptrdiff_t)schar_len(fillchar);
// If the group is left-aligned, add characters to the right.
if (min_group_width < 0) {
min_group_width = 0 - min_group_width;
while (group_len++ < min_group_width && out_p < out_end_p) {
if (minwid < 0) {
minwid = 0 - minwid;
while (group_len++ < minwid && out_p + fillchar_bytes <= out_end_p) {
schar_get_adv(&out_p, fillchar);
}
// If the group is right-aligned, shift everything to the right and
// prepend with filler characters.
} else {
// { Move the group to the right
group_len = (min_group_width - group_len) * (int)schar_len(fillchar);
memmove(t + group_len, t, (size_t)(out_p - t));
if (out_p + group_len >= (out_end_p + 1)) {
group_len = out_end_p - out_p;
ptrdiff_t added_cells = minwid - group_len;
ptrdiff_t added_bytes = added_cells * fillchar_bytes;
if (out_p + added_bytes > out_end_p) {
added_cells = (out_end_p - out_p) / fillchar_bytes;
added_bytes = added_cells * fillchar_bytes;
}
out_p += group_len;
// { Move the group to the right
memmove(t + added_bytes, t, (size_t)(out_p - t));
out_p += added_bytes;
// }
// Adjust item start positions
for (int n = stl_groupitems[groupdepth] + 1; n < curitem; n++) {
stl_items[n].start += group_len;
stl_items[n].start += added_bytes;
}
// Prepend the fill characters
for (; group_len > 0; group_len--) {
for (; added_cells > 0; added_cells--) {
schar_get_adv(&t, fillchar);
}
}

View File

@@ -429,6 +429,40 @@ describe('build_stl_str_hl', function()
'aaaa~~b~~c~~d~~e~~fg~~hi~~jk~~lmnop~~qrstuv~~~wxyz'
)
-- item group testing
statusline_test_align(
'should right align in right aligned groups',
30,
'%5(%l%), %5.(%l%), %5.5(%l%), %5.10(%l%)',
'~~~~0, ~~~~0, ~~~~0, ~~~~0',
{ expected_cell_count = 26 }
)
statusline_test_align(
'should left align in left aligned groups',
30,
'%-5(%l%), %-5.(%l%), %-5.5(%l%), %-5.10(%l%)',
'0~~~~, 0~~~~, 0~~~~, 0~~~~',
{ expected_cell_count = 26 }
)
statusline_test(
'Should truncate groups according to maxwid',
30,
'%.5(1234567%), %2.5(1234567%), %5.5(1234567%)',
'<4567, <4567, <4567',
{ expected_cell_count = 19 }
)
statusline_test_align(
'Should compensate with fillchar after truncating item group at multicell character',
20,
'%.5(12🙂345%), %5.5(12🙂345%), %50.5(12🙂345%)',
'<345, <345~, <345~',
{ expected_cell_count = 18 }
)
-- stl item testing
local tabline = ''
for i = 1, 1000 do