Skip to content

Commit 8be0510

Browse files
committed
Add support for proc_open() with a command array
In this case the progarm will be executed directly, without a shell. On Linux the arguments are passed directly to execvp and no escaping is necessary. On Windows we construct a command string using escaping with the default Windows command-line argument parsing method described at https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments. Apart from avoiding the issue of argument escaping, passing an array and bypassing shell has the advantage of allowing proper signal delivery to the opened process (rather than the shell).
1 parent 143f4e3 commit 8be0510

File tree

4 files changed

+265
-19
lines changed

4 files changed

+265
-19
lines changed

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ PHP NEWS
55
- Opcache:
66
. Fixed #78202 (Opcache stats for cache hits are capped at 32bit NUM). (cmb)
77

8+
- Standard:
9+
. Implemented FR #78177 (Make proc_open accept command array). (Nikita)
10+
811
27 Jun 2019, PHP 7.4.0alpha2
912

1013
- Core:

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,12 @@ PHP 7.4 UPGRADE NOTES
279279
arguments, in which case they will return an empty array. This is useful
280280
in conjunction with the spread operator, e.g. array_merge(...$arrays).
281281

282+
. proc_open() now accepts an array instead of a string for the command. In
283+
this case the process will be opened directly (without going through a
284+
shell) and PHP will take care of any necessary argument escaping.
285+
286+
proc_open(['php', '-r', 'echo "Hello World\n";'], $descriptors, $pipes);
287+
282288
========================================
283289
3. Changes in SAPI modules
284290
========================================

ext/standard/proc_open.c

Lines changed: 165 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include "php_globals.h"
3535
#include "SAPI.h"
3636
#include "main/php_network.h"
37+
#include "zend_smart_string.h"
3738

3839
#if HAVE_SYS_WAIT_H
3940
#include <sys/wait.h>
@@ -399,12 +400,87 @@ struct php_proc_open_descriptor_item {
399400
};
400401
/* }}} */
401402

