Skip to content

Commit 6285bb5

Browse files
committed
Support redirect+null descriptors in proc_open
This adds support for doing something like: [1 => ['pipe', 'w'], 2 => ['redirect', 1]] This will make descriptor 2 on the child end a dup'd descriptor 1. This is mainly useful in conjunction with shell-less mode, because we don't have an easy way to do "2>&1" there. Additionally we support: [1 => ['pipe', 'w'], 2 => ['null']] Which would be the same as a >/dev/null or >nul redirect, depending on platform.
1 parent 42cac9d commit 6285bb5

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

UPGRADING

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,13 @@ PHP 7.4 UPGRADE NOTES
304304

305305
proc_open(['php', '-r', 'echo "Hello World\n";'], $descriptors, $pipes);
306306

307+
. proc_open() now supports "redirect" and "null" descriptors. For example:
308+
309+
// Like 2>&1 on the shell
310+
proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['redirect', 1]], $pipes);
311+
// Like 2>/dev/null or 2>nul on the shell
312+
proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['null']], $pipes);
313+
307314
. password_hash() has argon2i(d) implementations from ext/sodium when PHP is
308315
built without libargon.
309316

ext/standard/proc_open.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ static inline HANDLE dup_fd_as_handle(int fd)
390390

391391
#define DESC_PIPE 1
392392
#define DESC_FILE 2
393+
#define DESC_REDIRECT 3
393394
#define DESC_PARENT_MODE_WRITE 8
394395

