Skip to content

Commit 30f012d

Browse files
mattnchrisbra
authored andcommitted
patch 9.2.0250: system() does not support bypassing the shell
Problem: system() and systemlist() only accept a String, requiring manual shell escaping for arguments with special characters. Solution: Accept a List as the first argument and execute the command bypassing the shell (Yasuhiro Matsumoto). fixes: #19789 closes: #19791 Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
1 parent bde5832 commit 30f012d

14 files changed

Lines changed: 522 additions & 16 deletions

File tree

runtime/doc/builtin.txt

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,8 @@ synconcealed({lnum}, {col}) List info about concealing
699699
synstack({lnum}, {col}) List stack of syntax IDs at {lnum} and
700700
{col}
701701
system({expr} [, {input}]) String output of shell command/filter {expr}
702-
systemlist({expr} [, {input}]) List output of shell command/filter {expr}
702+
systemlist({expr} [, {input}])
703+
List output of shell command/filter {expr}
703704
tabpagebuflist([{arg}]) List list of buffer numbers in tab page
704705
tabpagenr([{arg}]) Number number of current or last tab page
705706
tabpagewinnr({tabarg} [, {arg}])
@@ -11694,6 +11695,30 @@ system({expr} [, {input}]) *system()* *E677*
1169411695
Get the output of the shell command {expr} as a |String|. See
1169511696
|systemlist()| to get the output as a |List|.
1169611697

11698+
{expr} can be a |String| or a |List|.
11699+
When {expr} is a |String|, the command is executed through the
11700+
shell (see below for how the command is constructed).
11701+
11702+
*E1575*
11703+
When {expr} is a |List|, the first item is the executable and
11704+
the remaining items are passed as arguments directly. The
11705+
command is executed without using a shell, similar to
11706+
|job_start()|. Since no shell is involved, shell features
11707+
such as redirection, piping, globbing, environment variable
11708+
expansion and backtick expansion will not work. Characters
11709+
like ">" are passed as literal arguments to the command, not
11710+
interpreted as redirection. Use this form when arguments may
11711+
contain special characters that should not be interpreted by
11712+
the shell. Example: >
11713+
:let out = system(['grep', '-r', 'pattern', '.'])
11714+
< With the String form ">" would be shell redirection, but
11715+
with a List it is passed as a literal argument: >
11716+
:let out = system(['echo', 'hello', '>', 'world'])
11717+
< This outputs "hello > world", not redirect to a file.
11718+
11719+
To use the shell explicitly with a List: >
11720+
:let out = system(['/bin/sh', '-c', 'echo $HOME'])
11721+
<
1169711722
When {input} is given and is a |String| this string is written
1169811723
to a file and passed as stdin to the command. The string is
1169911724
written as-is, you need to take care of using the correct line
@@ -11719,11 +11744,11 @@ system({expr} [, {input}]) *system()* *E677*
1171911744
being echoed on the screen. >
1172011745
:silent let f = system('ls *.vim')
1172111746
<
11722-
Note: Use |shellescape()| or |::S| with |expand()| or
11723-
|fnamemodify()| to escape special characters in a command
11724-
argument. Newlines in {expr} may cause the command to fail.
11725-
The characters in 'shellquote' and 'shellxquote' may also
11726-
cause trouble.
11747+
Note: When {expr} is a String, use |shellescape()| or |::S|
11748+
with |expand()| or |fnamemodify()| to escape special
11749+
characters in a command argument. Newlines in {expr} may
11750+
cause the command to fail. The characters in 'shellquote'
11751+
and 'shellxquote' may also cause trouble.
1172711752
This is not to be used for interactive commands.
1172811753

1172911754
The result is a String. Example: >
@@ -11736,7 +11761,8 @@ system({expr} [, {input}]) *system()* *E677*
1173611761
To avoid the string being truncated at a NUL, all NUL
1173711762
characters are replaced with SOH (0x01).
1173811763

