]> granicus.if.org Git - php/commitdiff
Support socketpairs in proc_open()
authorMartin Schröder <m.schroeder2007@gmail.com>
Sun, 12 Jul 2020 18:56:47 +0000 (20:56 +0200)
committerNikita Popov <nikita.ppv@gmail.com>
Tue, 14 Jul 2020 08:35:45 +0000 (10:35 +0200)
Closes GH-5777.

UPGRADING
ext/standard/proc_open.c
ext/standard/tests/general_functions/proc_open_sockets1.inc [new file with mode: 0644]
ext/standard/tests/general_functions/proc_open_sockets1.phpt [new file with mode: 0644]
ext/standard/tests/general_functions/proc_open_sockets2.inc [new file with mode: 0644]
ext/standard/tests/general_functions/proc_open_sockets2.phpt [new file with mode: 0644]
ext/standard/tests/general_functions/proc_open_sockets3.phpt [new file with mode: 0644]
main/streams/plain_wrapper.c
win32/sockets.c
win32/sockets.h

index 704bd2924a43ba5b43de3a03f17b850131ae4c24..cb4021b8a502fff9945efa666886a5380fd23eef 100644 (file)
--- a/UPGRADING
+++ b/UPGRADING
@@ -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
index ac00dd318c17257d65e9f36ce03214e0ca4b2902..501d2c3f351bbae1c53644426a235a57a58691f2 100644 (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,32 +1268,31 @@ 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;
+               } 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;
+               }
 
-                               /* nasty hack; don't copy it */
-                               stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
+               if (stream) {
+                       zval retfp;
 
-                               php_stream_to_zval(stream, &retfp);
-                               add_index_zval(pipes, descriptors[i].index, &retfp);
+                       /* nasty hack; don't copy it */
+                       stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
 
-                               proc->pipes[i] = Z_RES(retfp);
-                               Z_ADDREF(retfp);
-                       }
-               } else {
-                       proc->pipes[i] = NULL;
+                       php_stream_to_zval(stream, &retfp);
+                       add_index_zval(pipes, descriptors[i].index, &retfp);
+
+                       proc->pipes[i] = Z_RES(retfp);
+                       Z_ADDREF(retfp);
                }
        }
 
diff --git a/ext/standard/tests/general_functions/proc_open_sockets1.inc b/ext/standard/tests/general_functions/proc_open_sockets1.inc
new file mode 100644 (file)
index 0000000..680f9ce
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+echo "hello";
+sleep(1);
+fwrite(STDERR, "SOME ERROR");
+sleep(1);
+echo "world";
diff --git a/ext/standard/tests/general_functions/proc_open_sockets1.phpt b/ext/standard/tests/general_functions/proc_open_sockets1.phpt
new file mode 100644 (file)
index 0000000..baecc44
--- /dev/null
@@ -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
diff --git a/ext/standard/tests/general_functions/proc_open_sockets2.inc b/ext/standard/tests/general_functions/proc_open_sockets2.inc
new file mode 100644 (file)
index 0000000..e9a6c9e
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+echo "hello";
+sleep(1);
+echo "world";
+
+echo strtoupper(trim(fgets(STDIN)));
diff --git a/ext/standard/tests/general_functions/proc_open_sockets2.phpt b/ext/standard/tests/general_functions/proc_open_sockets2.phpt
new file mode 100644 (file)
index 0000000..25f3153
--- /dev/null
@@ -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
diff --git a/ext/standard/tests/general_functions/proc_open_sockets3.phpt b/ext/standard/tests/general_functions/proc_open_sockets3.phpt
new file mode 100644 (file)
index 0000000..5ee9e53
--- /dev/null
@@ -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
index f33c65df68e5e3c243d97923da6ec2721e5b75c2..b6db92ddadda9d54cefa6c3c53c8704f993c967e 100644 (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
 }
index c38d2bccbc94649a5129eccef0d94a3f1b4f779d..0a9a230df5c1e0e4fdacd58b086cade42e19cd35 100644 (file)
 
 #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);
+}
index f254133cc8c11aec8d82fb07b86c228b008a4be2..2b2b5c4712642cb41c1f7021c884bced50bcdda4 100644 (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