From 8e208dcd30bf6ca112949b135bb1c9805436561b Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 05:46:45 +0700 Subject: [PATCH 01/12] run-tests: bypassed the shell for test subprocesses --- run-tests.php | 168 +++++++++++++++++++++++--- tests/basic/req60524-win.phpt | 2 +- tests/run-test/clean_environment.phpt | 16 +++ 3 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 tests/run-test/clean_environment.phpt diff --git a/run-tests.php b/run-tests.php index 998e7e24c337..faa18cc789b5 100755 --- a/run-tests.php +++ b/run-tests.php @@ -148,7 +148,7 @@ function main(): void $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, $ignored_by_ext, $ini_overwrites, $colorize, $log_format, $no_clean, $no_file_cache, - $pass_options, $php, $php_cgi, $preload, + $pass_options, $pass_options_args, $php, $php_cgi, $preload, $result_tests_file, $slow_min_ms, $start_time, $temp_source, $temp_target, $test_cnt, $test_files, $test_idx, $test_results, $testfile, @@ -328,6 +328,7 @@ function main(): void $failed_tests_file = false; $pass_option_n = false; $pass_options = ''; + $pass_options_args = []; $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; @@ -472,11 +473,13 @@ function main(): void case 'n': if (!$pass_option_n) { $pass_options .= ' -n'; + $pass_options_args[] = '-n'; } $pass_option_n = true; break; case 'e': $pass_options .= ' -e'; + $pass_options_args[] = '-e'; break; case '--preload': $preload = true; @@ -682,8 +685,13 @@ function main(): void if ($conf_passed !== null) { if (IS_WINDOWS) { $pass_options .= " -c " . escapeshellarg($conf_passed); + $pass_options_args[] = '-c'; + $pass_options_args[] = $conf_passed; } else { - $pass_options .= " -c '" . realpath($conf_passed) . "'"; + $configurationFile = realpath($conf_passed); + $pass_options .= " -c '" . $configurationFile . "'"; + $pass_options_args[] = '-c'; + $pass_options_args[] = (string) $configurationFile; } } @@ -1173,19 +1181,20 @@ function error_report(string $testname, string $logname, string $tested): void * @return false|string */ function system_with_timeout( - string $commandline, + string|array $commandline, ?array $env = null, ?string $stdin = null, bool $captureStdIn = true, bool $captureStdOut = true, - bool $captureStdErr = true + bool $captureStdErr = true, + bool $mergeStdErr = false ) { global $valgrind; // when proc_open cmd is passed as a string (without bypass_shell=true option) the cmd goes thru shell // and on Windows quotes are discarded, this is a fix to honor the quotes and allow values containing // spaces like '"C:\Program Files\PHP\php.exe"' to be passed as 1 argument correctly - if (IS_WINDOWS) { + if (IS_WINDOWS && is_string($commandline)) { $commandline = 'start "" /b /wait ' . $commandline . ' & exit'; } @@ -1204,7 +1213,9 @@ function system_with_timeout( $descriptorspec[1] = ['pipe', 'w']; } if ($captureStdErr) { - $descriptorspec[2] = ['pipe', 'w']; + $descriptorspec[2] = $mergeStdErr + ? ['redirect', 1] + : ['pipe', 'w']; } $proc = proc_open($commandline, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); @@ -1277,6 +1288,49 @@ function system_with_timeout( return $data; } +function can_run_with_structured_test_command(TestFile $test): bool +{ + global $preload, $valgrind; + + return !$valgrind + && !$preload + && !$test->hasAnySections( + 'ARGS', + 'CAPTURE_STDIO', + 'DEFLATE_POST', + 'GZIP_POST', + 'POST', + 'POST_RAW', + 'PUT', + ); +} + +function create_structured_test_command( + string $php, + array $sapiOptionArgs, + array $passOptionArgs, + array $iniSettings, + string $testFile, + int $numRepeats +): array { + $command = [ + $php, + ...$sapiOptionArgs, + ...$passOptionArgs, + ]; + if ($numRepeats > 1) { + $command[] = '--repeat'; + $command[] = (string) $numRepeats; + } + + return [ + ...$command, + ...settings2arguments($iniSettings), + '-f', + $testFile, + ]; +} + function run_all_tests(array $test_files, array $env, ?string $redir_tested = null): void { global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $file_cache, $shuffle; @@ -1837,7 +1891,7 @@ function skip_test(string $tested, string $tested_file, string $shortname, strin function run_test(string $php, $file, array $env): string { global $log_format, $ini_overwrites, $PHP_FAILED_TESTS; - global $pass_options, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; + global $pass_options, $pass_options_args, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; global $valgrind, $temp_source, $temp_target, $cfg, $environment; global $no_clean; global $SHOW_ONLY_GROUPS; @@ -1859,6 +1913,9 @@ function run_test(string $php, $file, array $env): string $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); } + $originalPhpExecutable = $php; + $phpExecutable = $php; + $sapiOptionArgs = []; $php = escapeshellarg($php); $orig_php = $php; @@ -1930,6 +1987,8 @@ function run_test(string $php, $file, array $env): string if (!$php_cgi) { return skip_test($tested, $tested_file, $shortname, 'CGI not available'); } + $phpExecutable = $php_cgi; + $sapiOptionArgs[] = '-C'; $php = escapeshellarg($php_cgi) . ' -C '; $uses_cgi = true; if ($num_repeats > 1) { @@ -1939,13 +1998,17 @@ function run_test(string $php, $file, array $env): string /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ $extra_options = ''; + $extraOptionArgs = []; if ($test->hasSection('PHPDBG')) { if (isset($phpdbg)) { + $phpExecutable = $phpdbg; + $sapiOptionArgs[] = '-qIb'; $php = escapeshellarg($phpdbg) . ' -qIb'; // Additional phpdbg command line options for sections that need to // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. $extra_options = '-rr'; + $extraOptionArgs[] = '-rr'; } else { return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); } @@ -2099,6 +2162,7 @@ function run_test(string $php, $file, array $env): string //$ini_overwrites[] = 'setting=value'; settings2array($ini_overwrites, $ini_settings); + $orig_ini_settings_args = settings2arguments($ini_settings); $orig_ini_settings = settings2params($ini_settings); if ($file_cache !== null) { @@ -2146,6 +2210,7 @@ function run_test(string $php, $file, array $env): string } } + $testIniSettings = $ini_settings; $ini_settings = settings2params($ini_settings); $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; @@ -2156,8 +2221,6 @@ function run_test(string $php, $file, array $env): string if ($test->sectionNotEmpty('SKIPIF')) { show_file_block('skip', $test->getSection('SKIPIF')); - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; if ($valgrind) { $env['USE_ZEND_ALLOC'] = '0'; @@ -2167,7 +2230,22 @@ function run_test(string $php, $file, array $env): string $junit->startTimer($shortname); $startTime = microtime(true); - $commandLine = "$extra $php $pass_options $extra_options -q $orig_ini_settings $no_file_cache -d display_errors=1 -d display_startup_errors=0"; + $commandLine = [ + $phpExecutable, + ...$sapiOptionArgs, + ...$pass_options_args, + ...$extraOptionArgs, + '-q', + ...$orig_ini_settings_args, + '-d', + 'opcache.file_cache=', + '-d', + 'opcache.file_cache_only=0', + '-d', + 'display_errors=1', + '-d', + 'display_startup_errors=0', + ]; $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env); $time = microtime(true) - $startTime; @@ -2498,7 +2576,26 @@ function run_test(string $php, $file, array $env): string $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; - $out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr); + $useStructuredCommand = can_run_with_structured_test_command($test); + $testCommand = $useStructuredCommand + ? create_structured_test_command( + $phpExecutable, + $sapiOptionArgs, + $pass_options_args, + $testIniSettings, + $test_file, + $num_repeats, + ) + : $cmd; + $out = system_with_timeout( + $testCommand, + $env, + $stdin, + $captureStdIn, + $captureStdOut, + $captureStdErr, + $useStructuredCommand && $captureStdOut && $captureStdErr, + ); $junit->stopTimer($shortname); $hrtime = hrtime(); @@ -2520,9 +2617,27 @@ function run_test(string $php, $file, array $env): string save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean); if (!$no_clean) { - $extra = !IS_WINDOWS ? - "unset REQUEST_METHOD; unset QUERY_STRING; unset PATH_TRANSLATED; unset SCRIPT_FILENAME; unset REQUEST_METHOD;" : ""; - $clean_output = system_with_timeout("$extra $orig_php $pass_options -q $orig_ini_settings $no_file_cache \"$test_clean\"", $env); + $cleanCommand = [ + $originalPhpExecutable, + ...$pass_options_args, + '-q', + ...$orig_ini_settings_args, + '-d', + 'opcache.file_cache=', + '-d', + 'opcache.file_cache_only=0', + $test_clean, + ]; + $cleanEnv = $env; + if (!IS_WINDOWS) { + unset( + $cleanEnv['REQUEST_METHOD'], + $cleanEnv['QUERY_STRING'], + $cleanEnv['PATH_TRANSLATED'], + $cleanEnv['SCRIPT_FILENAME'], + ); + } + $clean_output = system_with_timeout($cleanCommand, $cleanEnv); } if (!$cfg['keep']['clean']) { @@ -3033,6 +3148,20 @@ function settings2params(array $ini_settings): string return $settings; } +function settings2arguments(array $ini_settings): array +{ + $arguments = []; + + foreach ($ini_settings as $name => $value) { + foreach ((array) $value as $item) { + $arguments[] = '-d'; + $arguments[] = "$name=$item"; + } + } + + return $arguments; +} + function compute_summary(): void { global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results; @@ -3612,12 +3741,12 @@ public function __construct(bool $enable, bool $keepFile) $this->keepFile = $keepFile; } - public function checkSkip(string $php, string $code, string $checkFile, string $tempFile, array $env): string + public function checkSkip(string|array $command, string $code, string $checkFile, string $tempFile, array $env): string { // Extension tests frequently use something like $dir"; + $key = (is_array($command) ? implode("\0", $command) : $command) . " => $dir"; if (isset($this->skips[$key][$code])) { $this->hits++; @@ -3628,7 +3757,12 @@ public function checkSkip(string $php, string $code, string $checkFile, string $ } save_text($checkFile, $code, $tempFile); - $result = trim(system_with_timeout("$php \"$checkFile\"", $env)); + if (is_array($command)) { + $command[] = $checkFile; + } else { + $command .= " \"$checkFile\""; + } + $result = trim(system_with_timeout($command, $env)); if (strpos($result, 'nocache') === 0) { $result = ''; } else if ($this->enable) { diff --git a/tests/basic/req60524-win.phpt b/tests/basic/req60524-win.phpt index 26fa9d9c5c7b..d75eb8c7dcf0 100644 --- a/tests/basic/req60524-win.phpt +++ b/tests/basic/req60524-win.phpt @@ -10,4 +10,4 @@ if(PHP_OS_FAMILY !== "Windows") --FILE-- --EXPECT-- -C:\\Windows +C:\Windows diff --git a/tests/run-test/clean_environment.phpt b/tests/run-test/clean_environment.phpt new file mode 100644 index 000000000000..01d1cabff61b --- /dev/null +++ b/tests/run-test/clean_environment.phpt @@ -0,0 +1,16 @@ +--TEST-- +CLEAN does not inherit request environment variables on POSIX +--FILE-- + +--CLEAN-- + +--EXPECT-- From 9601fb757b54f29feef0279fabb1e6fa108426f9 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 08:05:34 +0700 Subject: [PATCH 02/12] run-tests: preserved skip-check environment isolation --- run-tests.php | 15 ++++++++++++--- tests/run-test/clean_environment.phpt | 12 +++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/run-tests.php b/run-tests.php index faa18cc789b5..23557553e507 100755 --- a/run-tests.php +++ b/run-tests.php @@ -2221,10 +2221,19 @@ function run_test(string $php, $file, array $env): string if ($test->sectionNotEmpty('SKIPIF')) { show_file_block('skip', $test->getSection('SKIPIF')); + $skipEnv = $env; + if (!IS_WINDOWS) { + unset( + $skipEnv['REQUEST_METHOD'], + $skipEnv['QUERY_STRING'], + $skipEnv['PATH_TRANSLATED'], + $skipEnv['SCRIPT_FILENAME'], + ); + } if ($valgrind) { - $env['USE_ZEND_ALLOC'] = '0'; - $env['ZEND_DONT_UNLOAD_MODULES'] = 1; + $skipEnv['USE_ZEND_ALLOC'] = '0'; + $skipEnv['ZEND_DONT_UNLOAD_MODULES'] = 1; } $junit->startTimer($shortname); @@ -2246,7 +2255,7 @@ function run_test(string $php, $file, array $env): string '-d', 'display_startup_errors=0', ]; - $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $env); + $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $skipEnv); $time = microtime(true) - $startTime; $junit->stopTimer($shortname); diff --git a/tests/run-test/clean_environment.phpt b/tests/run-test/clean_environment.phpt index 01d1cabff61b..cf2d3c4b4f57 100644 --- a/tests/run-test/clean_environment.phpt +++ b/tests/run-test/clean_environment.phpt @@ -1,5 +1,15 @@ --TEST-- -CLEAN does not inherit request environment variables on POSIX +SKIPIF and CLEAN do not inherit request environment variables on POSIX +--SKIPIF-- + --FILE-- From 2aa847ae06eb6436d69d4952eded7b3b8b7892b3 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 18:14:33 +0700 Subject: [PATCH 03/12] review: style nit --- run-tests.php | 78 +++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/run-tests.php b/run-tests.php index 23557553e507..34045f1caabd 100755 --- a/run-tests.php +++ b/run-tests.php @@ -688,10 +688,10 @@ function main(): void $pass_options_args[] = '-c'; $pass_options_args[] = $conf_passed; } else { - $configurationFile = realpath($conf_passed); - $pass_options .= " -c '" . $configurationFile . "'"; + $configuration_file = realpath($conf_passed); + $pass_options .= " -c '" . $configuration_file . "'"; $pass_options_args[] = '-c'; - $pass_options_args[] = (string) $configurationFile; + $pass_options_args[] = (string) $configuration_file; } } @@ -1913,9 +1913,9 @@ function run_test(string $php, $file, array $env): string $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); } - $originalPhpExecutable = $php; - $phpExecutable = $php; - $sapiOptionArgs = []; + $orig_php_path = $php; + $php_path = $php; + $sapi_option_args = []; $php = escapeshellarg($php); $orig_php = $php; @@ -1987,8 +1987,8 @@ function run_test(string $php, $file, array $env): string if (!$php_cgi) { return skip_test($tested, $tested_file, $shortname, 'CGI not available'); } - $phpExecutable = $php_cgi; - $sapiOptionArgs[] = '-C'; + $php_path = $php_cgi; + $sapi_option_args[] = '-C'; $php = escapeshellarg($php_cgi) . ' -C '; $uses_cgi = true; if ($num_repeats > 1) { @@ -1998,17 +1998,17 @@ function run_test(string $php, $file, array $env): string /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ $extra_options = ''; - $extraOptionArgs = []; + $extra_option_args = []; if ($test->hasSection('PHPDBG')) { if (isset($phpdbg)) { - $phpExecutable = $phpdbg; - $sapiOptionArgs[] = '-qIb'; + $php_path = $phpdbg; + $sapi_option_args[] = '-qIb'; $php = escapeshellarg($phpdbg) . ' -qIb'; // Additional phpdbg command line options for sections that need to // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. $extra_options = '-rr'; - $extraOptionArgs[] = '-rr'; + $extra_option_args[] = '-rr'; } else { return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); } @@ -2210,7 +2210,7 @@ function run_test(string $php, $file, array $env): string } } - $testIniSettings = $ini_settings; + $test_ini_settings = $ini_settings; $ini_settings = settings2params($ini_settings); $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; @@ -2221,29 +2221,29 @@ function run_test(string $php, $file, array $env): string if ($test->sectionNotEmpty('SKIPIF')) { show_file_block('skip', $test->getSection('SKIPIF')); - $skipEnv = $env; + $skip_env = $env; if (!IS_WINDOWS) { unset( - $skipEnv['REQUEST_METHOD'], - $skipEnv['QUERY_STRING'], - $skipEnv['PATH_TRANSLATED'], - $skipEnv['SCRIPT_FILENAME'], + $skip_env['REQUEST_METHOD'], + $skip_env['QUERY_STRING'], + $skip_env['PATH_TRANSLATED'], + $skip_env['SCRIPT_FILENAME'], ); } if ($valgrind) { - $skipEnv['USE_ZEND_ALLOC'] = '0'; - $skipEnv['ZEND_DONT_UNLOAD_MODULES'] = 1; + $skip_env['USE_ZEND_ALLOC'] = '0'; + $skip_env['ZEND_DONT_UNLOAD_MODULES'] = 1; } $junit->startTimer($shortname); $startTime = microtime(true); $commandLine = [ - $phpExecutable, - ...$sapiOptionArgs, + $php_path, + ...$sapi_option_args, ...$pass_options_args, - ...$extraOptionArgs, + ...$extra_option_args, '-q', ...$orig_ini_settings_args, '-d', @@ -2255,7 +2255,7 @@ function run_test(string $php, $file, array $env): string '-d', 'display_startup_errors=0', ]; - $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $skipEnv); + $output = $skipCache->checkSkip($commandLine, $test->getSection('SKIPIF'), $test_skipif, $temp_skipif, $skip_env); $time = microtime(true) - $startTime; $junit->stopTimer($shortname); @@ -2585,25 +2585,25 @@ function run_test(string $php, $file, array $env): string $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; - $useStructuredCommand = can_run_with_structured_test_command($test); - $testCommand = $useStructuredCommand + $use_structured_command = can_run_with_structured_test_command($test); + $test_command = $use_structured_command ? create_structured_test_command( - $phpExecutable, - $sapiOptionArgs, + $php_path, + $sapi_option_args, $pass_options_args, - $testIniSettings, + $test_ini_settings, $test_file, $num_repeats, ) : $cmd; $out = system_with_timeout( - $testCommand, + $test_command, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr, - $useStructuredCommand && $captureStdOut && $captureStdErr, + $use_structured_command && $captureStdOut && $captureStdErr, ); $junit->stopTimer($shortname); @@ -2626,8 +2626,8 @@ function run_test(string $php, $file, array $env): string save_text($test_clean, trim($test->getSection('CLEAN')), $temp_clean); if (!$no_clean) { - $cleanCommand = [ - $originalPhpExecutable, + $clean_command = [ + $orig_php_path, ...$pass_options_args, '-q', ...$orig_ini_settings_args, @@ -2637,16 +2637,16 @@ function run_test(string $php, $file, array $env): string 'opcache.file_cache_only=0', $test_clean, ]; - $cleanEnv = $env; + $clean_env = $env; if (!IS_WINDOWS) { unset( - $cleanEnv['REQUEST_METHOD'], - $cleanEnv['QUERY_STRING'], - $cleanEnv['PATH_TRANSLATED'], - $cleanEnv['SCRIPT_FILENAME'], + $clean_env['REQUEST_METHOD'], + $clean_env['QUERY_STRING'], + $clean_env['PATH_TRANSLATED'], + $clean_env['SCRIPT_FILENAME'], ); } - $clean_output = system_with_timeout($cleanCommand, $cleanEnv); + $clean_output = system_with_timeout($clean_command, $clean_env); } if (!$cfg['keep']['clean']) { From 9d12242f700929eb21d3f0787f897ccff9409e78 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 18:42:18 +0700 Subject: [PATCH 04/12] review: unified shell string handling --- run-tests.php | 79 +++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/run-tests.php b/run-tests.php index 34045f1caabd..5fdcacf674d6 100755 --- a/run-tests.php +++ b/run-tests.php @@ -148,7 +148,7 @@ function main(): void $exts_skipped, $exts_tested, $exts_to_test, $failed_tests_file, $ignored_by_ext, $ini_overwrites, $colorize, $log_format, $no_clean, $no_file_cache, - $pass_options, $pass_options_args, $php, $php_cgi, $preload, + $pass_option_args, $php, $php_cgi, $preload, $result_tests_file, $slow_min_ms, $start_time, $temp_source, $temp_target, $test_cnt, $test_files, $test_idx, $test_results, $testfile, @@ -327,8 +327,7 @@ function main(): void $result_tests_file = false; $failed_tests_file = false; $pass_option_n = false; - $pass_options = ''; - $pass_options_args = []; + $pass_option_args = []; $output_file = INIT_DIR . '/php_test_results_' . date('Ymd_Hi') . '.txt'; @@ -472,14 +471,12 @@ function main(): void break; case 'n': if (!$pass_option_n) { - $pass_options .= ' -n'; - $pass_options_args[] = '-n'; + $pass_option_args[] = '-n'; } $pass_option_n = true; break; case 'e': - $pass_options .= ' -e'; - $pass_options_args[] = '-e'; + $pass_option_args[] = '-e'; break; case '--preload': $preload = true; @@ -684,14 +681,12 @@ function main(): void if ($conf_passed !== null) { if (IS_WINDOWS) { - $pass_options .= " -c " . escapeshellarg($conf_passed); - $pass_options_args[] = '-c'; - $pass_options_args[] = $conf_passed; + $pass_option_args[] = '-c'; + $pass_option_args[] = $conf_passed; } else { $configuration_file = realpath($conf_passed); - $pass_options .= " -c '" . $configuration_file . "'"; - $pass_options_args[] = '-c'; - $pass_options_args[] = (string) $configuration_file; + $pass_option_args[] = '-c'; + $pass_option_args[] = (string) $configuration_file; } } @@ -824,8 +819,9 @@ function verify_config(string $php): void */ function write_information(array $user_tests, $phpdbg): void { - global $php, $php_cgi, $php_info, $ini_overwrites, $pass_options, $exts_to_test, $valgrind, $no_file_cache; + global $php, $php_cgi, $php_info, $ini_overwrites, $pass_option_args, $exts_to_test, $valgrind, $no_file_cache; $php_escaped = escapeshellarg($php); + $escaped_pass_options = escaped_shell_string_from($pass_option_args); // Get info from php $info_file = __DIR__ . '/run-test-info.php'; @@ -841,12 +837,12 @@ function write_information(array $user_tests, $phpdbg): void $info_params = []; settings2array($ini_overwrites, $info_params); $info_params = settings2params($info_params); - $php_info = shell_exec("$php_escaped $pass_options $info_params $no_file_cache \"$info_file\""); + $php_info = shell_exec("$php_escaped $escaped_pass_options $info_params $no_file_cache \"$info_file\""); define('TESTED_PHP_VERSION', shell_exec("$php_escaped -n -r \"echo PHP_VERSION;\"")); if ($php_cgi && $php != $php_cgi) { $php_cgi_escaped = escapeshellarg($php_cgi); - $php_info_cgi = shell_exec("$php_cgi_escaped $pass_options $info_params $no_file_cache -q \"$info_file\""); + $php_info_cgi = shell_exec("$php_cgi_escaped $escaped_pass_options $info_params $no_file_cache -q \"$info_file\""); $php_info_sep = "\n---------------------------------------------------------------------"; $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; } else { @@ -855,7 +851,7 @@ function write_information(array $user_tests, $phpdbg): void if ($phpdbg) { $phpdbg_escaped = escapeshellarg($phpdbg); - $phpdbg_info = shell_exec("$phpdbg_escaped $pass_options $info_params $no_file_cache -qrr \"$info_file\""); + $phpdbg_info = shell_exec("$phpdbg_escaped $escaped_pass_options $info_params $no_file_cache -qrr \"$info_file\""); $php_info_sep = "\n---------------------------------------------------------------------"; $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; } else { @@ -881,7 +877,7 @@ function write_information(array $user_tests, $phpdbg): void } echo implode(',', $exts); PHP); - $extensionsNames = explode(',', shell_exec("$php_escaped $pass_options $info_params $no_file_cache \"$info_file\"")); + $extensionsNames = explode(',', shell_exec("$php_escaped $escaped_pass_options $info_params $no_file_cache \"$info_file\"")); $exts_to_test = array_unique(remap_loaded_extensions_names($extensionsNames)); // check for extensions that need special handling and regenerate $info_params_ex = [ @@ -1891,7 +1887,7 @@ function skip_test(string $tested, string $tested_file, string $shortname, strin function run_test(string $php, $file, array $env): string { global $log_format, $ini_overwrites, $PHP_FAILED_TESTS; - global $pass_options, $pass_options_args, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; + global $pass_option_args, $DETAILED, $IN_REDIRECT, $test_cnt, $test_idx; global $valgrind, $temp_source, $temp_target, $cfg, $environment; global $no_clean; global $SHOW_ONLY_GROUPS; @@ -1913,11 +1909,11 @@ function run_test(string $php, $file, array $env): string $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); } + $escaped_pass_options = escaped_shell_string_from($pass_option_args); $orig_php_path = $php; $php_path = $php; $sapi_option_args = []; - $php = escapeshellarg($php); - $orig_php = $php; + $orig_php = escaped_shell_string_from([$orig_php_path]); $retried = false; retry: @@ -1989,7 +1985,6 @@ function run_test(string $php, $file, array $env): string } $php_path = $php_cgi; $sapi_option_args[] = '-C'; - $php = escapeshellarg($php_cgi) . ' -C '; $uses_cgi = true; if ($num_repeats > 1) { return skip_test($tested, $tested_file, $shortname, 'CGI does not support --repeat'); @@ -1997,17 +1992,14 @@ function run_test(string $php, $file, array $env): string } /* For phpdbg tests, check if phpdbg sapi is available and if it is, use it. */ - $extra_options = ''; $extra_option_args = []; if ($test->hasSection('PHPDBG')) { if (isset($phpdbg)) { $php_path = $phpdbg; - $sapi_option_args[] = '-qIb'; - $php = escapeshellarg($phpdbg) . ' -qIb'; + $sapi_option_args = ['-qIb']; // Additional phpdbg command line options for sections that need to // be run straight away. For example, EXTENSIONS, SKIPIF, CLEAN. - $extra_options = '-rr'; $extra_option_args[] = '-rr'; } else { return skip_test($tested, $tested_file, $shortname, 'phpdbg not available'); @@ -2017,6 +2009,9 @@ function run_test(string $php, $file, array $env): string } } + $php = escaped_shell_string_from([$php_path, ...$sapi_option_args]); + $extra_options = escaped_shell_string_from($extra_option_args); + foreach (['CLEAN', 'STDIN', 'CAPTURE_STDIO'] as $section) { if ($test->hasSection($section)) { if ($num_repeats > 1) { @@ -2134,7 +2129,7 @@ function run_test(string $php, $file, array $env): string $ext_params = []; settings2array($ini_overwrites, $ext_params); $ext_params = settings2params($ext_params); - [$ext_dir, $loaded] = $skipCache->getExtensions("$orig_php $pass_options $extra_options $ext_params $no_file_cache"); + [$ext_dir, $loaded] = $skipCache->getExtensions("$orig_php $escaped_pass_options $extra_options $ext_params $no_file_cache"); $ext_prefix = IS_WINDOWS ? "php_" : ""; $missing = []; foreach ($extensions as $req_ext) { @@ -2213,7 +2208,7 @@ function run_test(string $php, $file, array $env): string $test_ini_settings = $ini_settings; $ini_settings = settings2params($ini_settings); - $env['TEST_PHP_EXTRA_ARGS'] = $pass_options . ' ' . $ini_settings; + $env['TEST_PHP_EXTRA_ARGS'] = $escaped_pass_options . ' ' . $ini_settings; // Check if test should be skipped. $info = ''; @@ -2242,7 +2237,7 @@ function run_test(string $php, $file, array $env): string $commandLine = [ $php_path, ...$sapi_option_args, - ...$pass_options_args, + ...$pass_option_args, ...$extra_option_args, '-q', ...$orig_ini_settings_args, @@ -2429,10 +2424,7 @@ function run_test(string $php, $file, array $env): string if ($preload && !empty($test_file)) { save_text($preload_filename, "sectionNotEmpty('POST_RAW')) { @@ -2467,7 +2459,7 @@ function run_test(string $php, $file, array $env): string } save_text($tmp_post, $request); - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; + $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('PUT')) { $post = trim($test->getSection('PUT')); $raw_lines = explode("\n", $post); @@ -2498,7 +2490,7 @@ function run_test(string $php, $file, array $env): string } save_text($tmp_post, $request); - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; + $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('POST')) { $post = trim($test->getSection('POST')); $content_length = strlen($post); @@ -2513,7 +2505,7 @@ function run_test(string $php, $file, array $env): string $env['CONTENT_LENGTH'] = $content_length; } - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; + $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('GZIP_POST')) { $post = trim($test->getSection('GZIP_POST')); $post = gzencode($post, 9, FORCE_GZIP); @@ -2526,7 +2518,7 @@ function run_test(string $php, $file, array $env): string $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; $env['CONTENT_LENGTH'] = $content_length; - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; + $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('DEFLATE_POST')) { $post = trim($test->getSection('DEFLATE_POST')); $post = gzcompress($post, 9); @@ -2538,14 +2530,14 @@ function run_test(string $php, $file, array $env): string $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; $env['CONTENT_LENGTH'] = $content_length; - $cmd = "$php $pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; + $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } else { $env['REQUEST_METHOD'] = 'GET'; $env['CONTENT_TYPE'] = ''; $env['CONTENT_LENGTH'] = ''; $repeat_option = $num_repeats > 1 ? "--repeat $num_repeats" : ""; - $cmd = "$php $pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect"; + $cmd = "$php $escaped_pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect"; } $orig_cmd = $cmd; @@ -2590,7 +2582,7 @@ function run_test(string $php, $file, array $env): string ? create_structured_test_command( $php_path, $sapi_option_args, - $pass_options_args, + $pass_option_args, $test_ini_settings, $test_file, $num_repeats, @@ -2628,7 +2620,7 @@ function run_test(string $php, $file, array $env): string if (!$no_clean) { $clean_command = [ $orig_php_path, - ...$pass_options_args, + ...$pass_option_args, '-q', ...$orig_ini_settings_args, '-d', @@ -3171,6 +3163,11 @@ function settings2arguments(array $ini_settings): array return $arguments; } +function escaped_shell_string_from(array $arguments): string +{ + return implode(' ', array_map(escapeshellarg(...), $arguments)); +} + function compute_summary(): void { global $n_total, $test_results, $ignored_by_ext, $sum_results, $percent_results; From 877a33d5d15b06c23ad10d551517f51764f96ad9 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 21:22:48 +0700 Subject: [PATCH 05/12] review: switched to structured commands --- run-tests.php | 216 ++++++++++++++++++++++++-------------------------- 1 file changed, 104 insertions(+), 112 deletions(-) diff --git a/run-tests.php b/run-tests.php index 5fdcacf674d6..846f8b99cf77 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1177,9 +1177,10 @@ function error_report(string $testname, string $logname, string $tested): void * @return false|string */ function system_with_timeout( - string|array $commandline, + array $command, ?array $env = null, ?string $stdin = null, + ?string $stdinFile = null, bool $captureStdIn = true, bool $captureStdOut = true, bool $captureStdErr = true, @@ -1187,13 +1188,6 @@ function system_with_timeout( ) { global $valgrind; - // when proc_open cmd is passed as a string (without bypass_shell=true option) the cmd goes thru shell - // and on Windows quotes are discarded, this is a fix to honor the quotes and allow values containing - // spaces like '"C:\Program Files\PHP\php.exe"' to be passed as 1 argument correctly - if (IS_WINDOWS && is_string($commandline)) { - $commandline = 'start "" /b /wait ' . $commandline . ' & exit'; - } - $data = ''; $bin_env = []; @@ -1202,7 +1196,9 @@ function system_with_timeout( } $descriptorspec = []; - if ($captureStdIn) { + if ($stdinFile !== null) { + $descriptorspec[0] = ['file', $stdinFile, 'r']; + } elseif ($captureStdIn) { $descriptorspec[0] = ['pipe', 'r']; } if ($captureStdOut) { @@ -1213,13 +1209,13 @@ function system_with_timeout( ? ['redirect', 1] : ['pipe', 'w']; } - $proc = proc_open($commandline, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); + $proc = proc_open($command, $descriptorspec, $pipes, TEST_PHP_SRCDIR, $bin_env, ['suppress_errors' => true]); if (!$proc) { return false; } - if ($captureStdIn) { + if (isset($pipes[0])) { if (!is_null($stdin)) { fwrite($pipes[0], $stdin); } @@ -1284,36 +1280,14 @@ function system_with_timeout( return $data; } -function can_run_with_structured_test_command(TestFile $test): bool -{ - global $preload, $valgrind; - - return !$valgrind - && !$preload - && !$test->hasAnySections( - 'ARGS', - 'CAPTURE_STDIO', - 'DEFLATE_POST', - 'GZIP_POST', - 'POST', - 'POST_RAW', - 'PUT', - ); -} - -function create_structured_test_command( +function create_test_command( string $php, - array $sapiOptionArgs, - array $passOptionArgs, + array $optionArgs, array $iniSettings, string $testFile, int $numRepeats ): array { - $command = [ - $php, - ...$sapiOptionArgs, - ...$passOptionArgs, - ]; + $command = [$php, ...$optionArgs]; if ($numRepeats > 1) { $command[] = '--repeat'; $command[] = (string) $numRepeats; @@ -1327,6 +1301,15 @@ function create_structured_test_command( ]; } +function create_shell_invocation(string $command): array +{ + if (IS_WINDOWS) { + return [getenv('COMSPEC') ?: 'cmd.exe', '/d', '/s', '/c', $command]; + } + + return ['/bin/sh', '-c', "exec $command"]; +} + function run_all_tests(array $test_files, array $env, ?string $redir_tested = null): void { global $test_results, $failed_tests_file, $result_tests_file, $php, $test_idx, $file_cache, $shuffle; @@ -1909,11 +1892,8 @@ function run_test(string $php, $file, array $env): string $skipCache = new SkipCache($enableSkipCache, $cfg['keep']['skip']); } - $escaped_pass_options = escaped_shell_string_from($pass_option_args); - $orig_php_path = $php; $php_path = $php; $sapi_option_args = []; - $orig_php = escaped_shell_string_from([$orig_php_path]); $retried = false; retry: @@ -1971,12 +1951,6 @@ function run_test(string $php, $file, array $env): string $captureStdOut = true; $captureStdErr = true; } - if ($captureStdOut && $captureStdErr) { - $cmdRedirect = ' 2>&1'; - } else { - $cmdRedirect = ''; - } - /* For GET/POST/PUT tests, check if cgi sapi is available and if it is, use it. */ $uses_cgi = false; if ($test->isCGI()) { @@ -2009,9 +1983,6 @@ function run_test(string $php, $file, array $env): string } } - $php = escaped_shell_string_from([$php_path, ...$sapi_option_args]); - $extra_options = escaped_shell_string_from($extra_option_args); - foreach (['CLEAN', 'STDIN', 'CAPTURE_STDIO'] as $section) { if ($test->hasSection($section)) { if ($num_repeats > 1) { @@ -2129,7 +2100,12 @@ function run_test(string $php, $file, array $env): string $ext_params = []; settings2array($ini_overwrites, $ext_params); $ext_params = settings2params($ext_params); - [$ext_dir, $loaded] = $skipCache->getExtensions("$orig_php $escaped_pass_options $extra_options $ext_params $no_file_cache"); + $extension_command = escaped_shell_string_from([ + $php, + ...$pass_option_args, + ...$extra_option_args, + ]) . " $ext_params $no_file_cache"; + [$ext_dir, $loaded] = $skipCache->getExtensions($extension_command); $ext_prefix = IS_WINDOWS ? "php_" : ""; $missing = []; foreach ($extensions as $req_ext) { @@ -2158,8 +2134,6 @@ function run_test(string $php, $file, array $env): string settings2array($ini_overwrites, $ini_settings); $orig_ini_settings_args = settings2arguments($ini_settings); - $orig_ini_settings = settings2params($ini_settings); - if ($file_cache !== null) { $ini_settings['opcache.file_cache'] = get_file_cache_dir(); // Make sure warnings still show up on the second run. @@ -2208,6 +2182,7 @@ function run_test(string $php, $file, array $env): string $test_ini_settings = $ini_settings; $ini_settings = settings2params($ini_settings); + $escaped_pass_options = escaped_shell_string_from($pass_option_args); $env['TEST_PHP_EXTRA_ARGS'] = $escaped_pass_options . ' ' . $ini_settings; // Check if test should be skipped. @@ -2420,13 +2395,18 @@ function run_test(string $php, $file, array $env): string $env['HTTP_COOKIE'] = ''; } - $args = $test->hasSection('ARGS') ? ' -- ' . $test->getSection('ARGS') : ''; - + $test_option_args = [ + ...$sapi_option_args, + ...$pass_option_args, + ]; if ($preload && !empty($test_file)) { save_text($preload_filename, "hasSection('STDIN') ? $test->getSection('STDIN') : null; + $request = null; if ($test->sectionNotEmpty('POST_RAW')) { $post = trim($test->getSection('POST_RAW')); $raw_lines = explode("\n", $post); @@ -2457,9 +2437,6 @@ function run_test(string $php, $file, array $env): string $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); return 'BORKED'; } - - save_text($tmp_post, $request); - $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('PUT')) { $post = trim($test->getSection('PUT')); $raw_lines = explode("\n", $post); @@ -2488,13 +2465,9 @@ function run_test(string $php, $file, array $env): string $junit->markTestAs('BORK', $shortname, $tested, null, 'empty $request'); return 'BORKED'; } - - save_text($tmp_post, $request); - $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('POST')) { - $post = trim($test->getSection('POST')); - $content_length = strlen($post); - save_text($tmp_post, $post); + $request = trim($test->getSection('POST')); + $content_length = strlen($request); $env['REQUEST_METHOD'] = 'POST'; if (empty($env['CONTENT_TYPE'])) { @@ -2504,48 +2477,67 @@ function run_test(string $php, $file, array $env): string if (empty($env['CONTENT_LENGTH'])) { $env['CONTENT_LENGTH'] = $content_length; } - - $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('GZIP_POST')) { - $post = trim($test->getSection('GZIP_POST')); - $post = gzencode($post, 9, FORCE_GZIP); + $request = trim($test->getSection('GZIP_POST')); + $request = gzencode($request, 9, FORCE_GZIP); $env['HTTP_CONTENT_ENCODING'] = 'gzip'; - save_text($tmp_post, $post); - $content_length = strlen($post); + $content_length = strlen($request); $env['REQUEST_METHOD'] = 'POST'; $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; $env['CONTENT_LENGTH'] = $content_length; - - $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } elseif ($test->sectionNotEmpty('DEFLATE_POST')) { - $post = trim($test->getSection('DEFLATE_POST')); - $post = gzcompress($post, 9); + $request = trim($test->getSection('DEFLATE_POST')); + $request = gzcompress($request, 9); $env['HTTP_CONTENT_ENCODING'] = 'deflate'; - save_text($tmp_post, $post); - $content_length = strlen($post); + $content_length = strlen($request); $env['REQUEST_METHOD'] = 'POST'; $env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; $env['CONTENT_LENGTH'] = $content_length; - - $cmd = "$php $escaped_pass_options $ini_settings -f \"$test_file\"$cmdRedirect < \"$tmp_post\""; } else { $env['REQUEST_METHOD'] = 'GET'; $env['CONTENT_TYPE'] = ''; $env['CONTENT_LENGTH'] = ''; + } - $repeat_option = $num_repeats > 1 ? "--repeat $num_repeats" : ""; - $cmd = "$php $escaped_pass_options $repeat_option $ini_settings -f \"$test_file\" $args$cmdRedirect"; + $request_file = null; + if ($request !== null) { + // A file descriptor avoids blocking while writing large request bodies to a pipe. + save_text($tmp_post, $request); + $request_file = $tmp_post; + } + + $test_command = create_test_command( + $php_path, + $test_option_args, + $test_ini_settings, + $test_file, + $num_repeats, + ); + $orig_cmd = escaped_shell_string_from($test_command); + if ($test->hasSection('ARGS')) { + // Preserve the existing shell parsing of the raw ARGS section. + $orig_cmd .= ' -- ' . $test->getSection('ARGS'); + $test_command = create_shell_invocation($orig_cmd); + } + if ($request_file !== null) { + $orig_cmd .= ' < ' . escapeshellarg($request_file); + } + if ($captureStdOut && $captureStdErr) { + $orig_cmd .= ' 2>&1'; } - $orig_cmd = $cmd; if ($valgrind) { $env['USE_ZEND_ALLOC'] = '0'; $env['ZEND_DONT_UNLOAD_MODULES'] = 1; - $cmd = $valgrind->wrapCommand($cmd, $memcheck_filename, strpos($test_file, "pcre") !== false); + $test_command = $valgrind->wrapCommand( + $test_command, + $memcheck_filename, + strpos($test_file, "pcre") !== false, + ); } if ($test->hasSection('XLEAK')) { @@ -2559,6 +2551,7 @@ function run_test(string $php, $file, array $env): string } if ($DETAILED) { + $display_command = escaped_shell_string_from($test_command); echo " CONTENT_LENGTH = " . $env['CONTENT_LENGTH'] . " CONTENT_TYPE = " . $env['CONTENT_TYPE'] . " @@ -2568,7 +2561,7 @@ function run_test(string $php, $file, array $env): string REQUEST_METHOD = " . $env['REQUEST_METHOD'] . " SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . " HTTP_COOKIE = " . $env['HTTP_COOKIE'] . " -COMMAND $cmd +COMMAND $display_command "; } @@ -2576,26 +2569,15 @@ function run_test(string $php, $file, array $env): string $hrtime = hrtime(); $startTime = $hrtime[0] * 1000000000 + $hrtime[1]; - $stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null; - $use_structured_command = can_run_with_structured_test_command($test); - $test_command = $use_structured_command - ? create_structured_test_command( - $php_path, - $sapi_option_args, - $pass_option_args, - $test_ini_settings, - $test_file, - $num_repeats, - ) - : $cmd; $out = system_with_timeout( $test_command, $env, $stdin, + $request_file, $captureStdIn, $captureStdOut, $captureStdErr, - $use_structured_command && $captureStdOut && $captureStdErr, + $captureStdOut && $captureStdErr, ); $junit->stopTimer($shortname); @@ -2619,7 +2601,7 @@ function run_test(string $php, $file, array $env): string if (!$no_clean) { $clean_command = [ - $orig_php_path, + $php, ...$pass_option_args, '-q', ...$orig_ini_settings_args, @@ -3747,12 +3729,12 @@ public function __construct(bool $enable, bool $keepFile) $this->keepFile = $keepFile; } - public function checkSkip(string|array $command, string $code, string $checkFile, string $tempFile, array $env): string + public function checkSkip(array $command, string $code, string $checkFile, string $tempFile, array $env): string { // Extension tests frequently use something like $dir"; + $key = implode("\0", $command) . " => $dir"; if (isset($this->skips[$key][$code])) { $this->hits++; @@ -3763,11 +3745,7 @@ public function checkSkip(string|array $command, string $code, string $checkFile } save_text($checkFile, $code, $tempFile); - if (is_array($command)) { - $command[] = $checkFile; - } else { - $command .= " \"$checkFile\""; - } + $command[] = $checkFile; $result = trim(system_with_timeout($command, $env)); if (strpos($result, 'nocache') === 0) { $result = ''; @@ -3816,7 +3794,11 @@ public function getHeader(): string public function __construct(array $environment, string $tool = 'memcheck') { $this->tool = $tool; - $header = system_with_timeout("valgrind --tool={$this->tool} --version", $environment); + $header = system_with_timeout([ + 'valgrind', + "--tool={$this->tool}", + '--version', + ], $environment); if (!$header) { error("Valgrind returned no version info for {$this->tool}, cannot proceed.\n". "Please check if Valgrind is installed and the tool is named correctly."); @@ -3830,18 +3812,28 @@ public function __construct(array $environment, string $tool = 'memcheck') $this->version_3_8_0 = version_compare($version, '3.8.0', '>='); } - public function wrapCommand(string $cmd, string $memcheck_filename, bool $check_all): string + public function wrapCommand(array $command, string $memcheck_filename, bool $check_all): array { - $vcmd = "valgrind -q --tool={$this->tool} --trace-children=yes"; + $valgrind_arguments = [ + 'valgrind', + '-q', + "--tool={$this->tool}", + '--trace-children=yes', + ]; + if ($check_all) { - $vcmd .= ' --smc-check=all'; + $valgrind_arguments[] = '--smc-check=all'; } - /* --vex-iropt-register-updates=allregs-at-mem-access is necessary for phpdbg watchpoint tests */ - if ($this->version_3_8_0) { - return "$vcmd --vex-iropt-register-updates=allregs-at-mem-access --log-file=$memcheck_filename $cmd"; - } - return "$vcmd --vex-iropt-precise-memory-exns=yes --log-file=$memcheck_filename $cmd"; + $valgrind_arguments[] = $this->version_3_8_0 + ? '--vex-iropt-register-updates=allregs-at-mem-access' // necessary for phpdbg watchpoint tests + : '--vex-iropt-precise-memory-exns=yes'; + + return [ + ...$valgrind_arguments, + "--log-file=$memcheck_filename", + ...$command, + ]; } } From 3a217643e58f64902bf53a8b48b05a5a7dbbe468 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 22:55:39 +0700 Subject: [PATCH 06/12] review: var naming nit --- run-tests.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run-tests.php b/run-tests.php index 846f8b99cf77..8c22e86c3abf 100755 --- a/run-tests.php +++ b/run-tests.php @@ -2551,7 +2551,7 @@ function run_test(string $php, $file, array $env): string } if ($DETAILED) { - $display_command = escaped_shell_string_from($test_command); + $orig_cmd = escaped_shell_string_from($test_command); echo " CONTENT_LENGTH = " . $env['CONTENT_LENGTH'] . " CONTENT_TYPE = " . $env['CONTENT_TYPE'] . " @@ -2561,7 +2561,7 @@ function run_test(string $php, $file, array $env): string REQUEST_METHOD = " . $env['REQUEST_METHOD'] . " SCRIPT_FILENAME = " . $env['SCRIPT_FILENAME'] . " HTTP_COOKIE = " . $env['HTTP_COOKIE'] . " -COMMAND $display_command +COMMAND $orig_cmd "; } From 63196df06dd0fc38677b560b6a01141932b69706 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 23:28:27 +0700 Subject: [PATCH 07/12] review: switched to structured arguments --- run-tests.php | 51 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/run-tests.php b/run-tests.php index 8c22e86c3abf..9d39ffd56817 100755 --- a/run-tests.php +++ b/run-tests.php @@ -306,7 +306,10 @@ function main(): void 'date.timezone=UTC', ]; - $no_file_cache = '-d opcache.file_cache= -d opcache.file_cache_only=0'; + $no_file_cache = [ + '-d', 'opcache.file_cache=', + '-d', 'opcache.file_cache_only=0', + ]; // Determine the tests to be run. @@ -822,6 +825,7 @@ function write_information(array $user_tests, $phpdbg): void global $php, $php_cgi, $php_info, $ini_overwrites, $pass_option_args, $exts_to_test, $valgrind, $no_file_cache; $php_escaped = escapeshellarg($php); $escaped_pass_options = escaped_shell_string_from($pass_option_args); + $escaped_no_file_cache = escaped_shell_string_from($no_file_cache); // Get info from php $info_file = __DIR__ . '/run-test-info.php'; @@ -837,12 +841,12 @@ function write_information(array $user_tests, $phpdbg): void $info_params = []; settings2array($ini_overwrites, $info_params); $info_params = settings2params($info_params); - $php_info = shell_exec("$php_escaped $escaped_pass_options $info_params $no_file_cache \"$info_file\""); + $php_info = shell_exec("$php_escaped $escaped_pass_options $info_params $escaped_no_file_cache \"$info_file\""); define('TESTED_PHP_VERSION', shell_exec("$php_escaped -n -r \"echo PHP_VERSION;\"")); if ($php_cgi && $php != $php_cgi) { $php_cgi_escaped = escapeshellarg($php_cgi); - $php_info_cgi = shell_exec("$php_cgi_escaped $escaped_pass_options $info_params $no_file_cache -q \"$info_file\""); + $php_info_cgi = shell_exec("$php_cgi_escaped $escaped_pass_options $info_params $escaped_no_file_cache -q \"$info_file\""); $php_info_sep = "\n---------------------------------------------------------------------"; $php_cgi_info = "$php_info_sep\nPHP : $php_cgi $php_info_cgi$php_info_sep"; } else { @@ -851,7 +855,7 @@ function write_information(array $user_tests, $phpdbg): void if ($phpdbg) { $phpdbg_escaped = escapeshellarg($phpdbg); - $phpdbg_info = shell_exec("$phpdbg_escaped $escaped_pass_options $info_params $no_file_cache -qrr \"$info_file\""); + $phpdbg_info = shell_exec("$phpdbg_escaped $escaped_pass_options $info_params $escaped_no_file_cache -qrr \"$info_file\""); $php_info_sep = "\n---------------------------------------------------------------------"; $phpdbg_info = "$php_info_sep\nPHP : $phpdbg $phpdbg_info$php_info_sep"; } else { @@ -877,7 +881,7 @@ function write_information(array $user_tests, $phpdbg): void } echo implode(',', $exts); PHP); - $extensionsNames = explode(',', shell_exec("$php_escaped $escaped_pass_options $info_params $no_file_cache \"$info_file\"")); + $extensionsNames = explode(',', shell_exec("$php_escaped $escaped_pass_options $info_params $escaped_no_file_cache \"$info_file\"")); $exts_to_test = array_unique(remap_loaded_extensions_names($extensionsNames)); // check for extensions that need special handling and regenerate $info_params_ex = [ @@ -2099,13 +2103,16 @@ function run_test(string $php, $file, array $env): string if ($extensions != []) { $ext_params = []; settings2array($ini_overwrites, $ext_params); - $ext_params = settings2params($ext_params); - $extension_command = escaped_shell_string_from([ + $ext_params = settings2arguments($ext_params); + + [$ext_dir, $loaded] = $skipCache->getExtensions([ $php, ...$pass_option_args, ...$extra_option_args, - ]) . " $ext_params $no_file_cache"; - [$ext_dir, $loaded] = $skipCache->getExtensions($extension_command); + ...$ext_params, + ...$no_file_cache, + ]); + $ext_prefix = IS_WINDOWS ? "php_" : ""; $missing = []; foreach ($extensions as $req_ext) { @@ -3761,19 +3768,33 @@ public function checkSkip(array $command, string $code, string $checkFile, strin return $result; } - public function getExtensions(string $php): array + public function getExtensions(array $command): array { - if (isset($this->extensions[$php])) { + $key = implode("\0", $command); + if (isset($this->extensions[$key])) { $this->extHits++; - return $this->extensions[$php]; + return $this->extensions[$key]; } - $extDir = shell_exec("$php -d display_errors=0 -r \"echo ini_get('extension_dir');\""); - $extensionsNames = explode(",", shell_exec("$php -d display_errors=0 -r \"echo implode(',', get_loaded_extensions());\"")); + $output = shell_exec(escaped_shell_string_from([ + ...$command, + '-d', + 'display_errors=0', + '-r', + 'echo ini_get("extension_dir"), "\0", implode(",", get_loaded_extensions());', + ])); + + if (!is_string($output) || !str_contains($output, "\0")) { + error("Unable to query loaded PHP extensions."); + } + + [$extDir, $extensionsNames] = explode("\0", $output, 2); + + $extensionsNames = explode(",", $extensionsNames); $extensions = remap_loaded_extensions_names($extensionsNames); $result = [$extDir, $extensions]; - $this->extensions[$php] = $result; + $this->extensions[$key] = $result; $this->extMisses++; return $result; From c36e6648dbfa5b12bbaafa9efcc03086ec0fe15c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 23:41:51 +0700 Subject: [PATCH 08/12] fix: restored Windows `--ARGS--` shell handling --- run-tests.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/run-tests.php b/run-tests.php index 9d39ffd56817..fb2cad26b896 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1181,7 +1181,7 @@ function error_report(string $testname, string $logname, string $tested): void * @return false|string */ function system_with_timeout( - array $command, + array|string $command, ?array $env = null, ?string $stdin = null, ?string $stdinFile = null, @@ -1192,6 +1192,10 @@ function system_with_timeout( ) { global $valgrind; + if (IS_WINDOWS && is_string($command)) { + $command = 'start "" /b /wait ' . $command . ' & exit'; + } + $data = ''; $bin_env = []; @@ -1305,10 +1309,10 @@ function create_test_command( ]; } -function create_shell_invocation(string $command): array +function create_shell_invocation(string $command): array|string { if (IS_WINDOWS) { - return [getenv('COMSPEC') ?: 'cmd.exe', '/d', '/s', '/c', $command]; + return $command; } return ['/bin/sh', '-c', "exec $command"]; From 6d606d6a0bfdf4d1e814c84501c0d4f0491e9622 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Fri, 31 Jul 2026 23:52:52 +0700 Subject: [PATCH 09/12] review: added NEWS entry --- NEWS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS b/NEWS index 4364d69650e5..e08d6efc0a09 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.6.0beta1 +- Core: + . Changed run-tests.php to run test subprocesses without a shell where + possible. (NickSdot) + - GMP: . Added optional $definitely_prime output parameter to gmp_prevprime(). (Weilin Du) From af77880889c58f961d20dbad285aa42fce95243c Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 1 Aug 2026 02:21:16 +0700 Subject: [PATCH 10/12] ci: fixed windows quoting --- run-tests.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/run-tests.php b/run-tests.php index fb2cad26b896..961fdfe66344 100755 --- a/run-tests.php +++ b/run-tests.php @@ -3780,13 +3780,17 @@ public function getExtensions(array $command): array return $this->extensions[$key]; } - $output = shell_exec(escaped_shell_string_from([ + $probeCommand = escaped_shell_string_from([ ...$command, '-d', 'display_errors=0', '-r', - 'echo ini_get("extension_dir"), "\0", implode(",", get_loaded_extensions());', - ])); + ]); + + $output = shell_exec( + $probeCommand + . ' "echo ini_get(\'extension_dir\'), chr(0), implode(\',\', get_loaded_extensions());"' + ); if (!is_string($output) || !str_contains($output, "\0")) { error("Unable to query loaded PHP extensions."); From fcf9d3cc3b4dbc0dfd6f77ddbe99fa162c16e435 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 1 Aug 2026 03:23:50 +0700 Subject: [PATCH 11/12] chore: added comment back --- run-tests.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run-tests.php b/run-tests.php index 961fdfe66344..6331d078a59e 100755 --- a/run-tests.php +++ b/run-tests.php @@ -1192,6 +1192,9 @@ function system_with_timeout( ) { global $valgrind; + // when proc_open cmd is passed as a string (without bypass_shell=true option) the cmd goes thru shell + // and on Windows quotes are discarded, this is a fix to honor the quotes and allow values containing + // spaces like '"C:\Program Files\PHP\php.exe"' to be passed as 1 argument correctly if (IS_WINDOWS && is_string($command)) { $command = 'start "" /b /wait ' . $command . ' & exit'; } From 074aabab41dda5b2f83881066dd855b5d1583400 Mon Sep 17 00:00:00 2001 From: NickSdot Date: Sat, 1 Aug 2026 04:07:03 +0700 Subject: [PATCH 12/12] fix: verbose output --- run-tests.php | 1 - 1 file changed, 1 deletion(-) diff --git a/run-tests.php b/run-tests.php index 6331d078a59e..703dd643ca7c 100755 --- a/run-tests.php +++ b/run-tests.php @@ -2565,7 +2565,6 @@ function run_test(string $php, $file, array $env): string } if ($DETAILED) { - $orig_cmd = escaped_shell_string_from($test_command); echo " CONTENT_LENGTH = " . $env['CONTENT_LENGTH'] . " CONTENT_TYPE = " . $env['CONTENT_TYPE'] . "