403+
static zend_string *get_valid_arg_string(zval *zv, int elem_num) {
404+
zend_string *str = zval_get_string(zv);
405+
if (!str) {
406+
return NULL;
407+
}
408+
409+
if (strlen(ZSTR_VAL(str)) != ZSTR_LEN(str)) {
410+
php_error_docref(NULL, E_WARNING,
411+
"Command array element %d contains a null byte", elem_num);
412+
zend_string_release(str);
413+
return NULL;
414+
}
415+
416+
return str;
417+
}
418+
419+
#ifdef PHP_WIN32
420+
static void append_backslashes(smart_string *str, size_t num_bs) {
421+
size_t i;
422+
for (i = 0; i < num_bs; i++) {
423+
smart_string_appendc(str, '\\');
424+
}
425+
}
426+
427+
/* See https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments */
428+
static void append_win_escaped_arg(smart_string *str, char *arg) {
429+
char c;
430+
size_t num_bs = 0;
431+
smart_string_appendc(str, '"');
432+
while ((c = *arg)) {
433+
if (c == '\\') {
434+
num_bs++;
435+
} else {
436+
if (c == '"') {
437+
/* Backslashes before " need to be doubled. */
438+
num_bs = num_bs * 2 + 1;
439+
}
440+
append_backslashes(str, num_bs);
441+
smart_string_appendc(str, c);
442+
num_bs = 0;
443+
}
444+
arg++;
445+
}
446+
append_backslashes(str, num_bs * 2);
447+
smart_string_appendc(str, '"');
448+
}
449+
450+
static char *create_win_command_from_args(HashTable *args) {
451+
smart_string str = {0};
452+
zval *arg_zv;
453+
zend_bool is_prog_name = 1;
454+
int elem_num = 0;
455+
456+
ZEND_HASH_FOREACH_VAL(args, arg_zv) {
457+
zend_string *arg_str = get_valid_arg_string(arg_zv, ++elem_num);
458+
if (!arg_str) {
459+
smart_string_free(&str);
460+
return NULL;
461+
}
462+
463+
if (!is_prog_name) {
464+
smart_string_appendc(&str, ' ');
465+
}
466+
467+
append_win_escaped_arg(&str, ZSTR_VAL(arg_str));
468+
469+
is_prog_name = 0;
470+
zend_string_release(arg_str);
471+
} ZEND_HASH_FOREACH_END();
472+
smart_string_0(&str);
473+
return str.c;
474+
}
475+
#endif
476+
402477
/* {{{ proto resource proc_open(string command, array descriptorspec, array &pipes [, string cwd [, array env [, array other_options]]])
403478
Run a process with more control over it's file descriptors */
404479
PHP_FUNCTION(proc_open)
405480
{
406-
char *command, *cwd=NULL;
407-
size_t command_len, cwd_len = 0;
481+
zval *command_zv;
482+
char *command = NULL, *cwd = NULL;
483+
size_t cwd_len = 0;
408484
zval *descriptorspec;
409485
zval *pipes;
410486
zval *environment = NULL;
@@ -428,23 +504,23 @@ PHP_FUNCTION(proc_open)
428504
char cur_cwd[MAXPATHLEN];
429505
wchar_t *cmdw = NULL, *cwdw = NULL, *envpw = NULL;
430506
size_t tmp_len;
431-
#endif
432-
php_process_id_t child;
433-
struct php_process_handle *proc;
434-
int is_persistent = 0; /* TODO: ensure that persistent procs will work */
435-
#ifdef PHP_WIN32
436507
int suppress_errors = 0;
437508
int bypass_shell = 0;
438509
int blocking_pipes = 0;
439510
int create_process_group = 0;
511+
#else
512+
char **argv = NULL;
440513
#endif
514+
php_process_id_t child;
515+
struct php_process_handle *proc;
516+
int is_persistent = 0; /* TODO: ensure that persistent procs will work */
441517
#if PHP_CAN_DO_PTS
442518
php_file_descriptor_t dev_ptmx = -1; /* master */
443519
php_file_descriptor_t slave_pty = -1;
444520
#endif
445521

446522
ZEND_PARSE_PARAMETERS_START(3, 6)
447-
Z_PARAM_STRING(command, command_len)
523+
Z_PARAM_ZVAL(command_zv)
448524
Z_PARAM_ARRAY(descriptorspec)
449525
Z_PARAM_ZVAL(pipes)
450526
Z_PARAM_OPTIONAL
@@ -453,7 +529,48 @@ PHP_FUNCTION(proc_open)
453529
Z_PARAM_ARRAY_EX(other_options, 1, 0)
454530
ZEND_PARSE_PARAMETERS_END_EX(RETURN_FALSE);
455531

456-
command = pestrdup(command, is_persistent);
532+
memset(&env, 0, sizeof(env));
533+
534+
if (Z_TYPE_P(command_zv) == IS_ARRAY) {
535+
zval *arg_zv;
536+
uint32_t num_elems = zend_hash_num_elements(Z_ARRVAL_P(command_zv));
537+
if (num_elems == 0) {
538+
php_error_docref(NULL, E_WARNING, "Command array must have at least one element");
539+
RETURN_FALSE;
540+
}
541+
542+
#ifdef PHP_WIN32
543+
bypass_shell = 1;
544+
command = create_win_command_from_args(Z_ARRVAL_P(command_zv));
545+
if (!command) {
546+
RETURN_FALSE;
547+
}
548+
#else
549+
argv = safe_emalloc(sizeof(char *), num_elems + 1, 0);
550+
i = 0;
551+
ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(command_zv), arg_zv) {
552+
zend_string *arg_str = get_valid_arg_string(arg_zv, i + 1);
553+
if (!arg_str) {
554+
argv[i] = NULL;
555+
goto exit_fail;
556+
}
557+
558+
if (i == 0) {
559+
command = pestrdup(ZSTR_VAL(arg_str), is_persistent);
560+
}
561+
562+
argv[i++] = estrdup(ZSTR_VAL(arg_str));
563+
zend_string_release(arg_str);
564+
} ZEND_HASH_FOREACH_END();
565+
argv[i] = NULL;
566+
567+
/* As the array is non-empty, we should have found a command. */
568+
ZEND_ASSERT(command);
569+
#endif
570+
} else {
571+
convert_to_string(command_zv);
572+
command = pestrdup(Z_STRVAL_P(command_zv), is_persistent);
573+
}
457574

