Support socketpairs in proc_open()

Closes GH-5777.
This commit is contained in:
Martin Schröder 2020-07-12 20:56:47 +02:00 committed by Nikita Popov
parent 1a00d015be
commit 547d98b81d
10 changed files with 301 additions and 38 deletions

View file

@ -631,6 +631,14 @@ PHP 8.0 UPGRADE NOTES
$proc = proc_open($command, [['pty'], ['pty'], ['pty']], $pipes);
. proc_open() now supports socket pair descriptors. The following attaches
a distinct socket pair to stdin, stdout and stderr:
$proc = proc_open(
$command, [['socket'], ['socket'], ['socket']], $pipes);
Unlike pipes, sockets do not suffer from blocking I/O issues on Windows.
However, not all programs may work correctly with stdio sockets.
. Sorting functions are now stable, which means that equal-comparing elements
will retain their original order.
RFC: https://wiki.php.net/rfc/stable_sorting

View file

@ -444,11 +444,18 @@ static inline HANDLE dup_fd_as_handle(int fd)
# define close_descriptor(fd) close(fd)
#endif
/* Determines the type of a descriptor item. */
typedef enum _descriptor_type {
DESCRIPTOR_TYPE_STD,
DESCRIPTOR_TYPE_PIPE,
DESCRIPTOR_TYPE_SOCKET
} descriptor_type;
/* One instance of this struct is created for each item in `$descriptorspec` argument to `proc_open`
* They are used within `proc_open` and freed before it returns */
typedef struct _descriptorspec_item {
int index; /* desired FD # in child process */
int is_pipe;
descriptor_type type;
php_file_descriptor_t childend; /* FD # opened for use in child
* (will be copied to `index` in child) */
php_file_descriptor_t parentend; /* FD # opened for use in parent
@ -679,7 +686,7 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
}
}
desc->is_pipe = 1;
desc->type = DESCRIPTOR_TYPE_PIPE;
desc->childend = dup(*slave_fd);
desc->parentend = dup(*master_fd);
desc->mode_flags = O_RDWR;
@ -690,6 +697,19 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
#endif
}
/* Mark the descriptor close-on-exec, so it won't be inherited by children */
static php_file_descriptor_t make_descriptor_cloexec(php_file_descriptor_t fd)
{
#ifdef PHP_WIN32
return dup_handle(fd, FALSE, TRUE);
#else
#if defined(F_SETFD) && defined(FD_CLOEXEC)
fcntl(fd, F_SETFD, FD_CLOEXEC);
#endif
return fd;
#endif
}
static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *zmode)
{
php_file_descriptor_t newpipe[2];
@ -699,7 +719,7 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
return FAILURE;
}
desc->is_pipe = 1;
desc->type = DESCRIPTOR_TYPE_PIPE;
if (strncmp(ZSTR_VAL(zmode), "w", 1) != 0) {
desc->parentend = newpipe[1];
@ -711,10 +731,9 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
desc->mode_flags = O_RDONLY;
}
#ifdef PHP_WIN32
/* don't let the child inherit the parent side of the pipe */
desc->parentend = dup_handle(desc->parentend, FALSE, TRUE);
desc->parentend = make_descriptor_cloexec(desc->parentend);
#ifdef PHP_WIN32
if (ZSTR_LEN(zmode) >= 2 && ZSTR_VAL(zmode)[1] == 'b')
desc->mode_flags |= O_BINARY;
#endif
@ -722,6 +741,32 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
return SUCCESS;
}
#ifdef PHP_WIN32
#define create_socketpair(socks) socketpair_win32(AF_INET, SOCK_STREAM, 0, (socks), 0)
#else
#define create_socketpair(socks) socketpair(AF_UNIX, SOCK_STREAM, 0, (socks))
#endif
static int set_proc_descriptor_to_socket(descriptorspec_item *desc)
{
php_socket_t sock[2];
if (create_socketpair(sock)) {
zend_string *err = php_socket_error_str(php_socket_errno());
php_error_docref(NULL, E_WARNING, "Unable to create socket pair: %s", ZSTR_VAL(err));
zend_string_release(err);
return FAILURE;
}
desc->type = DESCRIPTOR_TYPE_SOCKET;
desc->parentend = make_descriptor_cloexec((php_file_descriptor_t) sock[0]);
/* Pass sock[1] to child because it will never use overlapped IO on Windows. */
desc->childend = (php_file_descriptor_t) sock[1];
return SUCCESS;
}
static int set_proc_descriptor_to_file(descriptorspec_item *desc, zend_string *file_path,
zend_string *file_mode)
{
@ -827,6 +872,9 @@ static int set_proc_descriptor_from_array(zval *descitem, descriptorspec_item *d
goto finish;
}
retval = set_proc_descriptor_to_pipe(&descriptors[ndesc], zmode);
} else if (zend_string_equals_literal(ztype, "socket")) {
/* Set descriptor to socketpair */
retval = set_proc_descriptor_to_socket(&descriptors[ndesc]);
} else if (zend_string_equals_literal(ztype, "file")) {
/* Set descriptor to file */
if ((zfile = get_string_parameter(descitem, 1, "file name parameter for 'file'")) == NULL) {
@ -903,7 +951,7 @@ static int close_parentends_of_pipes(descriptorspec_item *descriptors, int ndesc
* Also, dup() the child end of all pipes as necessary so they will use the FD
* number which the user requested */
for (int i = 0; i < ndesc; i++) {
if (descriptors[i].is_pipe) {
if (descriptors[i].type != DESCRIPTOR_TYPE_STD) {
close(descriptors[i].parentend);
}
if (descriptors[i].childend != descriptors[i].index) {
@ -1194,12 +1242,13 @@ PHP_FUNCTION(proc_open)
/* Clean up all the child ends and then open streams on the parent
* ends, where appropriate */
for (i = 0; i < ndesc; i++) {
char *mode_string = NULL;
php_stream *stream = NULL;
close_descriptor(descriptors[i].childend);
if (descriptors[i].is_pipe) {
if (descriptors[i].type == DESCRIPTOR_TYPE_PIPE) {
char *mode_string = NULL;
switch (descriptors[i].mode_flags) {
#ifdef PHP_WIN32
case O_WRONLY|O_BINARY:
@ -1219,33 +1268,32 @@ PHP_FUNCTION(proc_open)
mode_string = "r+";
break;
}
#ifdef PHP_WIN32
stream = php_stream_fopen_from_fd(_open_osfhandle((zend_intptr_t)descriptors[i].parentend,
descriptors[i].mode_flags), mode_string, NULL);
php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, blocking_pipes, NULL);
#else
stream = php_stream_fopen_from_fd(descriptors[i].parentend, mode_string, NULL);
# if defined(F_SETFD) && defined(FD_CLOEXEC)
/* Mark the descriptor close-on-exec, so it won't be inherited by
* potential other children */
fcntl(descriptors[i].parentend, F_SETFD, FD_CLOEXEC);
# endif
#endif
if (stream) {
zval retfp;
/* nasty hack; don't copy it */
stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
php_stream_to_zval(stream, &retfp);
add_index_zval(pipes, descriptors[i].index, &retfp);
proc->pipes[i] = Z_RES(retfp);
Z_ADDREF(retfp);
}
} else if (descriptors[i].type == DESCRIPTOR_TYPE_SOCKET) {
stream = php_stream_sock_open_from_socket((php_socket_t) descriptors[i].parentend, NULL);
} else {
proc->pipes[i] = NULL;
}
if (stream) {
zval retfp;
/* nasty hack; don't copy it */
stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
php_stream_to_zval(stream, &retfp);
add_index_zval(pipes, descriptors[i].index, &retfp);
proc->pipes[i] = Z_RES(retfp);
Z_ADDREF(retfp);
}
}
if (1) {

View file

@ -0,0 +1,7 @@
<?php
echo "hello";
sleep(1);
fwrite(STDERR, "SOME ERROR");
sleep(1);
echo "world";

View file

@ -0,0 +1,56 @@
--TEST--
proc_open() with output socketpairs
--FILE--
<?php
$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets1.inc'
];
$spec = [
['null'],
['socket'],
['socket']
];
$proc = proc_open($cmd, $spec, $pipes);
foreach ($pipes as $pipe) {
var_dump(stream_set_blocking($pipe, false));
}
while ($pipes) {
$r = $pipes;
$w = null;
$e = null;
if (!stream_select($r, $w, $e, null, 0)) {
throw new Error("Select failed");
}
foreach ($r as $i => $pipe) {
if (!is_resource($pipe) || feof($pipe)) {
unset($pipes[$i]);
continue;
}
$chunk = @fread($pipe, 8192);
if ($chunk === false) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}
if ($chunk !== '') {
echo "PIPE {$i} << {$chunk}\n";
}
}
}
?>
--EXPECTF--
bool(true)
bool(true)
PIPE 1 << hello
PIPE 2 << SOME ERROR
PIPE 1 << world

View file

@ -0,0 +1,7 @@
<?php
echo "hello";
sleep(1);
echo "world";
echo strtoupper(trim(fgets(STDIN)));

View file

@ -0,0 +1,67 @@
--TEST--
proc_open() with IO socketpairs
--FILE--
<?php
function poll($pipe, $read = true)
{
$r = ($read == true) ? [$pipe] : null;
$w = ($read == false) ? [$pipe] : null;
$e = null;
if (!stream_select($r, $w, $e, null, 0)) {
throw new \Error("Select failed");
}
}
function read_pipe($pipe): string
{
poll($pipe);
if (false === ($chunk = @fread($pipe, 8192))) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}
return $chunk;
}
function write_pipe($pipe, $data)
{
poll($pipe, false);
if (false == @fwrite($pipe, $data)) {
throw new Error("Failed to write: " . (error_get_last()['message'] ?? 'N/A'));
}
}
$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets2.inc'
];
$spec = [
['socket'],
['socket']
];
$proc = proc_open($cmd, $spec, $pipes);
foreach ($pipes as $pipe) {
var_dump(stream_set_blocking($pipe, false));
}
printf("STDOUT << %s\n", read_pipe($pipes[1]));
printf("STDOUT << %s\n", read_pipe($pipes[1]));
write_pipe($pipes[0], 'done');
fclose($pipes[0]);
printf("STDOUT << %s\n", read_pipe($pipes[1]));
?>
--EXPECTF--
bool(true)
bool(true)
STDOUT << hello
STDOUT << world
STDOUT << DONE

View file

@ -0,0 +1,55 @@
--TEST--
proc_open() with socket and pipe
--FILE--
<?php
function poll($pipe, $read = true)
{
$r = ($read == true) ? [$pipe] : null;
$w = ($read == false) ? [$pipe] : null;
$e = null;
if (!stream_select($r, $w, $e, null, 0)) {
throw new \Error("Select failed");
}
}
function read_pipe($pipe): string
{
poll($pipe);
if (false === ($chunk = @fread($pipe, 8192))) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}
return $chunk;
}
$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets2.inc'
];
$spec = [
['pipe', 'r'],
['socket']
];
$proc = proc_open($cmd, $spec, $pipes);
var_dump(stream_set_blocking($pipes[1], false));
printf("STDOUT << %s\n", read_pipe($pipes[1]));
printf("STDOUT << %s\n", read_pipe($pipes[1]));
fwrite($pipes[0], 'done');
fclose($pipes[0]);
printf("STDOUT << %s\n", read_pipe($pipes[1]));
?>
--EXPECTF--
bool(true)
STDOUT << hello
STDOUT << world
STDOUT << DONE

View file

@ -257,6 +257,11 @@ static void detect_is_seekable(php_stdio_stream_data *self) {
self->is_seekable = !(file_type == FILE_TYPE_PIPE || file_type == FILE_TYPE_CHAR);
self->is_pipe = file_type == FILE_TYPE_PIPE;
/* Additional check needed to distinguish between pipes and sockets. */
if (self->is_pipe && !GetNamedPipeInfo((HANDLE) handle, NULL, NULL, NULL, NULL)) {
self->is_pipe = 0;
}
}
#endif
}

