Skip to content

Commit b53b529

Browse files
authored
ext/pcntl: Refactor pcntl_exec() (#17172)
* Refactor usage of strlcpy As we allocate the buffer, we know the string will fit inside the buffer. * Throw ValueErrors when strings contain null bytes The underlying C calls work with C strings, which are NULL terminated. * exec_pcntl() always return false Thus, update stubs to formally have a return type of `false`.
1 parent 916288e commit b53b529

File tree

5 files changed

+98
-41
lines changed

5 files changed

+98
-41
lines changed

UPGRADING

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ PHP 8.5 UPGRADE NOTES
4242
. ldap_get_option() and ldap_set_option() now throw a ValueError when
4343
passing an invalid option.
4444

45+
- PCNTL:
46+
. pcntl_exec() now throws ValueErrors when entries of the $args parameter
47+
contain null bytes.
48+
. pcntl_exec() now throws ValueErrors when entries or keys of the
49+
$env_vars parameter contain null bytes.
50+
4551
- SPL:
4652
. ArrayObject no longer accepts enums, as modifying the $name or $value
4753
properties can break engine assumptions.
@@ -91,6 +97,9 @@ PHP 8.5 UPGRADE NOTES
9197
gzfile, gzopen and readgzfile functions had been changed
9298
from int to boolean.
9399

100+
- PCNTL:
101+
. pcntl_exec() now has a formal return type of false.
102+
94103
- PDO_PGSQL:
95104
. PDO::pgsqlCopyFromArray also supports inputs as Iterable.
96105
. Pdo\Pgsql::setAttribute and Pdo\Pgsql::prepare supports

ext/pcntl/pcntl.c

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -620,44 +620,44 @@ PHP_FUNCTION(pcntl_wstopsig)
620620
/* {{{ Executes specified program in current process space as defined by exec(2) */
621621
PHP_FUNCTION(pcntl_exec)
622622
{
623-
zval *args = NULL, *envs = NULL;
623+
zval *args = NULL;
624+
HashTable *env_vars_ht = NULL;
624625
zval *element;
625-
HashTable *args_hash, *envs_hash;
626-
int argc = 0, argi = 0;
627-
int envc = 0, envi = 0;
628626
char **argv = NULL, **envp = NULL;
629-
char **current_arg, **pair;
630-
size_t pair_length;
631-
zend_string *key;
632627
char *path;
633628
size_t path_len;
634-
zend_ulong key_num;
635629

636630
ZEND_PARSE_PARAMETERS_START(1, 3)
637631
Z_PARAM_PATH(path, path_len)
638632
Z_PARAM_OPTIONAL
639633
Z_PARAM_ARRAY(args)
640-
Z_PARAM_ARRAY(envs)
634+
Z_PARAM_ARRAY_HT(env_vars_ht)
641635
ZEND_PARSE_PARAMETERS_END();
642636

643-
if (ZEND_NUM_ARGS() > 1) {
637+
if (args != NULL) {
638+
// TODO Check array is a list?
644639
/* Build argument list */
645640
SEPARATE_ARRAY(args);
646-
args_hash = Z_ARRVAL_P(args);
647-
argc = zend_hash_num_elements(args_hash);
641+
const HashTable *args_ht = Z_ARRVAL_P(args);
642+
uint32_t argc = zend_hash_num_elements(args_ht);
648643

644+
/* We want a NULL terminated array of char* with the first entry being the path,
645+
* followed by the arguments */
649646
argv = safe_emalloc((argc + 2), sizeof(char *), 0);
650-
*argv = path;
651-
current_arg = argv+1;
652-
ZEND_HASH_FOREACH_VAL(args_hash, element) {
653-
if (argi >= argc) break;
647+
argv[0] = path;
648+
char **current_arg = argv+1;
649+
ZEND_HASH_FOREACH_VAL(args_ht, element) {
654650
if (!try_convert_to_string(element)) {
655651
efree(argv);
656652
RETURN_THROWS();
657653
}
654+
if (zend_str_has_nul_byte(Z_STR_P(element))) {
655+
zend_argument_value_error(2, "individual argument must not contain null bytes");
656+
efree(argv);
657+
RETURN_THROWS();
658+
}
658659

659660
*current_arg = Z_STRVAL_P(element);
660-
argi++;
661661
current_arg++;
662662
} ZEND_HASH_FOREACH_END();
663663
*current_arg = NULL;
@@ -667,39 +667,51 @@ PHP_FUNCTION(pcntl_exec)
667667
argv[1] = NULL;
668668
}
669669

670-
if ( ZEND_NUM_ARGS() == 3 ) {
670+
if (env_vars_ht != NULL) {
671671
/* Build environment pair list */
672-
SEPARATE_ARRAY(envs);
673-
envs_hash = Z_ARRVAL_P(envs);
674-
envc = zend_hash_num_elements(envs_hash);
672+
char **pair;
673+
zend_ulong key_num;
674+
zend_string *key;
675675

676-
size_t envp_len = (envc + 1);
676+
/* We want a NULL terminated array of char* */
677+
size_t envp_len = zend_hash_num_elements(env_vars_ht) + 1;
677678
pair = envp = safe_emalloc(envp_len, sizeof(char *), 0);
678679
memset(envp, 0, sizeof(char *) * envp_len);
679-
ZEND_HASH_FOREACH_KEY_VAL(envs_hash, key_num, key, element) {
680-
if (envi >= envc) break;
681-
if (!key) {
682-
key = zend_long_to_str(key_num);
683-
} else {
684-
zend_string_addref(key);
680+
ZEND_HASH_FOREACH_KEY_VAL(env_vars_ht, key_num, key, element) {
681+
zend_string *element_str = zval_try_get_string(element);
682+
if (element_str == NULL) {
683+
goto cleanup_env_vars;
685684
}
686685

687-
if (!try_convert_to_string(element)) {
688-
zend_string_release(key);
686+
if (zend_str_has_nul_byte(element_str)) {
687+
zend_argument_value_error(3, "value for environment variable must not contain null bytes");
688+
zend_string_release_ex(element_str, false);
689689
goto cleanup_env_vars;
690690
}
691691

692+
/* putenv() allows integer environment variables */
693+
if (!key) {
694+
key = zend_long_to_str((zend_long) key_num);
695+
} else {
696+
if (zend_str_has_nul_byte(key)) {
697+
zend_argument_value_error(3, "name for environment variable must not contain null bytes");
698+
zend_string_release_ex(element_str, false);
699+
goto cleanup_env_vars;
700+
}
701+
zend_string_addref(key);
702+
}
703+
692704
/* Length of element + equal sign + length of key + null */
693-
ZEND_ASSERT(Z_STRLEN_P(element) < SIZE_MAX && ZSTR_LEN(key) < SIZE_MAX);
694-
*pair = safe_emalloc(Z_STRLEN_P(element) + 1, sizeof(char), ZSTR_LEN(key) + 1);
695-
pair_length = Z_STRLEN_P(element) + ZSTR_LEN(key) + 2;
696-
strlcpy(*pair, ZSTR_VAL(key), ZSTR_LEN(key) + 1);
697-
strlcat(*pair, "=", pair_length);
698-
strlcat(*pair, Z_STRVAL_P(element), pair_length);
705+
*pair = safe_emalloc(ZSTR_LEN(element_str) + 1, sizeof(char), ZSTR_LEN(key) + 1);
706+
/* Copy key=element + final null byte into buffer */
707+
memcpy(*pair, ZSTR_VAL(key), ZSTR_LEN(key));
708+
(*pair)[ZSTR_LEN(key)] = '=';
709+
/* Copy null byte */
710+
memcpy(*pair + ZSTR_LEN(key) + 1, ZSTR_VAL(element_str), ZSTR_LEN(element_str) + 1);
699711

700712
/* Cleanup */
701-
zend_string_release_ex(key, 0);
702-
envi++;
713+
zend_string_release_ex(key, false);
714+
zend_string_release_ex(element_str, false);
703715
pair++;
704716
} ZEND_HASH_FOREACH_END();
705717
*(pair) = NULL;

ext/pcntl/pcntl.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1055,7 +1055,7 @@ function pcntl_wtermsig(int $status): int|false {}
10551055

10561056
function pcntl_wstopsig(int $status): int|false {}
10571057

1058-
function pcntl_exec(string $path, array $args = [], array $env_vars = []): bool {}
1058+
function pcntl_exec(string $path, array $args = [], array $env_vars = []): false {}
10591059

10601060
function pcntl_alarm(int $seconds): int {}
10611061

ext/pcntl/pcntl_arginfo.h

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
pcntl_exec(): Test cleanup after value that contain null bytes has been encountered for $args and $env_vars.
3+
--EXTENSIONS--
4+
pcntl
5+
--FILE--
6+
<?php
7+
try {
8+
pcntl_exec('cmd', ['-n', "value\0null\0byte"]);
9+
} catch (Throwable $e) {
10+
echo $e::class, ': ', $e->getMessage(), "\n";
11+
}
12+
13+
try {
14+
pcntl_exec(
15+
'cmd',
16+
['-n'],
17+
['var1' => 'value1', 'var2' => "value\0null\0byte"],
18+
);
19+
} catch (Throwable $e) {
20+
echo $e::class, ': ', $e->getMessage(), "\n";
21+
}
22+
23+
try {
24+
pcntl_exec(
25+
'cmd',
26+
['-n'],
27+
['var1' => 'value1', "key\0null\0byte" => "value2"],
28+
);
29+
} catch (Throwable $e) {
30+
echo $e::class, ': ', $e->getMessage(), "\n";
31+
}
32+
?>
33+
--EXPECT--
34+
ValueError: pcntl_exec(): Argument #2 ($args) individual argument must not contain null bytes
35+
ValueError: pcntl_exec(): Argument #3 ($env_vars) value for environment variable must not contain null bytes
36+
ValueError: pcntl_exec(): Argument #3 ($env_vars) name for environment variable must not contain null bytes

0 commit comments

Comments
 (0)