458575
#ifdef PHP_WIN32
459576
if (other_options) {
@@ -464,6 +581,7 @@ PHP_FUNCTION(proc_open)
464581
}
465582
}
466583

584+
/* TODO Deprecate in favor of array command? */
467585
item = zend_hash_str_find(Z_ARRVAL_P(other_options), "bypass_shell", sizeof("bypass_shell") - 1);
468586
if (item != NULL) {
469587
if (Z_TYPE_P(item) == IS_TRUE || ((Z_TYPE_P(item) == IS_LONG) && Z_LVAL_P(item))) {
@@ -487,12 +605,8 @@ PHP_FUNCTION(proc_open)
487605
}
488606
#endif
489607

490-
command_len = strlen(command);
491-
492608
if (environment) {
493609
env = _php_array_to_envp(environment, is_persistent);
494-
} else {
495-
memset(&env, 0, sizeof(env));
496610
}
497611

498612
ndescriptors_array = zend_hash_num_elements(Z_ARRVAL_P(descriptorspec));
@@ -744,7 +858,7 @@ PHP_FUNCTION(proc_open)
744858
}
745859
}
746860

747-
cmdw = php_win32_cp_conv_any_to_w(command, command_len, &tmp_len);
861+
cmdw = php_win32_cp_conv_any_to_w(command, strlen(command), &tmp_len);
748862
if (!cmdw) {
749863
php_error_docref(NULL, E_WARNING, "Command conversion failed");
750864
goto exit_fail;
@@ -852,10 +966,18 @@ PHP_FUNCTION(proc_open)
852966
php_ignore_value(chdir(cwd));
853967
}
854968

855-
if (env.envarray) {
856-
execle("/bin/sh", "sh", "-c", command, NULL, env.envarray);
969+
if (argv) {
970+
/* execvpe() is non-portable, use environ instead. */
971+
if (env.envarray) {
972+
environ = env.envarray;
973+
}
974+
execvp(command, argv);
857975
} else {
858-
execl("/bin/sh", "sh", "-c", command, NULL);
976+
if (env.envarray) {
977+
execle("/bin/sh", "sh", "-c", command, NULL, env.envarray);
978+
} else {
979+
execl("/bin/sh", "sh", "-c", command, NULL);
980+
}
859981
}
860982
_exit(127);
861983

@@ -960,18 +1082,42 @@ PHP_FUNCTION(proc_open)
9601082
}
9611083
}
9621084

1085+
#ifndef PHP_WIN32
1086+
if (argv) {
1087+
char **arg = argv;
1088+
while (*arg != NULL) {
1089+
efree(*arg);
1090+
arg++;
1091+
}
1092+
efree(argv);
1093+
}
1094+
#endif
1095+
9631096
efree(descriptors);
9641097
ZVAL_RES(return_value, zend_register_resource(proc, le_proc_open));
9651098
return;
9661099

