From 84ae09dd79b9888ba71dc2a28f9afcac3e7b8901 Mon Sep 17 00:00:00 2001 From: Christian Brabandt Date: Fri, 8 May 2026 21:29:21 +0000 Subject: [PATCH] patch 9.2.0458: Crash with invalid shellredir/shellpipe value Problem: Crash with invalid shellredir/shellpipe value (bfredl) Solution: Validate the option and allow only a single "%s". fixes: #20157 closes: #20159 Signed-off-by: Christian Brabandt --- runtime/doc/options.txt | 4 ++++ runtime/doc/tags | 1 + src/errors.h | 2 ++ src/option.c | 37 +++++++++++++++++++++++++++++++ src/optiondefs.h | 4 ++-- src/po/vim.pot | 6 ++++- src/proto/option.pro | 1 + src/testdir/test_options.vim | 2 ++ src/testdir/util/gen_opt_test.vim | 4 ++++ src/version.c | 2 ++ 10 files changed, 60 insertions(+), 3 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 969a17c164..c3753f197f 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -7846,6 +7846,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. This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. @@ -7889,6 +7890,9 @@ A jump table for the options with a short description can be found at |Q_op|. become obsolete (at least for Unix). This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. + *E1577* + Only a single "%s" item is allowed in the option value. + *'shellslash'* *'ssl'* *'noshellslash'* *'nossl'* 'shellslash' 'ssl' boolean (default off) diff --git a/runtime/doc/tags b/runtime/doc/tags index 4ad699ed55..3168235dad 100644 --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -4779,6 +4779,7 @@ E1573 channel.txt /*E1573* E1574 channel.txt /*E1574* E1575 builtin.txt /*E1575* E1576 tagsrch.txt /*E1576* +E1577 options.txt /*E1577* E158 sign.txt /*E158* E159 sign.txt /*E159* E16 cmdline.txt /*E16* diff --git a/src/errors.h b/src/errors.h index 384dfaab9c..3ba7eca8f5 100644 --- a/src/errors.h +++ b/src/errors.h @@ -3811,3 +3811,5 @@ EXTERN char e_cannot_create_pipes[] #endif EXTERN char e_tag_file_entry_must_not_be_url[] INIT(= N_("E1576: Tag file entry must not be a URL")); +EXTERN char e_invalid_format_string_single_percent_s[] + INIT(= N_("E1577: Invalid format string, only one \"%s\" is allowed")); diff --git a/src/option.c b/src/option.c index b44a3990ad..426e58e29b 100644 --- a/src/option.c +++ b/src/option.c @@ -4527,6 +4527,43 @@ did_set_maxsearchcount(optset_T *args UNUSED) #undef MAX_SEARCH_COUNT } +/* + * Validate 'shellpipe'/'shellredir' option. + */ + char * +did_set_shellpipe_redir(optset_T *args) +{ + char_u *p; + bool seen = false; + + for (p = args->os_newval.string; *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; +} + #if defined(BACKSLASH_IN_FILENAME) /* diff --git a/src/optiondefs.h b/src/optiondefs.h index 0a93b70f8e..afcaf96319 100644 --- a/src/optiondefs.h +++ b/src/optiondefs.h @@ -2323,7 +2323,7 @@ static struct vimoption options[] = (char_u *)0L} SCTX_INIT}, {"shellpipe", "sp", P_STRING|P_VI_DEF|P_SECURE, #ifdef FEAT_QUICKFIX - (char_u *)&p_sp, PV_NONE, NULL, NULL, + (char_u *)&p_sp, PV_NONE, did_set_shellpipe_redir, NULL, { # if defined(UNIX) (char_u *)"| tee", @@ -2340,7 +2340,7 @@ static struct vimoption options[] = (char_u *)&p_shq, PV_NONE, NULL, NULL, {(char_u *)"", (char_u *)0L} SCTX_INIT}, {"shellredir", "srr", P_STRING|P_VI_DEF|P_SECURE, - (char_u *)&p_srr, PV_NONE, NULL, NULL, + (char_u *)&p_srr, PV_NONE, did_set_shellpipe_redir, NULL, {(char_u *)">", (char_u *)0L} SCTX_INIT}, {"shellslash", "ssl", P_BOOL|P_VI_DEF, #ifdef BACKSLASH_IN_FILENAME diff --git a/src/po/vim.pot b/src/po/vim.pot index 4dd6ba9093..5db2850389 100644 --- a/src/po/vim.pot +++ b/src/po/vim.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Vim\n" "Report-Msgid-Bugs-To: vim-dev@vim.org\n" -"POT-Creation-Date: 2026-04-29 19:49+0000\n" +"POT-Creation-Date: 2026-05-07 19:25+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -8860,6 +8860,10 @@ msgstr "" msgid "E1576: Tag file entry must not be a URL" msgstr "" +#, c-format +msgid "E1577: Invalid format string, only one \"%s\" is allowed" +msgstr "" + #. type of cmdline window or 0 #. result of cmdline window or 0 #. buffer of cmdline window or NULL diff --git a/src/proto/option.pro b/src/proto/option.pro index ae586ea978..ad8c9043ab 100644 --- a/src/proto/option.pro +++ b/src/proto/option.pro @@ -65,6 +65,7 @@ char *did_set_pyxversion(optset_T *args); char *did_set_readonly(optset_T *args); char *did_set_scrollbind(optset_T *args); char *did_set_maxsearchcount(optset_T *args); +char *did_set_shellpipe_redir(optset_T *args); char *did_set_shellslash(optset_T *args); char *did_set_shiftwidth_tabstop(optset_T *args); char *did_set_showtabline(optset_T *args); diff --git a/src/testdir/test_options.vim b/src/testdir/test_options.vim index fa2667525f..340ca36419 100644 --- a/src/testdir/test_options.vim +++ b/src/testdir/test_options.vim @@ -2658,6 +2658,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'], diff --git a/src/testdir/util/gen_opt_test.vim b/src/testdir/util/gen_opt_test.vim index 91ed2c80cd..f5d71ad185 100644 --- a/src/testdir/util/gen_opt_test.vim +++ b/src/testdir/util/gen_opt_test.vim @@ -307,6 +307,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/src/version.c b/src/version.c index 709f46b8df..2aefdf7e5b 100644 --- a/src/version.c +++ b/src/version.c @@ -729,6 +729,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 458, /**/ 457, /**/