From 83d9b9afca13af5164198f0cbabfa41f33f11b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Hoffmann?= Date: Sat, 13 Jun 2026 19:57:01 +0200 Subject: [PATCH] fix(statusline): prevent various buffer overflows with item groups #40219 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 "∙∙∙∙", 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 e36659c82f6d51631cf6a88a92ff2b467a078b18) --- src/nvim/statusline.c | 37 +++++++++++++++++++++-------------- test/unit/statusline_spec.lua | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 1ac013f43b..017064c9e5 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -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); } } diff --git a/test/unit/statusline_spec.lua b/test/unit/statusline_spec.lua index 1388063a92..abf45474bb 100644 --- a/test/unit/statusline_spec.lua +++ b/test/unit/statusline_spec.lua @@ -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