9671100
exit_fail:
968-
efree(descriptors);
1101+
if (descriptors) {
1102+
efree(descriptors);
1103+
}
9691104
_php_free_envp(env, is_persistent);
970-
pefree(command, is_persistent);
1105+
if (command) {
1106+
pefree(command, is_persistent);
1107+
}
9711108
#ifdef PHP_WIN32
9721109
free(cwdw);
9731110
free(cmdw);
9741111
free(envpw);
1112+
#else
1113+
if (argv) {
1114+
char **arg = argv;
1115+
while (*arg != NULL) {
1116+
efree(*arg);
1117+
arg++;
1118+
}
1119+
efree(argv);
1120+
}
9751121
#endif
9761122
#if PHP_CAN_DO_PTS
9771123
if (dev_ptmx >= 0) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
--TEST--
2+
Using proc_open() with a command array (no shell)
3+
--FILE--
4+
<?php
5+
6+
$php = getenv('TEST_PHP_EXECUTABLE');
7+
$ds = [
8+
0 => ['pipe', 'r'],
9+
1 => ['pipe', 'w'],
10+
2 => ['pipe', 'w'],
11+
];
12+
13+
echo "Empty command array:";
14+
var_dump(proc_open([], $ds, $pipes));
15+
16+
echo "\nNul byte in program name:";
17+
var_dump(proc_open(["php\0oops"], $ds, $pipes));
18+
19+
echo "\nNul byte in argument:";
20+
var_dump(proc_open(["php", "arg\0oops"], $ds, $pipes));
21+
22+
echo "\nBasic usage:\n";
23+
$proc = proc_open([$php, '-r', 'echo "Hello World!\n";'], $ds, $pipes);
24+
fpassthru($pipes[1]);
25+
proc_close($proc);
26+
27+
putenv('ENV_1=ENV_1');
28+
$env = ['ENV_2' => 'ENV_2'];
29+
$cmd = [$php, '-r', 'var_dump(getenv("ENV_1"), getenv("ENV_2"));'];
30+
31+
echo "\nEnvironment inheritance:\n";
32+
$proc = proc_open($cmd, $ds, $pipes);
33+
fpassthru($pipes[1]);
34+
proc_close($proc);
35+
36+
echo "\nExplicit environment:\n";
37+
$proc = proc_open($cmd, $ds, $pipes, null, $env);
38+
fpassthru($pipes[1]);
39+
proc_close($proc);
40+
41+
echo "\nCheck that arguments are correctly passed through:\n";
42+
$args = [
43+
"Simple",
44+
"White space\ttab\nnewline",
45+
'"Quoted"',
46+
'Qu"ot"ed',
47+
'\\Back\\slash\\',
48+
'\\\\Back\\\\slash\\\\',
49+
'\\"Qu\\"ot\\"ed\\"',
50+
];
51+
$cmd = [$php, '-r', 'var_export(array_slice($argv, 1));', '--', ...$args];
52+
$proc = proc_open($cmd, $ds, $pipes);
53+
fpassthru($pipes[1]);
54+
proc_close($proc);
55+
56+
?>
57+
--EXPECTF--
58+
Empty command array:
59+
Warning: proc_open(): Command array must have at least one element in %s on line %d
60+
bool(false)
61+
62+
Nul byte in program name:
63+
Warning: proc_open(): Command array element 1 contains a null byte in %s on line %d
64+
bool(false)
65+
66+
Nul byte in argument:
67+
Warning: proc_open(): Command array element 2 contains a null byte in %s on line %d
68+
bool(false)
69+
70+
Basic usage:
71+
Hello World!
72+
73+
Environment inheritance:
74+
string(5) "ENV_1"
75+
bool(false)
76+
77+
Explicit environment:
78+
bool(false)
79+
string(5) "ENV_2"
80+
81+
Check that arguments are correctly passed through:
82+
array (
83+
0 => 'Simple',
84+
1 => 'White space tab
85+
newline',
86+
2 => '"Quoted"',
87+
3 => 'Qu"ot"ed',
88+
4 => '\\Back\\slash\\',
89+
5 => '\\\\Back\\\\slash\\\\',
90+
6 => '\\"Qu\\"ot\\"ed\\"',
91+
)

0 commit comments

Comments
 (0)