commit 0fa0717d4ed7b59ac2211977482e7a0de7f74b4d
parent fcf752476a33a26058b3342ac09108f76801bd4b
Author: zeertzjq <zeertzjq@outlook.com>
Date: Mon, 29 Sep 2025 07:48:46 +0800
vim-patch:9.1.1802: 'nowrap' in a modeline may hide malicious code (#35946)
Problem: 'nowrap' in a modeline may hide malicious code.
Solution: Forcibly use '>' as 'listchars' "extends" if 'nowrap' was set
from a modeline (zeertzjq).
Manual `:setlocal nowrap` disables this behavior. There is a separate
problem with `:set nowrap` that also applies to some other options.
related: vim/vim#18214
related: vim/vim#18399
closes: vim/vim#18425
https://github.com/vim/vim/commit/9d5208a9313dd8b0d62c97af5485f1715af98a1c
Cherry-pick some test_modeline.vim changes from patches 9.0.{0363,0626}.
Diffstat:
7 files changed, 89 insertions(+), 12 deletions(-)
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -7639,6 +7639,11 @@ A jump table for the options with a short description can be found at |Q_op|.
< See 'sidescroll', 'listchars' and |wrap-off|.
This option can't be set from a |modeline| when the 'diff' option is
on.
+ If 'nowrap' was set from a |modeline| or in the |sandbox|, '>' is used
+ as the |lcs-extends| character regardless of the value of the 'list'
+ and 'listchars' options. This is to prevent malicious code outside
+ the viewport from going unnoticed. Use `:setlocal nowrap` manually
+ afterwards to disable this behavior.
*'wrapmargin'* *'wm'*
'wrapmargin' 'wm' number (default 0)
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -8408,6 +8408,11 @@ vim.go.wiw = vim.go.winwidth
--- See 'sidescroll', 'listchars' and `wrap-off`.
--- This option can't be set from a `modeline` when the 'diff' option is
--- on.
+--- If 'nowrap' was set from a `modeline` or in the `sandbox`, '>' is used
+--- as the `lcs-extends` character regardless of the value of the 'list'
+--- and 'listchars' options. This is to prevent malicious code outside
+--- the viewport from going unnoticed. Use `:setlocal nowrap` manually
+--- afterwards to disable this behavior.
---
--- @type boolean
vim.o.wrap = true
diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h
@@ -1303,6 +1303,7 @@ struct window_S {
#define GLOBAL_WO(p) ((char *)(p) + sizeof(winopt_T))
// A few options have local flags for kOptFlagInsecure.
+ uint32_t w_p_wrap_flags; // flags for 'wrap'
uint32_t w_p_stl_flags; // flags for 'statusline'
uint32_t w_p_wbr_flags; // flags for 'winbar'
uint32_t w_p_fde_flags; // flags for 'foldexpr'
diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c
@@ -153,6 +153,21 @@ void drawline_free_all_mem(void)
}
#endif
+/// Get the 'listchars' "extends" characters to use for "wp", or NUL if it
+/// shouldn't be used.
+static schar_T get_lcs_ext(win_T *wp)
+{
+ if (wp->w_p_wrap) {
+ // Line never continues beyond the right of the screen with 'wrap'.
+ return NUL;
+ }
+ if (wp->w_p_wrap_flags & kOptFlagInsecure) {
+ // If 'nowrap' was set from a modeline, forcibly use '>'.
+ return schar_from_ascii('>');
+ }
+ return wp->w_p_list ? wp->w_p_lcs_chars.ext : NUL;
+}
+
/// Advance wlv->color_cols if not NULL
static void advance_color_col(winlinevars_T *wlv, int vcol)
{
@@ -2865,12 +2880,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b
break;
}
- // Show "extends" character from 'listchars' if beyond the line end and
- // 'list' is set.
- // Don't show this with 'wrap' as the line can't be scrolled horizontally.
- if (wp->w_p_lcs_chars.ext != NUL
- && wp->w_p_list
- && !wp->w_p_wrap
+ // Show "extends" character from 'listchars' if beyond the line end.
+ const schar_T lcs_ext = get_lcs_ext(wp);
+ if (lcs_ext != NUL
&& wlv.filler_todo <= 0
&& wlv.col == view_width - 1
&& !has_foldtext) {
@@ -2882,7 +2894,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, int col_rows, b
|| (lcs_eol > 0 && lcs_eol_todo)
|| (wlv.n_extra > 0 && (wlv.sc_extra != NUL || *wlv.p_extra != NUL))
|| (may_have_inline_virt && has_more_inline_virt(&wlv, ptr - line))) {
- mb_schar = wp->w_p_lcs_chars.ext;
+ mb_schar = lcs_ext;
wlv.char_attr = win_hl_attr(wp, HLF_AT);
mb_c = schar_get_first_codepoint(mb_schar);
}
diff --git a/src/nvim/option.c b/src/nvim/option.c
@@ -1644,6 +1644,8 @@ uint32_t *insecure_flag(win_T *const wp, OptIndex opt_idx, int opt_flags)
if (opt_flags & OPT_LOCAL) {
assert(wp != NULL);
switch (opt_idx) {
+ case kOptWrap:
+ return &wp->w_p_wrap_flags;
case kOptStatusline:
return &wp->w_p_stl_flags;
case kOptWinbar:
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -10811,6 +10811,11 @@ local options = {
< See 'sidescroll', 'listchars' and |wrap-off|.
This option can't be set from a |modeline| when the 'diff' option is
on.
+ If 'nowrap' was set from a |modeline| or in the |sandbox|, '>' is used
+ as the |lcs-extends| character regardless of the value of the 'list'
+ and 'listchars' options. This is to prevent malicious code outside
+ the viewport from going unnoticed. Use `:setlocal nowrap` manually
+ afterwards to disable this behavior.
]=],
full_name = 'wrap',
redraw = { 'current_window' },
diff --git a/test/old/testdir/test_modeline.vim b/test/old/testdir/test_modeline.vim
@@ -358,22 +358,69 @@ endfunc
" Some options cannot be set from the modeline when 'diff' option is set
func Test_modeline_diff_buffer()
- call writefile(['vim: diff foldmethod=marker wrap'], 'Xfile')
+ call writefile(['vim: diff foldmethod=marker wrap'], 'Xmdifile', 'D')
set foldmethod& nowrap
- new Xfile
+ new Xmdifile
call assert_equal('manual', &foldmethod)
call assert_false(&wrap)
set wrap&
- call delete('Xfile')
bw
endfunc
func Test_modeline_disable()
set modeline
- call writefile(['vim: sw=2', 'vim: nomodeline', 'vim: sw=3'], 'Xmodeline_disable')
+ call writefile(['vim: sw=2', 'vim: nomodeline', 'vim: sw=3'], 'Xmodeline_disable', 'D')
edit Xmodeline_disable
call assert_equal(2, &sw)
- call delete('Xmodeline_disable')
+endfunc
+
+" If 'nowrap' is set from a modeline, '>' is used forcibly as lcs-extends.
+func Test_modeline_nowrap_lcs_extends()
+ call writefile([
+ \ 'aaa',
+ \ 'bbb',
+ \ 'ccc evil',
+ \ 'ddd vim: nowrap',
+ \ ], 'Xmodeline_nowrap', 'D')
+ call NewWindow(10, 20)
+
+ setlocal nolist listchars=
+ edit Xmodeline_nowrap
+ let expect_insecure = [
+ \ 'aaa ',
+ \ 'bbb ',
+ \ 'ccc >',
+ \ 'ddd >',
+ \ '~ ',
+ \ ]
+ call assert_equal(expect_insecure, ScreenLines([1, 5], 20))
+
+ setlocal nowrap
+ let expect_secure = [
+ \ 'aaa ',
+ \ 'bbb ',
+ \ 'ccc ',
+ \ 'ddd ',
+ \ '~ ',
+ \ ]
+ call assert_equal(expect_secure, ScreenLines([1, 5], 20))
+
+ setlocal list listchars=extends:+
+ let expect_secure = [
+ \ 'aaa ',
+ \ 'bbb ',
+ \ 'ccc +',
+ \ 'ddd +',
+ \ '~ ',
+ \ ]
+ call assert_equal(expect_secure, ScreenLines([1, 5], 20))
+
+ edit Xmodeline_nowrap
+ call assert_equal(expect_insecure, ScreenLines([1, 5], 20))
+ setlocal nowrap
+ call assert_equal(expect_secure, ScreenLines([1, 5], 20))
+
+ call CloseWindow()
endfunc
" vim: shiftwidth=2 sts=2 expandtab