395396
struct php_proc_open_descriptor_item {
@@ -760,6 +761,85 @@ PHP_FUNCTION(proc_open)
760761
#else
761762
descriptors[ndesc].childend = fd;
762763
#endif
764+
} else if (strcmp(Z_STRVAL_P(ztype), "redirect") == 0) {
765+
zval *ztarget = zend_hash_index_find_deref(Z_ARRVAL_P(descitem), 1);
766+
struct php_proc_open_descriptor_item *target = NULL;
767+
php_file_descriptor_t childend;
768+
769+
if (!ztarget) {
770+
php_error_docref(NULL, E_WARNING, "Missing redirection target");
771+
goto exit_fail;
772+
}
773+
if (Z_TYPE_P(ztarget) != IS_LONG) {
774+
php_error_docref(NULL, E_WARNING, "Redirection target must be an integer");
775+
goto exit_fail;
776+
}
777+
778+
for (i = 0; i < ndesc; i++) {
779+
if (descriptors[i].index == Z_LVAL_P(ztarget)) {
780+
target = &descriptors[i];
781+
break;
782+
}
783+
}
784+
if (target) {
785+
childend = target->childend;
786+
} else {
787+
if (Z_LVAL_P(ztarget) < 0 || Z_LVAL_P(ztarget) > 2) {
788+
php_error_docref(NULL, E_WARNING,
789+
"Redirection target " ZEND_LONG_FMT " not found", Z_LVAL_P(ztarget));
790+
goto exit_fail;
791+
}
792+
793+
/* Support referring to a stdin/stdout/stderr pipe adopted from the parent,
794+
* which happens whenever an explicit override is not provided. */
795+
#ifndef PHP_WIN32
796+
childend = Z_LVAL_P(ztarget);
797+
#else
798+
switch (Z_LVAL_P(ztarget)) {
799+
case 0: childend = GetStdHandle(STD_INPUT_HANDLE); break;
800+
case 1: childend = GetStdHandle(STD_OUTPUT_HANDLE); break;
801+
case 2: childend = GetStdHandle(STD_ERROR_HANDLE); break;
802+
EMPTY_SWITCH_DEFAULT_CASE()
803+
}
804+
#endif
805+
}
806+
807+
#ifdef PHP_WIN32
808+
descriptors[ndesc].childend = dup_handle(childend, TRUE, FALSE);
809+
if (descriptors[ndesc].childend == NULL) {
810+
php_error_docref(NULL, E_WARNING,
811+
"Failed to dup() for descriptor " ZEND_LONG_FMT, nindex);
812+
goto exit_fail;
813+
}
814+
#else
815+
descriptors[ndesc].childend = dup(childend);
816+
if (descriptors[ndesc].childend < 0) {
817+
php_error_docref(NULL, E_WARNING,
818+
"Failed to dup() for descriptor " ZEND_LONG_FMT " - %s",
819+
nindex, strerror(errno));
820+
goto exit_fail;
821+
}
822+
#endif
823+
descriptors[ndesc].mode = DESC_REDIRECT;
824+
} else if (strcmp(Z_STRVAL_P(ztype), "null") == 0) {
825+
#ifndef PHP_WIN32
826+
descriptors[ndesc].childend = open("/dev/null", O_RDWR);
827+
if (descriptors[ndesc].childend < 0) {
828+
php_error_docref(NULL, E_WARNING,
829+
"Failed to open /dev/null - %s", strerror(errno));
830+
goto exit_fail;
831+
}
832+
#else
833+
descriptors[ndesc].childend = CreateFileA(
834+
"nul", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
835+
NULL, OPEN_EXISTING, 0, NULL);
836+
descriptors[ndesc].childend = VCWD_OPEN("nul", O_RDWR);
837+
if (descriptors[ndesc].childend == NULL) {
838+
php_error_docref(NULL, E_WARNING, "Failed to open nul");
839+
goto exit_fail;
840+
}
841+
#endif
842+
descriptors[ndesc].mode = DESC_FILE;
763843
} else if (strcmp(Z_STRVAL_P(ztype), "pty") == 0) {
764844
#if PHP_CAN_DO_PTS
765845
if (dev_ptmx == -1) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
Null pipes in proc_open()
3+
--FILE--
4+
<?php
5+
6+
$php = getenv('TEST_PHP_EXECUTABLE');
7+
$cmd = [$php, '-r', 'echo "Test"; fprintf(STDERR, "Error");'];
8+
9+
$proc = proc_open($cmd, [1 => ['null'], 2 => ['pipe', 'w']], $pipes);
10+
var_dump($pipes);
11+
var_dump(stream_get_contents($pipes[2]));
12+
proc_close($proc);
13+
14+
$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['null']], $pipes);
15+
var_dump($pipes);
16+
var_dump(stream_get_contents($pipes[1]));
17+
proc_close($proc);
18+
19+
?>
20+
--EXPECT--
21+
array(1) {
22+
[2]=>
23+
resource(4) of type (stream)
24+
}
25+
string(5) "Error"
26+
array(1) {
27+
[1]=>
28+
resource(6) of type (stream)
29+
}
30+
string(4) "Test"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
Redirection support in proc_open
3+
--FILE--
4+
<?php
5+
6+
$php = getenv('TEST_PHP_EXECUTABLE');
7+
var_dump(proc_open([$php], [['redirect']], $pipes));
8+
var_dump(proc_open([$php], [['redirect', 'foo']], $pipes));
9+
var_dump(proc_open([$php], [['redirect', 42]], $pipes));
10+
11+
echo "\nWith pipe:\n";
12+
$cmd = [$php, '-r', 'echo "Test\n"; fprintf(STDERR, "Error");'];
13+
$proc = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['redirect', 1]], $pipes);
14+
var_dump($pipes);
15+
var_dump(stream_get_contents($pipes[1]));
16+
proc_close($proc);
17+
18+
echo "\nWith filename:\n";
19+
$fileName = __DIR__ . '/proc_open_redirect.txt';
20+
$proc = proc_open($cmd, [1 => ['file', $fileName, 'w'], 2 => ['redirect', 1]], $pipes);
21+
var_dump($pipes);
22+
proc_close($proc);
23+
var_dump(file_get_contents($fileName));
24+
unlink($fileName);
25+
26+
echo "\nWith file:\n";
27+
$file = fopen($fileName, 'w');
28+
$proc = proc_open($cmd, [1 => $file, 2 => ['redirect', 1]], $pipes);
29+
var_dump($pipes);
30+
proc_close($proc);
31+
fclose($file);
32+
var_dump(file_get_contents($fileName));
33+
unlink($fileName);
34+
35+
echo "\nWith inherited stdout:\n";
36+
$proc = proc_open($cmd, [2 => ['redirect', 1]], $pipes);
37+
proc_close($proc);
38+
39+
?>
40+
--EXPECTF--
41+
Warning: proc_open(): Missing redirection target in %s on line %d
42+
bool(false)
43+
44+
Warning: proc_open(): Redirection target must be an integer in %s on line %d
45+
bool(false)
46+
47+
Warning: proc_open(): Redirection target 42 not found in %s on line %d
48+
bool(false)
49+
50+
With pipe:
51+
array(1) {
52+
[1]=>
53+
resource(4) of type (stream)
54+
}
55+
string(10) "Test
56+
Error"
57+
58+
With filename:
59+
array(0) {
60+
}
61+
string(10) "Test
62+
Error"
63+
64+
With file:
65+
array(0) {
66+
}
67+
string(10) "Test
68+
Error"
69+
70+
With inherited stdout:
71+
Test
72+
Error

0 commit comments

Comments
 (0)