From ffe87d91f72a4e9c0e72597d42a2a3fd1f0081c7 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 9 May 2026 09:13:19 +0800 Subject: [PATCH] vim-patch:9.2.0458: Crash with invalid shellredir/shellpipe value (#39691) Problem: Crash with invalid shellredir/shellpipe value (bfredl) Solution: Validate the option and allow only a single "%s". fixes: vim/vim#20157 closes: vim/vim#20159 https://github.com/vim/vim/commit/84ae09dd79b9888ba71dc2a28f9afcac3e7b8901 Co-authored-by: Christian Brabandt --- runtime/doc/options.txt | 3 +++ runtime/lua/vim/_meta/options.gen.lua | 3 +++ src/nvim/errors.h | 1 + src/nvim/options.lua | 5 +++++ src/nvim/optionstr.c | 30 +++++++++++++++++++++++++++ test/old/testdir/gen_opt_test.vim | 4 ++++ test/old/testdir/test_options.vim | 2 ++ 7 files changed, 48 insertions(+) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 6f0c3deb68..7675584fe5 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5653,6 +5653,7 @@ A jump table for the options with a short description can be found at |Q_op|. Note: When using a pipe like "| tee", you'll lose the exit code of the shell command. This might be configurable by your shell, look for the pipefail option (for bash and zsh, use ":set -o pipefail"). + Only a single "%s" value is allowed. *'shellquote'* *'shq'* 'shellquote' 'shq' string (default ""; Windows, when 'shell' @@ -5692,6 +5693,8 @@ A jump table for the options with a short description can be found at |Q_op|. explicitly set before. In the future pipes may be used for filtering and this option will become obsolete (at least for Unix). + *E1577* + Only a single "%s" item is allowed in the option value. *'shellslash'* *'ssl'* *'noshellslash'* *'nossl'* 'shellslash' 'ssl' boolean (default on, Windows: off) diff --git a/runtime/lua/vim/_meta/options.gen.lua b/runtime/lua/vim/_meta/options.gen.lua index 732c8df891..e85098cecd 100644 --- a/runtime/lua/vim/_meta/options.gen.lua +++ b/runtime/lua/vim/_meta/options.gen.lua @@ -5918,6 +5918,7 @@ vim.go.shcf = vim.go.shellcmdflag --- Note: When using a pipe like "| tee", you'll lose the exit code of the --- shell command. This might be configurable by your shell, look for --- the pipefail option (for bash and zsh, use ":set -o pipefail"). +--- Only a single "%s" value is allowed. --- --- @type string vim.o.shellpipe = "| tee" @@ -5960,6 +5961,8 @@ vim.go.shq = vim.go.shellquote --- explicitly set before. --- In the future pipes may be used for filtering and this option will --- become obsolete (at least for Unix). +--- *E1577* +--- Only a single "%s" item is allowed in the option value. --- --- @type string vim.o.shellredir = ">" diff --git a/src/nvim/errors.h b/src/nvim/errors.h index 113b2058ff..5bc8dace62 100644 --- a/src/nvim/errors.h +++ b/src/nvim/errors.h @@ -223,6 +223,7 @@ EXTERN const char e_cannot_have_more_than_nr_diff_anchors[] INIT( = N_("E1549: C EXTERN const char e_failed_to_find_all_diff_anchors[] INIT( = N_("E1550: Failed to find all diff anchors")); EXTERN const char e_diff_anchors_with_hidden_windows[] INIT( = N_("E1562: Diff anchors cannot be used with hidden diff windows")); EXTERN const char e_leadtab_requires_tab[] INIT( = N_("E1572: 'listchars' field \"leadtab\" requires \"tab\" to be specified")); +EXTERN const char e_invalid_format_string_single_percent_s[] INIT( = N_("E1577: Invalid format string, only one \"%s\" is allowed")); EXTERN const char e_trustfile[] INIT(= N_("E5570: Cannot update trust file: %s")); EXTERN const char e_cannot_read_from_str_2[] INIT(= N_("E282: Cannot read from \"%s\"")); diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 48a455b55f..2e1cdb23e5 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -7722,6 +7722,7 @@ local options = { }, { abbreviation = 'sp', + cb = 'did_set_shellpipe_redir', defaults = { condition = 'MSWIN', if_false = '| tee', @@ -7758,6 +7759,7 @@ local options = { Note: When using a pipe like "| tee", you'll lose the exit code of the shell command. This might be configurable by your shell, look for the pipefail option (for bash and zsh, use ":set -o pipefail"). + Only a single "%s" value is allowed. ]=], full_name = 'shellpipe', scope = { 'global' }, @@ -7793,6 +7795,7 @@ local options = { }, { abbreviation = 'srr', + cb = 'did_set_shellpipe_redir', defaults = { condition = 'MSWIN', if_false = '>', @@ -7819,6 +7822,8 @@ local options = { explicitly set before. In the future pipes may be used for filtering and this option will become obsolete (at least for Unix). + *E1577* + Only a single "%s" item is allowed in the option value. ]=], full_name = 'shellredir', scope = { 'global' }, diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 06e9ca0e94..d16d2b18c0 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -1714,6 +1714,36 @@ const char *did_set_shada(optset_T *args) return NULL; } +/// Validate 'shellpipe'/'shellredir' option. +const char *did_set_shellpipe_redir(optset_T *args) +{ + bool seen = false; + + for (char *p = args->os_newval.string.data; *p != NUL; p++) { + if (*p != '%') { + continue; + } + if (p[1] == NUL) { + return e_invalid_format_string_single_percent_s; + } + if (p[1] == '%') { + p++; // skip second % + continue; + } + + if (p[1] == 's') { + if (seen) { + return e_invalid_format_string_single_percent_s; + } + seen = true; + p++; // consume 's' + continue; + } + return e_invalid_format_string_single_percent_s; + } + return NULL; +} + /// The 'shortmess' option is changed. const char *did_set_shortmess(optset_T *args) { diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim index fd772665e9..3f1991b57a 100644 --- a/test/old/testdir/gen_opt_test.vim +++ b/test/old/testdir/gen_opt_test.vim @@ -328,6 +328,10 @@ let test_values = { \ 'sessionoptions': [['', 'blank', 'curdir', 'sesdir', \ 'help,options,slash'], \ ['xxx', 'curdir,sesdir']], + \ 'shellpipe': [[ '', '>', '>%s2>&1', '\|tee', '\|&tee', '2>&1\|tee', '%%'], + \ ['%s%s%s', '%s%p%d']], + \ 'shellredir': [[ '', '>', '>%s2>&1', '\|tee', '\|&tee', '2>&1\|tee', '%%'], + \ ['%s%s%s', '%s%p%d']], \ 'showcmdloc': [['last', 'statusline', 'tabline'], ['xxx']], "\ 'signcolumn': [['', 'auto', 'no', 'yes', 'number'], ['xxx', 'no,yes']], \ 'spellfile': [['', 'file.en.add', 'xxx.en.add,yyy.gb.add,zzz.ja.add', diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index 33043b9ee5..97bbb36143 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -2629,6 +2629,8 @@ func Test_string_option_revert_on_failure() \ ['selection', 'exclusive', 'a123'], \ ['selectmode', 'cmd', 'a123'], \ ['sessionoptions', 'options', 'a123'], + \ ['shellpipe', '>%s', "%s%s%s"], + \ ['shellredir', '>%s', "%s%s%s"], \ ['shortmess', 'w', '2'], \ ['showbreak', '>>', "\x01"], \ ['showcmdloc', 'statusline', 'a123'],