11739-
The command executed is constructed using several options:
11764+
When {expr} is a String, the command executed is constructed
11765+
using several options:
1174011766
'shell' 'shellcmdflag' 'shellxquote' {expr} 'shellredir' {tmp} 'shellxquote'
1174111767
({tmp} is an automatically generated file name).
1174211768
For Unix, braces are put around {expr} to allow for
@@ -11763,6 +11789,9 @@ system({expr} [, {input}]) *system()* *E677*
1176311789
systemlist({expr} [, {input}]) *systemlist()*
1176411790
Same as |system()|, but returns a |List| with lines (parts of
1176511791
output separated by NL) with NULs transformed into NLs.
11792+
Like |system()|, {expr} can be a |String| (executed through
11793+
the shell) or a |List| (executed directly without a shell).
11794+
See |system()| for details.
1176611795
Output is the same as |readfile()| will output with {binary}
1176711796
argument set to "b", except that there is no extra empty item
1176811797
when the result ends in a NL.

runtime/doc/tags

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4771,6 +4771,7 @@ E1571 builtin.txt /*E1571*
47714771
E1572 options.txt /*E1572*
47724772
E1573 channel.txt /*E1573*
47734773
E1574 channel.txt /*E1574*
4774+
E1575 builtin.txt /*E1575*
47744775
E158 sign.txt /*E158*
47754776
E159 sign.txt /*E159*
47764777
E16 cmdline.txt /*E16*

runtime/doc/version9.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52611,6 +52611,8 @@ Other ~
5261152611
- New "leadtab" value for the 'listchars' setting.
5261252612
- Improved |:set+=|, |:set^=| and |:set-=| handling of comma-separated "key:value"
5261352613
pairs individually (e.g. 'listchars', 'fillchars', 'diffopt').
52614+
- |system()| and |systemlist()| functions accept a list as first argument,
52615+
bypassing the shell completely.
5261452616

5261552617
xxd ~
5261652618
---

src/errors.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3795,8 +3795,6 @@ EXTERN char e_osc_response_timed_out[]
37953795
#ifdef FEAT_EVAL
37963796
EXTERN char e_cannot_add_listener_in_listener_callback[]
37973797
INIT(= N_("E1569: Cannot use listener_add in a listener callback"));
3798-
#endif
3799-
#ifdef FEAT_EVAL
38003798
EXTERN char e_cannot_add_redraw_listener_in_listener_callback[]
38013799
INIT(= N_("E1570: Cannot use redraw_listener_add in a redraw listener callback"));
38023800
EXTERN char e_no_redraw_listener_callbacks_defined[]
@@ -3810,3 +3808,7 @@ EXTERN char e_cannot_listen_on_port[]
38103808
EXTERN char e_gethostbyname_in_channel_listen[]
38113809
INIT(= N_("E1574: gethostbyname(): cannot resolve hostname in channel_listen()"));
38123810
#endif
3811+
#ifdef FEAT_EVAL
3812+
EXTERN char e_cannot_create_pipes[]
3813+
INIT(= N_("E1575: Cannot create pipes"));
3814+
#endif

src/evalfunc.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,7 @@ static argcheck_T arg45_sign_place[] = {arg_number, arg_string, arg_string, arg_
13801380
static argcheck_T arg23_slice[] = {arg_slice1, arg_number, arg_number};
13811381
static argcheck_T arg13_sortuniq[] = {arg_list_any_mod, arg_sort_how, arg_dict_any};
13821382
static argcheck_T arg24_strpart[] = {arg_string, arg_number, arg_number, arg_bool};
1383-
static argcheck_T arg12_system[] = {arg_string, arg_str_or_nr_or_list};
1383+
static argcheck_T arg12_system[] = {arg_string_or_list_any, arg_str_or_nr_or_list};
13841384
static argcheck_T arg23_win_execute[] = {arg_number, arg_string_or_list_string, arg_string};
13851385
static argcheck_T arg23_writefile[] = {arg_list_or_blob, arg_string, arg_string};
13861386
static argcheck_T arg24_match_func[] = {arg_string_or_list_any, arg_string, arg_number, arg_number};

src/misc1.c

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,14 +2511,17 @@ get_cmd_output_as_rettv(
25112511
FILE *fd;
25122512
list_T *list = NULL;
25132513
int flags = SHELL_SILENT;
2514+
int use_argv = FALSE;
2515+
char **argv = NULL;
2516+
int argc = 0;
25142517

25152518
rettv->v_type = VAR_STRING;
25162519
rettv->vval.v_string = NULL;
25172520
if (check_restricted() || check_secure())
25182521
goto errret;
25192522

25202523
if (in_vim9script()
2521-
&& (check_for_string_arg(argvars, 0) == FAIL
2524+
&& (check_for_string_or_list_arg(argvars, 0) == FAIL
25222525
|| check_for_opt_string_or_number_or_list_arg(argvars, 1)
25232526
== FAIL))
25242527
return;
@@ -2598,6 +2601,47 @@ get_cmd_output_as_rettv(
25982601
}
25992602
}
26002603

2604+
// When the command is a List, execute directly without the shell.
2605+
if (argvars[0].v_type == VAR_LIST)
2606+
{
2607+
list_T *l = argvars[0].vval.v_list;
2608+
2609+
if (l == NULL || l->lv_len < 1)
2610+
{
2611+
emsg(_(e_invalid_argument));
2612+
goto errret;
2613+
}
2614+
if (build_argv_from_list(l, &argv, &argc) == FAIL)
2615+
goto errret;
2616+
if (argc == 0 || *skipwhite((char_u *)argv[0]) == NUL)
2617+
{
2618+
emsg(_(e_invalid_argument));
2619+
goto errret;
2620+
}
2621+
use_argv = TRUE;
2622+
2623+
if (p_verbose > 3)
2624+
{
2625+
int i;
2626+
garray_T ga;
2627+
2628+
verbose_enter();
2629+
ga_init2(&ga, 1, 200);
2630+
for (i = 0; i < argc; ++i)
2631+
{
2632+
if (i > 0)
2633+
ga_append(&ga, ' ');
2634+
ga_concat(&ga, (char_u *)argv[i]);
2635+
}
2636+
ga_append(&ga, NUL);
2637+
smsg(_("Executing directly: \"%s\""), (char *)ga.ga_data);
2638+
msg_putchar_attr('\n', 0);
2639+
cursor_on();
2640+
verbose_leave();
2641+
ga_clear(&ga);
2642+
}
2643+
}
2644+
26012645
// Omit SHELL_COOKED when invoked with ":silent". Avoids that the shell
26022646
// echoes typeahead, that messes up the display.
26032647
if (!msg_silent)
@@ -2612,7 +2656,10 @@ get_cmd_output_as_rettv(
26122656
char_u *end;
26132657
int i;
26142658

2615-
res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, &len);
2659+
if (use_argv)
2660+
res = mch_get_cmd_output_direct(argv, infile, flags, &len);
2661+
else
2662+
res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, &len);
26162663
if (res == NULL)
26172664
goto errret;
26182665

@@ -2652,7 +2699,10 @@ get_cmd_output_as_rettv(
26522699
}
26532700
else
26542701
{
2655-
res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, NULL);
2702+
if (use_argv)
2703+
res = mch_get_cmd_output_direct(argv, infile, flags, NULL);
2704+
else
2705+
res = get_cmd_output(tv_get_string(&argvars[0]), infile, flags, NULL);
26562706
# ifdef USE_CRNL
26572707
// translate <CR><NL> into <NL>
26582708
if (res != NULL)
@@ -2674,6 +2724,13 @@ get_cmd_output_as_rettv(
26742724
}
26752725

26762726
errret:
2727+
if (argv != NULL)
2728+
{
2729+
int i;
2730+
for (i = 0; argv[i] != NULL; i++)
2731+
vim_free(argv[i]);
2732+
vim_free(argv);
2733+
}
26772734
if (infile != NULL)
26782735
{
26792736
mch_remove(infile);

src/os_unix.c

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5915,6 +5915,154 @@ mch_call_shell(
59155915
#endif
59165916
}
59175917

5918+
#if defined(FEAT_EVAL)
5919+
/*
5920+
* Execute "argv" directly without the shell and return the output.
5921+
* Used by system() and systemlist() when the command is a List.
5922+
* "infile" is an optional temp file for stdin input.
5923+
* "flags" is SHELL_SILENT etc.
5924+
* When "ret_len" is not NULL, set it to the length of the output.
5925+
* Returns the output in allocated memory (or NULL on error).
5926+
* Sets v:shell_error to the exit status.
5927+
*/
5928+
char_u *
5929+
mch_get_cmd_output_direct(
5930+
char **argv,
5931+
char_u *infile,
5932+
int flags UNUSED,
5933+
int *ret_len)
5934+
{
5935+
pid_t pid;
5936+
int fd_out[2] = {-1, -1};
5937+
int status = -1;
5938+
char_u *buffer = NULL;
5939+
garray_T ga;
5940+
SIGSET_DECL(curset)
5941+
5942+
ga_init2(&ga, 1, 4096);
5943+
5944+
ch_log(NULL, "directly executing: %s", argv[0]);
5945+
5946+
if (pipe(fd_out) < 0)
5947+
{
5948+
emsg(_(e_cannot_create_pipes));
5949+
return NULL;
5950+
}
5951+
5952+
BLOCK_SIGNALS(&curset);
5953+
pid = fork();
5954+
if (pid == -1)
5955+
{
5956+
UNBLOCK_SIGNALS(&curset);
5957+
close(fd_out[0]);
5958+
close(fd_out[1]);
5959+
emsg(_("\nCannot fork\n"));
5960+
return NULL;
5961+
}
5962+
5963+
if (pid == 0)
5964+
{
5965+
// child process
5966+
reset_signals();
5967+
UNBLOCK_SIGNALS(&curset);
5968+
5969+
if (ch_log_active())
5970+
{
5971+
ch_log(NULL, "closing channel log in the child process");
5972+
ch_logfile((char_u *)"", (char_u *)"");
5973+
}
5974+
5975+
// Set up stdin.
5976+
if (infile != NULL)
5977+
{
5978+
int fd_in = open((char *)infile, O_RDONLY);
5979+
if (fd_in >= 0)
5980+
{
5981+
close(0);
5982+
vim_ignored = dup(fd_in);
5983+
close(fd_in);
5984+
}
5985+
}
5986+
else
5987+
{
5988+
int nullfd = open("/dev/null", O_RDONLY);
5989+
if (nullfd >= 0)
5990+
{
5991+
close(0);
5992+
vim_ignored = dup(nullfd);
5993+
close(nullfd);
5994+
}
5995+
}
5996+
5997+
// Set up stdout: write end of pipe.
5998+
close(fd_out[0]);
5999+
close(1);
6000+
vim_ignored = dup(fd_out[1]);
6001+
// Also redirect stderr to the pipe.
6002+
close(2);
6003+
vim_ignored = dup(fd_out[1]);
6004+
close(fd_out[1]);
6005+
6006+
execvp(argv[0], argv);
6007+
_exit(127);
6008+
// NOTREACHED
6009+
}
6010+
6011+
// parent process
6012+
UNBLOCK_SIGNALS(&curset);
6013+
close(fd_out[1]);
6014+
6015+
// Read output from child.
6016+
for (;;)
6017+
{
6018+
char buf[4096];
6019+
int n;
6020+
6021+
n = (int)read(fd_out[0], buf, sizeof(buf));
6022+
if (n <= 0)
6023+
break;
6024+
ga_grow(&ga, n);
6025+
mch_memmove((char *)ga.ga_data + ga.ga_len, buf, n);
6026+
ga.ga_len += n;
6027+
}
6028+
close(fd_out[0]);
6029+
6030+
// Wait for child to finish.
6031+
(void)waitpid(pid, &status, 0);
6032+
if (WIFEXITED(status))
6033+
status = WEXITSTATUS(status);
6034+
else
6035+
status = -1;
6036+
set_vim_var_nr(VV_SHELL_ERROR, (long)status);
6037+
6038+
if (ga.ga_len > 0)
6039+
{
6040+
buffer = alloc(ga.ga_len + 1);
6041+
if (buffer != NULL)
6042+
{
6043+
mch_memmove(buffer, ga.ga_data, ga.ga_len);
6044+
if (ret_len == NULL)
6045+
{
6046+
int i;
6047+
6048+
// Change NUL into SOH, otherwise the string is truncated.
6049+
for (i = 0; i < ga.ga_len; ++i)
6050+
if (buffer[i] == NUL)
6051+
buffer[i] = 1;
6052+
buffer[ga.ga_len] = NUL;
6053+
}
6054+
else
6055+
*ret_len = ga.ga_len;
6056+
}
6057+
}
6058+
else if (ret_len != NULL)
6059+
*ret_len = 0;
6060+
6061+
ga_clear(&ga);
6062+
return buffer;
6063+
}
6064+
#endif
6065+
59186066
#if defined(FEAT_JOB_CHANNEL)
59196067
void
59206068
mch_job_start(char **argv, job_T *job, jobopt_T *options, int is_terminal)

0 commit comments

Comments
 (0)