View file

@ -24,34 +24,33 @@
#include "php.h"
PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2])
PHPAPI int socketpair_win32(int domain, int type, int protocol, SOCKET sock[2], int overlapped)
{
struct sockaddr_in address;
SOCKET redirect;
int size = sizeof(address);
if(domain != AF_INET) {
if (domain != AF_INET) {
WSASetLastError(WSAENOPROTOOPT);
return -1;
}
sock[0] = sock[1] = redirect = INVALID_SOCKET;
sock[1] = redirect = INVALID_SOCKET;
sock[0] = socket(domain, type, protocol);
sock[0] = socket(domain, type, protocol);
if (INVALID_SOCKET == sock[0]) {
goto error;
}
address.sin_addr.s_addr = INADDR_ANY;
address.sin_family = AF_INET;
address.sin_port = 0;
address.sin_family = AF_INET;
address.sin_port = 0;
if (bind(sock[0], (struct sockaddr*)&address, sizeof(address)) != 0) {
if (bind(sock[0], (struct sockaddr *) &address, sizeof(address)) != 0) {
goto error;
}
if(getsockname(sock[0], (struct sockaddr *)&address, &size) != 0) {
if (getsockname(sock[0], (struct sockaddr *) &address, &size) != 0) {
goto error;
}
@ -59,17 +58,22 @@ PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2])
goto error;
}
sock[1] = socket(domain, type, protocol);
if (overlapped) {
sock[1] = socket(domain, type, protocol);
} else {
sock[1] = WSASocket(domain, type, protocol, NULL, 0, 0);
}
if (INVALID_SOCKET == sock[1]) {
goto error;
}
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if(connect(sock[1], (struct sockaddr*)&address, sizeof(address)) != 0) {
if (connect(sock[1], (struct sockaddr *) &address, sizeof(address)) != 0) {
goto error;
}
redirect = accept(sock[0],(struct sockaddr*)&address, &size);
redirect = accept(sock[0], (struct sockaddr *) &address, &size);
if (INVALID_SOCKET == redirect) {
goto error;
}
@ -86,3 +90,8 @@ error:
WSASetLastError(WSAECONNABORTED);
return -1;
}
PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2])
{
return socketpair_win32(domain, type, protocol, sock, 1);
}

View file

@ -22,6 +22,7 @@
#ifndef PHP_WIN32_SOCKETS_H
#define PHP_WIN32_SOCKETS_H
PHPAPI int socketpair_win32(int domain, int type, int protocol, SOCKET sock[2], int overlapped);
PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2]);
#endif