diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 4d6f26271e..c83f29e258 100644 --- 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 index 10f295be42..c7290df25b 100644 --- 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 index 3e3f147e37..f9f8a183ab 100644 --- 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 index 4a39065aeb..d696459b18 100644 --- 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 index 7f85963b02..3e6de605f2 100644 --- 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 index 2aefcd70b2..bd53a42461 100644 --- 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 index 2cd9e49a12..a1050f82cb 100644 --- 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