xref: /PHP-8.1/ext/standard/proc_open.c (revision e3c784f2)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Author: Wez Furlong <wez@thebrainroom.com>                           |
14    +----------------------------------------------------------------------+
15  */
16 
17 #include "php.h"
18 #include <stdio.h>
19 #include <ctype.h>
20 #include <signal.h>
21 #include "php_string.h"
22 #include "ext/standard/head.h"
23 #include "ext/standard/basic_functions.h"
24 #include "ext/standard/file.h"
25 #include "exec.h"
26 #include "php_globals.h"
27 #include "SAPI.h"
28 #include "main/php_network.h"
29 #include "zend_smart_str.h"
30 
31 #if HAVE_SYS_WAIT_H
32 #include <sys/wait.h>
33 #endif
34 
35 #if HAVE_FCNTL_H
36 #include <fcntl.h>
37 #endif
38 
39 /* This symbol is defined in ext/standard/config.m4.
40  * Essentially, it is set if you HAVE_FORK || PHP_WIN32
41  * Other platforms may modify that configure check and add suitable #ifdefs
42  * around the alternate code. */
43 #ifdef PHP_CAN_SUPPORT_PROC_OPEN
44 
45 #if HAVE_OPENPTY
46 # if HAVE_PTY_H
47 #  include <pty.h>
48 # elif defined(__FreeBSD__)
49 /* FreeBSD defines `openpty` in <libutil.h> */
50 #  include <libutil.h>
51 # elif defined(__NetBSD__) || defined(__DragonFly__)
52 /* On recent NetBSD/DragonFlyBSD releases the emalloc, estrdup ... calls had been introduced in libutil */
53 #  if defined(__NetBSD__)
54 #    include <sys/termios.h>
55 #  else
56 #    include <termios.h>
57 #  endif
58 extern int openpty(int *, int *, char *, struct termios *, struct winsize *);
59 # elif defined(__sun)
60 #    include <termios.h>
61 # else
62 /* Mac OS X (and some BSDs) define `openpty` in <util.h> */
63 #  include <util.h>
64 # endif
65 #elif defined(__sun)
66 # include <fcntl.h>
67 # include <stropts.h>
68 # include <termios.h>
69 # define HAVE_OPENPTY 1
70 
71 /* Solaris before version 11.4 and Illumos do not have any openpty implementation */
openpty(int * master,int * slave,char * name,struct termios * termp,struct winsize * winp)72 int openpty(int *master, int *slave, char *name, struct termios *termp, struct winsize *winp)
73 {
74 	int fd, sd;
75 	const char *slaveid;
76 
77 	assert(master);
78 	assert(slave);
79 
80 	sd = *master = *slave = -1;
81 	fd = open("/dev/ptmx", O_NOCTTY|O_RDWR);
82 	if (fd == -1) {
83 		return -1;
84 	}
85 	/* Checking if we can have to the pseudo terminal */
86 	if (grantpt(fd) != 0 || unlockpt(fd) != 0) {
87 		goto fail;
88 	}
89 	slaveid = ptsname(fd);
90 	if (!slaveid) {
91 		goto fail;
92 	}
93 
94 	/* Getting the slave path and pushing pseudo terminal */
95 	sd = open(slaveid, O_NOCTTY|O_RDONLY);
96 	if (sd == -1 || ioctl(sd, I_PUSH, "ptem") == -1) {
97 		goto fail;
98 	}
99 	if (termp) {
100 		if (tcgetattr(sd, termp) < 0) {
101 			goto fail;
102 		}
103 	}
104 	if (winp) {
105 		if (ioctl(sd, TIOCSWINSZ, winp) == -1) {
106 			goto fail;
107 		}
108 	}
109 
110 	*slave = sd;
111 	*master = fd;
112 	return 0;
113 fail:
114 	if (sd != -1) {
115 		close(sd);
116 	}
117 	if (fd != -1) {
118 		close(fd);
119 	}
120 	return -1;
121 }
122 #endif
123 
124 #include "proc_open.h"
125 
126 static int le_proc_open; /* Resource number for `proc` resources */
127 
128 /* {{{ _php_array_to_envp
129  * Process the `environment` argument to `proc_open`
130  * Convert into data structures which can be passed to underlying OS APIs like `exec` on POSIX or
131  * `CreateProcessW` on Win32 */
_php_array_to_envp(zval * environment)132 static php_process_env _php_array_to_envp(zval *environment)
133 {
134 	zval *element;
135 	php_process_env env;
136 	zend_string *key, *str;
137 #ifndef PHP_WIN32
138 	char **ep;
139 #endif
140 	char *p;
141 	size_t sizeenv = 0;
142 	HashTable *env_hash; /* temporary PHP array used as helper */
143 
144 	memset(&env, 0, sizeof(env));
145 
146 	if (!environment) {
147 		return env;
148 	}
149 
150 	uint32_t cnt = zend_hash_num_elements(Z_ARRVAL_P(environment));
151 
152 	if (cnt < 1) {
153 #ifndef PHP_WIN32
154 		env.envarray = (char **) ecalloc(1, sizeof(char *));
155 #endif
156 		env.envp = (char *) ecalloc(4, 1);
157 		return env;
158 	}
159 
160 	ALLOC_HASHTABLE(env_hash);
161 	zend_hash_init(env_hash, cnt, NULL, NULL, 0);
162 
163 	/* first, we have to get the size of all the elements in the hash */
164 	ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(environment), key, element) {
165 		str = zval_get_string(element);
166 
167 		if (ZSTR_LEN(str) == 0) {
168 			zend_string_release_ex(str, 0);
169 			continue;
170 		}
171 
172 		sizeenv += ZSTR_LEN(str) + 1;
173 
174 		if (key && ZSTR_LEN(key)) {
175 			sizeenv += ZSTR_LEN(key) + 1;
176 			zend_hash_add_ptr(env_hash, key, str);
177 		} else {
178 			zend_hash_next_index_insert_ptr(env_hash, str);
179 		}
180 	} ZEND_HASH_FOREACH_END();
181 
182 #ifndef PHP_WIN32
183 	ep = env.envarray = (char **) ecalloc(cnt + 1, sizeof(char *));
184 #endif
185 	p = env.envp = (char *) ecalloc(sizeenv + 4, 1);
186 
187 	ZEND_HASH_FOREACH_STR_KEY_PTR(env_hash, key, str) {
188 #ifndef PHP_WIN32
189 		*ep = p;
190 		++ep;
191 #endif
192 
193 		if (key) {
194 			memcpy(p, ZSTR_VAL(key), ZSTR_LEN(key));
195 			p += ZSTR_LEN(key);
196 			*p++ = '=';
197 		}
198 
199 		memcpy(p, ZSTR_VAL(str), ZSTR_LEN(str));
200 		p += ZSTR_LEN(str);
201 		*p++ = '\0';
202 		zend_string_release_ex(str, 0);
203 	} ZEND_HASH_FOREACH_END();
204 
205 	assert((uint32_t)(p - env.envp) <= sizeenv);
206 
207 	zend_hash_destroy(env_hash);
208 	FREE_HASHTABLE(env_hash);
209 
210 	return env;
211 }
212 /* }}} */
213 
214 /* {{{ _php_free_envp
215  * Free the structures allocated by `_php_array_to_envp` */
_php_free_envp(php_process_env env)216 static void _php_free_envp(php_process_env env)
217 {
218 #ifndef PHP_WIN32
219 	if (env.envarray) {
220 		efree(env.envarray);
221 	}
222 #endif
223 	if (env.envp) {
224 		efree(env.envp);
225 	}
226 }
227 /* }}} */
228 
229 /* {{{ proc_open_rsrc_dtor
230  * Free `proc` resource, either because all references to it were dropped or because `pclose` or
231  * `proc_close` were called */
proc_open_rsrc_dtor(zend_resource * rsrc)232 static void proc_open_rsrc_dtor(zend_resource *rsrc)
233 {
234 	php_process_handle *proc = (php_process_handle*)rsrc->ptr;
235 #ifdef PHP_WIN32
236 	DWORD wstatus;
237 #elif HAVE_SYS_WAIT_H
238 	int wstatus;
239 	int waitpid_options = 0;
240 	pid_t wait_pid;
241 #endif
242 
243 	/* Close all handles to avoid a deadlock */
244 	for (int i = 0; i < proc->npipes; i++) {
245 		if (proc->pipes[i] != NULL) {
246 			GC_DELREF(proc->pipes[i]);
247 			zend_list_close(proc->pipes[i]);
248 			proc->pipes[i] = NULL;
249 		}
250 	}
251 
252 	/* `pclose_wait` tells us: Are we freeing this resource because `pclose` or `proc_close` were
253 	 * called? If so, we need to wait until the child process exits, because its exit code is
254 	 * needed as the return value of those functions.
255 	 * But if we're freeing the resource because of GC, don't wait. */
256 #ifdef PHP_WIN32
257 	if (FG(pclose_wait)) {
258 		WaitForSingleObject(proc->childHandle, INFINITE);
259 	}
260 	GetExitCodeProcess(proc->childHandle, &wstatus);
261 	if (wstatus == STILL_ACTIVE) {
262 		FG(pclose_ret) = -1;
263 	} else {
264 		FG(pclose_ret) = wstatus;
265 	}
266 	CloseHandle(proc->childHandle);
267 
268 #elif HAVE_SYS_WAIT_H
269 	if (!FG(pclose_wait)) {
270 		waitpid_options = WNOHANG;
271 	}
272 	do {
273 		wait_pid = waitpid(proc->child, &wstatus, waitpid_options);
274 	} while (wait_pid == -1 && errno == EINTR);
275 
276 	if (wait_pid <= 0) {
277 		FG(pclose_ret) = -1;
278 	} else {
279 		if (WIFEXITED(wstatus)) {
280 			wstatus = WEXITSTATUS(wstatus);
281 		}
282 		FG(pclose_ret) = wstatus;
283 	}
284 
285 #else
286 	FG(pclose_ret) = -1;
287 #endif
288 
289 	_php_free_envp(proc->env);
290 	efree(proc->pipes);
291 	zend_string_release_ex(proc->command, false);
292 	efree(proc);
293 }
294 /* }}} */
295 
296 /* {{{ PHP_MINIT_FUNCTION(proc_open) */
PHP_MINIT_FUNCTION(proc_open)297 PHP_MINIT_FUNCTION(proc_open)
298 {
299 	le_proc_open = zend_register_list_destructors_ex(proc_open_rsrc_dtor, NULL, "process",
300 		module_number);
301 	return SUCCESS;
302 }
303 /* }}} */
304 
305 /* {{{ Kill a process opened by `proc_open` */
PHP_FUNCTION(proc_terminate)306 PHP_FUNCTION(proc_terminate)
307 {
308 	zval *zproc;
309 	php_process_handle *proc;
310 	zend_long sig_no = SIGTERM;
311 
312 	ZEND_PARSE_PARAMETERS_START(1, 2)
313 		Z_PARAM_RESOURCE(zproc)
314 		Z_PARAM_OPTIONAL
315 		Z_PARAM_LONG(sig_no)
316 	ZEND_PARSE_PARAMETERS_END();
317 
318 	proc = (php_process_handle*)zend_fetch_resource(Z_RES_P(zproc), "process", le_proc_open);
319 	if (proc == NULL) {
320 		RETURN_THROWS();
321 	}
322 
323 #ifdef PHP_WIN32
324 	RETURN_BOOL(TerminateProcess(proc->childHandle, 255));
325 #else
326 	RETURN_BOOL(kill(proc->child, sig_no) == 0);
327 #endif
328 }
329 /* }}} */
330 
331 /* {{{ Close a process opened by `proc_open` */
PHP_FUNCTION(proc_close)332 PHP_FUNCTION(proc_close)
333 {
334 	zval *zproc;
335 	php_process_handle *proc;
336 
337 	ZEND_PARSE_PARAMETERS_START(1, 1)
338 		Z_PARAM_RESOURCE(zproc)
339 	ZEND_PARSE_PARAMETERS_END();
340 
341 	proc = (php_process_handle*)zend_fetch_resource(Z_RES_P(zproc), "process", le_proc_open);
342 	if (proc == NULL) {
343 		RETURN_THROWS();
344 	}
345 
346 	FG(pclose_wait) = 1; /* See comment in `proc_open_rsrc_dtor` */
347 	zend_list_close(Z_RES_P(zproc));
348 	FG(pclose_wait) = 0;
349 	RETURN_LONG(FG(pclose_ret));
350 }
351 /* }}} */
352 
353 /* {{{ Get information about a process opened by `proc_open` */
PHP_FUNCTION(proc_get_status)354 PHP_FUNCTION(proc_get_status)
355 {
356 	zval *zproc;
357 	php_process_handle *proc;
358 #ifdef PHP_WIN32
359 	DWORD wstatus;
360 #elif HAVE_SYS_WAIT_H
361 	int wstatus;
362 	pid_t wait_pid;
363 #endif
364 	bool running = 1, signaled = 0, stopped = 0;
365 	int exitcode = -1, termsig = 0, stopsig = 0;
366 
367 	ZEND_PARSE_PARAMETERS_START(1, 1)
368 		Z_PARAM_RESOURCE(zproc)
369 	ZEND_PARSE_PARAMETERS_END();
370 
371 	proc = (php_process_handle*)zend_fetch_resource(Z_RES_P(zproc), "process", le_proc_open);
372 	if (proc == NULL) {
373 		RETURN_THROWS();
374 	}
375 
376 	array_init(return_value);
377 	add_assoc_str(return_value, "command", zend_string_copy(proc->command));
378 	add_assoc_long(return_value, "pid", (zend_long)proc->child);
379 
380 #ifdef PHP_WIN32
381 	GetExitCodeProcess(proc->childHandle, &wstatus);
382 	running = wstatus == STILL_ACTIVE;
383 	exitcode = running ? -1 : wstatus;
384 
385 #elif HAVE_SYS_WAIT_H
386 	wait_pid = waitpid(proc->child, &wstatus, WNOHANG|WUNTRACED);
387 
388 	if (wait_pid == proc->child) {
389 		if (WIFEXITED(wstatus)) {
390 			running = 0;
391 			exitcode = WEXITSTATUS(wstatus);
392 		}
393 		if (WIFSIGNALED(wstatus)) {
394 			running = 0;
395 			signaled = 1;
396 			termsig = WTERMSIG(wstatus);
397 		}
398 		if (WIFSTOPPED(wstatus)) {
399 			stopped = 1;
400 			stopsig = WSTOPSIG(wstatus);
401 		}
402 	} else if (wait_pid == -1) {
403 		/* The only error which could occur here is ECHILD, which means that the PID we were
404 		 * looking for either does not exist or is not a child of this process */
405 		running = 0;
406 	}
407 #endif
408 
409 	add_assoc_bool(return_value, "running", running);
410 	add_assoc_bool(return_value, "signaled", signaled);
411 	add_assoc_bool(return_value, "stopped", stopped);
412 	add_assoc_long(return_value, "exitcode", exitcode);
413 	add_assoc_long(return_value, "termsig", termsig);
414 	add_assoc_long(return_value, "stopsig", stopsig);
415 }
416 /* }}} */
417 
418 #ifdef PHP_WIN32
419 
420 /* We use this to allow child processes to inherit handles
421  * One static instance can be shared and used for all calls to `proc_open`, since the values are
422  * never changed */
423 SECURITY_ATTRIBUTES php_proc_open_security = {
424 	.nLength = sizeof(SECURITY_ATTRIBUTES),
425 	.lpSecurityDescriptor = NULL,
426 	.bInheritHandle = TRUE
427 };
428 
429 # define pipe(pair)		(CreatePipe(&pair[0], &pair[1], &php_proc_open_security, 0) ? 0 : -1)
430 
431 # define COMSPEC_NT	"cmd.exe"
432 
dup_handle(HANDLE src,BOOL inherit,BOOL closeorig)433 static inline HANDLE dup_handle(HANDLE src, BOOL inherit, BOOL closeorig)
434 {
435 	HANDLE copy, self = GetCurrentProcess();
436 
437 	if (!DuplicateHandle(self, src, self, &copy, 0, inherit, DUPLICATE_SAME_ACCESS |
438 				(closeorig ? DUPLICATE_CLOSE_SOURCE : 0)))
439 		return NULL;
440 	return copy;
441 }
442 
dup_fd_as_handle(int fd)443 static inline HANDLE dup_fd_as_handle(int fd)
444 {
445 	return dup_handle((HANDLE)_get_osfhandle(fd), TRUE, FALSE);
446 }
447 
448 # define close_descriptor(fd)	CloseHandle(fd)
449 #else /* !PHP_WIN32 */
450 # define close_descriptor(fd)	close(fd)
451 #endif
452 
453 /* Determines the type of a descriptor item. */
454 typedef enum _descriptor_type {
455 	DESCRIPTOR_TYPE_STD,
456 	DESCRIPTOR_TYPE_PIPE,
457 	DESCRIPTOR_TYPE_SOCKET
458 } descriptor_type;
459 
460 /* One instance of this struct is created for each item in `$descriptorspec` argument to `proc_open`
461  * They are used within `proc_open` and freed before it returns */
462 typedef struct _descriptorspec_item {
463 	int index;                       /* desired FD # in child process */
464 	descriptor_type type;
465 	php_file_descriptor_t childend;  /* FD # opened for use in child
466 	                                  * (will be copied to `index` in child) */
467 	php_file_descriptor_t parentend; /* FD # opened for use in parent
468 	                                  * (for pipes only; will be 0 otherwise) */
469 	int mode_flags;                  /* mode for opening FDs: r/o, r/w, binary (on Win32), etc */
470 } descriptorspec_item;
471 
get_valid_arg_string(zval * zv,int elem_num)472 static zend_string *get_valid_arg_string(zval *zv, int elem_num) {
473 	zend_string *str = zval_get_string(zv);
474 	if (!str) {
475 		return NULL;
476 	}
477 
478 	if (strlen(ZSTR_VAL(str)) != ZSTR_LEN(str)) {
479 		zend_value_error("Command array element %d contains a null byte", elem_num);
480 		zend_string_release(str);
481 		return NULL;
482 	}
483 
484 	return str;
485 }
486 
487 #ifdef PHP_WIN32
append_backslashes(smart_str * str,size_t num_bs)488 static void append_backslashes(smart_str *str, size_t num_bs)
489 {
490 	for (size_t i = 0; i < num_bs; i++) {
491 		smart_str_appendc(str, '\\');
492 	}
493 }
494 
495 const char *special_chars = "()!^\"<>&|%";
496 
is_special_character_present(const zend_string * arg)497 static bool is_special_character_present(const zend_string *arg)
498 {
499 	for (size_t i = 0; i < ZSTR_LEN(arg); ++i) {
500 		if (strchr(special_chars, ZSTR_VAL(arg)[i]) != NULL) {
501 			return true;
502 		}
503 	}
504 	return false;
505 }
506 
507 /* See https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments and
508  * https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way */
append_win_escaped_arg(smart_str * str,zend_string * arg,bool is_cmd_argument)509 static void append_win_escaped_arg(smart_str *str, zend_string *arg, bool is_cmd_argument)
510 {
511 	size_t num_bs = 0;
512 	bool has_special_character = false;
513 
514 	if (is_cmd_argument) {
515 		has_special_character = is_special_character_present(arg);
516 		if (has_special_character) {
517 			/* Escape double quote with ^ if executed by cmd.exe. */
518 			smart_str_appendc(str, '^');
519 		}
520 	}
521 	smart_str_appendc(str, '"');
522 	for (size_t i = 0; i < ZSTR_LEN(arg); ++i) {
523 		char c = ZSTR_VAL(arg)[i];
524 		if (c == '\\') {
525 			num_bs++;
526 			continue;
527 		}
528 
529 		if (c == '"') {
530 			/* Backslashes before " need to be doubled. */
531 			num_bs = num_bs * 2 + 1;
532 		}
533 		append_backslashes(str, num_bs);
534 		if (has_special_character && strchr(special_chars, c) != NULL) {
535 			/* Escape special chars with ^ if executed by cmd.exe. */
536 			smart_str_appendc(str, '^');
537 		}
538 		smart_str_appendc(str, c);
539 		num_bs = 0;
540 	}
541 	append_backslashes(str, num_bs * 2);
542 	if (has_special_character) {
543 		/* Escape double quote with ^ if executed by cmd.exe. */
544 		smart_str_appendc(str, '^');
545 	}
546 	smart_str_appendc(str, '"');
547 }
548 
stricmp_end(const char * suffix,const char * str)549 static inline int stricmp_end(const char* suffix, const char* str) {
550     size_t suffix_len = strlen(suffix);
551     size_t str_len = strlen(str);
552 
553     if (suffix_len > str_len) {
554         return -1; /* Suffix is longer than string, cannot match. */
555     }
556 
557     /* Compare the end of the string with the suffix, ignoring case. */
558     return _stricmp(str + (str_len - suffix_len), suffix);
559 }
560 
is_executed_by_cmd(const char * prog_name)561 static bool is_executed_by_cmd(const char *prog_name)
562 {
563 	/* If program name is cmd.exe, then return true. */
564 	if (_stricmp("cmd.exe", prog_name) == 0 || _stricmp("cmd", prog_name) == 0
565 			|| stricmp_end("\\cmd.exe", prog_name) == 0 || stricmp_end("\\cmd", prog_name) == 0) {
566 		return true;
567 	}
568 
569     /* Find the last occurrence of the directory separator (backslash or forward slash). */
570     char *last_separator = strrchr(prog_name, '\\');
571     char *last_separator_fwd = strrchr(prog_name, '/');
572     if (last_separator_fwd && (!last_separator || last_separator < last_separator_fwd)) {
573         last_separator = last_separator_fwd;
574     }
575 
576     /* Find the last dot in the filename after the last directory separator. */
577     char *extension = NULL;
578     if (last_separator != NULL) {
579         extension = strrchr(last_separator, '.');
580     } else {
581         extension = strrchr(prog_name, '.');
582     }
583 
584     if (extension == NULL || extension == prog_name) {
585         /* No file extension found, it is not batch file. */
586         return false;
587     }
588 
589     /* Check if the file extension is ".bat" or ".cmd" which is always executed by cmd.exe. */
590     return _stricmp(extension, ".bat") == 0 || _stricmp(extension, ".cmd") == 0;
591 }
592 
create_win_command_from_args(HashTable * args)593 static zend_string *create_win_command_from_args(HashTable *args)
594 {
595 	smart_str str = {0};
596 	zval *arg_zv;
597 	bool is_prog_name = true;
598 	bool is_cmd_execution = false;
599 	int elem_num = 0;
600 
601 	ZEND_HASH_FOREACH_VAL(args, arg_zv) {
602 		zend_string *arg_str = get_valid_arg_string(arg_zv, ++elem_num);
603 		if (!arg_str) {
604 			smart_str_free(&str);
605 			return NULL;
606 		}
607 
608 		if (is_prog_name) {
609 			is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str));
610 		} else {
611 			smart_str_appendc(&str, ' ');
612 		}
613 
614 		append_win_escaped_arg(&str, arg_str, !is_prog_name && is_cmd_execution);
615 
616 		is_prog_name = 0;
617 		zend_string_release(arg_str);
618 	} ZEND_HASH_FOREACH_END();
619 	smart_str_0(&str);
620 	return str.s;
621 }
622 
623 /* Get a boolean option from the `other_options` array which can be passed to `proc_open`.
624  * (Currently, all options apply on Windows only.) */
get_option(zval * other_options,char * opt_name,size_t opt_name_len)625 static bool get_option(zval *other_options, char *opt_name, size_t opt_name_len)
626 {
627 	HashTable *opt_ary = Z_ARRVAL_P(other_options);
628 	zval *item = zend_hash_str_find_deref(opt_ary, opt_name, opt_name_len);
629 	return item != NULL &&
630 		(Z_TYPE_P(item) == IS_TRUE ||
631 		((Z_TYPE_P(item) == IS_LONG) && Z_LVAL_P(item)));
632 }
633 
634 /* Initialize STARTUPINFOW struct, used on Windows when spawning a process.
635  * Ref: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfow */
init_startup_info(STARTUPINFOW * si,descriptorspec_item * descriptors,int ndesc)636 static void init_startup_info(STARTUPINFOW *si, descriptorspec_item *descriptors, int ndesc)
637 {
638 	memset(si, 0, sizeof(STARTUPINFOW));
639 	si->cb = sizeof(STARTUPINFOW);
640 	si->dwFlags = STARTF_USESTDHANDLES;
641 
642 	si->hStdInput  = GetStdHandle(STD_INPUT_HANDLE);
643 	si->hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
644 	si->hStdError  = GetStdHandle(STD_ERROR_HANDLE);
645 
646 	/* redirect stdin/stdout/stderr if requested */
647 	for (int i = 0; i < ndesc; i++) {
648 		switch (descriptors[i].index) {
649 			case 0:
650 				si->hStdInput = descriptors[i].childend;
651 				break;
652 			case 1:
653 				si->hStdOutput = descriptors[i].childend;
654 				break;
655 			case 2:
656 				si->hStdError = descriptors[i].childend;
657 				break;
658 		}
659 	}
660 }
661 
init_process_info(PROCESS_INFORMATION * pi)662 static void init_process_info(PROCESS_INFORMATION *pi)
663 {
664 	memset(&pi, 0, sizeof(pi));
665 }
666 
convert_command_to_use_shell(wchar_t ** cmdw,size_t cmdw_len)667 static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len)
668 {
669 	size_t len = sizeof(COMSPEC_NT) + sizeof(" /s /c ") + cmdw_len + 3;
670 	wchar_t *cmdw_shell = (wchar_t *)malloc(len * sizeof(wchar_t));
671 
672 	if (cmdw_shell == NULL) {
673 		php_error_docref(NULL, E_WARNING, "Command conversion failed");
674 		return FAILURE;
675 	}
676 
677 	if (_snwprintf(cmdw_shell, len, L"%hs /s /c \"%s\"", COMSPEC_NT, *cmdw) == -1) {
678 		free(cmdw_shell);
679 		php_error_docref(NULL, E_WARNING, "Command conversion failed");
680 		return FAILURE;
681 	}
682 
683 	free(*cmdw);
684 	*cmdw = cmdw_shell;
685 
686 	return SUCCESS;
687 }
688 #endif
689 
690 /* Convert command parameter array passed as first argument to `proc_open` into command string */
get_command_from_array(HashTable * array,char *** argv,int num_elems)691 static zend_string* get_command_from_array(HashTable *array, char ***argv, int num_elems)
692 {
693 	zval *arg_zv;
694 	zend_string *command = NULL;
695 	int i = 0;
696 
697 	*argv = safe_emalloc(sizeof(char *), num_elems + 1, 0);
698 
699 	ZEND_HASH_FOREACH_VAL(array, arg_zv) {
700 		zend_string *arg_str = get_valid_arg_string(arg_zv, i + 1);
701 		if (!arg_str) {
702 			/* Terminate with NULL so exit_fail code knows how many entries to free */
703 			(*argv)[i] = NULL;
704 			if (command != NULL) {
705 				zend_string_release_ex(command, false);
706 			}
707 			return NULL;
708 		}
709 
710 		if (i == 0) {
711 			command = zend_string_copy(arg_str);
712 		}
713 
714 		(*argv)[i++] = estrdup(ZSTR_VAL(arg_str));
715 		zend_string_release(arg_str);
716 	} ZEND_HASH_FOREACH_END();
717 
718 	(*argv)[i] = NULL;
719 	return command;
720 }
721 
alloc_descriptor_array(HashTable * descriptorspec)722 static descriptorspec_item* alloc_descriptor_array(HashTable *descriptorspec)
723 {
724 	uint32_t ndescriptors = zend_hash_num_elements(descriptorspec);
725 	return ecalloc(sizeof(descriptorspec_item), ndescriptors);
726 }
727 
get_string_parameter(zval * array,int index,char * param_name)728 static zend_string* get_string_parameter(zval *array, int index, char *param_name)
729 {
730 	zval *array_item;
731 	if ((array_item = zend_hash_index_find(Z_ARRVAL_P(array), index)) == NULL) {
732 		zend_value_error("Missing %s", param_name);
733 		return NULL;
734 	}
735 	return zval_try_get_string(array_item);
736 }
737 
set_proc_descriptor_to_blackhole(descriptorspec_item * desc)738 static zend_result set_proc_descriptor_to_blackhole(descriptorspec_item *desc)
739 {
740 #ifdef PHP_WIN32
741 	desc->childend = CreateFileA("nul", GENERIC_READ | GENERIC_WRITE,
742 		FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
743 	if (desc->childend == NULL) {
744 		php_error_docref(NULL, E_WARNING, "Failed to open nul");
745 		return FAILURE;
746 	}
747 #else
748 	desc->childend = open("/dev/null", O_RDWR);
749 	if (desc->childend < 0) {
750 		php_error_docref(NULL, E_WARNING, "Failed to open /dev/null: %s", strerror(errno));
751 		return FAILURE;
752 	}
753 #endif
754 	return SUCCESS;
755 }
756 
set_proc_descriptor_to_pty(descriptorspec_item * desc,int * master_fd,int * slave_fd)757 static zend_result set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd, int *slave_fd)
758 {
759 #if HAVE_OPENPTY
760 	/* All FDs set to PTY in the child process will go to the slave end of the same PTY.
761 	 * Likewise, all the corresponding entries in `$pipes` in the parent will all go to the master
762 	 * end of the same PTY.
763 	 * If this is the first descriptorspec set to 'pty', find an available PTY and get master and
764 	 * slave FDs. */
765 	if (*master_fd == -1) {
766 		if (openpty(master_fd, slave_fd, NULL, NULL, NULL)) {
767 			php_error_docref(NULL, E_WARNING, "Could not open PTY (pseudoterminal): %s", strerror(errno));
768 			return FAILURE;
769 		}
770 	}
771 
772 	desc->type       = DESCRIPTOR_TYPE_PIPE;
773 	desc->childend   = dup(*slave_fd);
774 	desc->parentend  = dup(*master_fd);
775 	desc->mode_flags = O_RDWR;
776 	return SUCCESS;
777 #else
778 	php_error_docref(NULL, E_WARNING, "PTY (pseudoterminal) not supported on this system");
779 	return FAILURE;
780 #endif
781 }
782 
783 /* Mark the descriptor close-on-exec, so it won't be inherited by children */
make_descriptor_cloexec(php_file_descriptor_t fd)784 static php_file_descriptor_t make_descriptor_cloexec(php_file_descriptor_t fd)
785 {
786 #ifdef PHP_WIN32
787 	return dup_handle(fd, FALSE, TRUE);
788 #else
789 #if defined(F_SETFD) && defined(FD_CLOEXEC)
790 	fcntl(fd, F_SETFD, FD_CLOEXEC);
791 #endif
792 	return fd;
793 #endif
794 }
795 
set_proc_descriptor_to_pipe(descriptorspec_item * desc,zend_string * zmode)796 static zend_result set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *zmode)
797 {
798 	php_file_descriptor_t newpipe[2];
799 
800 	if (pipe(newpipe)) {
801 		php_error_docref(NULL, E_WARNING, "Unable to create pipe %s", strerror(errno));
802 		return FAILURE;
803 	}
804 
805 	desc->type = DESCRIPTOR_TYPE_PIPE;
806 
807 	if (strncmp(ZSTR_VAL(zmode), "w", 1) != 0) {
808 		desc->parentend = newpipe[1];
809 		desc->childend = newpipe[0];
810 		desc->mode_flags = O_WRONLY;
811 	} else {
812 		desc->parentend = newpipe[0];
813 		desc->childend = newpipe[1];
814 		desc->mode_flags = O_RDONLY;
815 	}
816 
817 	desc->parentend = make_descriptor_cloexec(desc->parentend);
818 
819 #ifdef PHP_WIN32
820 	if (ZSTR_LEN(zmode) >= 2 && ZSTR_VAL(zmode)[1] == 'b')
821 		desc->mode_flags |= O_BINARY;
822 #endif
823 
824 	return SUCCESS;
825 }
826 
827 #ifdef PHP_WIN32
828 #define create_socketpair(socks) socketpair_win32(AF_INET, SOCK_STREAM, 0, (socks), 0)
829 #else
830 #define create_socketpair(socks) socketpair(AF_UNIX, SOCK_STREAM, 0, (socks))
831 #endif
832 
set_proc_descriptor_to_socket(descriptorspec_item * desc)833 static zend_result set_proc_descriptor_to_socket(descriptorspec_item *desc)
834 {
835 	php_socket_t sock[2];
836 
837 	if (create_socketpair(sock)) {
838 		zend_string *err = php_socket_error_str(php_socket_errno());
839 		php_error_docref(NULL, E_WARNING, "Unable to create socket pair: %s", ZSTR_VAL(err));
840 		zend_string_release(err);
841 		return FAILURE;
842 	}
843 
844 	desc->type = DESCRIPTOR_TYPE_SOCKET;
845 	desc->parentend = make_descriptor_cloexec((php_file_descriptor_t) sock[0]);
846 
847 	/* Pass sock[1] to child because it will never use overlapped IO on Windows. */
848 	desc->childend = (php_file_descriptor_t) sock[1];
849 
850 	return SUCCESS;
851 }
852 
set_proc_descriptor_to_file(descriptorspec_item * desc,zend_string * file_path,zend_string * file_mode)853 static zend_result set_proc_descriptor_to_file(descriptorspec_item *desc, zend_string *file_path,
854 	zend_string *file_mode)
855 {
856 	php_socket_t fd;
857 
858 	/* try a wrapper */
859 	php_stream *stream = php_stream_open_wrapper(ZSTR_VAL(file_path), ZSTR_VAL(file_mode),
860 		REPORT_ERRORS|STREAM_WILL_CAST, NULL);
861 	if (stream == NULL) {
862 		return FAILURE;
863 	}
864 
865 	/* force into an fd */
866 	if (php_stream_cast(stream, PHP_STREAM_CAST_RELEASE|PHP_STREAM_AS_FD, (void **)&fd,
867 		REPORT_ERRORS) == FAILURE) {
868 		return FAILURE;
869 	}
870 
871 #ifdef PHP_WIN32
872 	desc->childend = dup_fd_as_handle((int)fd);
873 	_close((int)fd);
874 
875 	/* Simulate the append mode by fseeking to the end of the file
876 	 * This introduces a potential race condition, but it is the best we can do */
877 	if (strchr(ZSTR_VAL(file_mode), 'a')) {
878 		SetFilePointer(desc->childend, 0, NULL, FILE_END);
879 	}
880 #else
881 	desc->childend = fd;
882 #endif
883 	return SUCCESS;
884 }
885 
dup_proc_descriptor(php_file_descriptor_t from,php_file_descriptor_t * to,zend_ulong nindex)886 static zend_result dup_proc_descriptor(php_file_descriptor_t from, php_file_descriptor_t *to,
887 	zend_ulong nindex)
888 {
889 #ifdef PHP_WIN32
890 	*to = dup_handle(from, TRUE, FALSE);
891 	if (*to == NULL) {
892 		php_error_docref(NULL, E_WARNING, "Failed to dup() for descriptor " ZEND_LONG_FMT, nindex);
893 		return FAILURE;
894 	}
895 #else
896 	*to = dup(from);
897 	if (*to < 0) {
898 		php_error_docref(NULL, E_WARNING, "Failed to dup() for descriptor " ZEND_LONG_FMT ": %s",
899 			nindex, strerror(errno));
900 		return FAILURE;
901 	}
902 #endif
903 	return SUCCESS;
904 }
905 
redirect_proc_descriptor(descriptorspec_item * desc,int target,descriptorspec_item * descriptors,int ndesc,int nindex)906 static zend_result redirect_proc_descriptor(descriptorspec_item *desc, int target,
907 	descriptorspec_item *descriptors, int ndesc, int nindex)
908 {
909 	php_file_descriptor_t redirect_to = PHP_INVALID_FD;
910 
911 	for (int i = 0; i < ndesc; i++) {
912 		if (descriptors[i].index == target) {
913 			redirect_to = descriptors[i].childend;
914 			break;
915 		}
916 	}
917 
918 	if (redirect_to == PHP_INVALID_FD) { /* Didn't find the index we wanted */
919 		if (target < 0 || target > 2) {
920 			php_error_docref(NULL, E_WARNING, "Redirection target %d not found", target);
921 			return FAILURE;
922 		}
923 
924 		/* Support referring to a stdin/stdout/stderr pipe adopted from the parent,
925 		 * which happens whenever an explicit override is not provided. */
926 #ifndef PHP_WIN32
927 		redirect_to = target;
928 #else
929 		switch (target) {
930 			case 0: redirect_to = GetStdHandle(STD_INPUT_HANDLE); break;
931 			case 1: redirect_to = GetStdHandle(STD_OUTPUT_HANDLE); break;
932 			case 2: redirect_to = GetStdHandle(STD_ERROR_HANDLE); break;
933 			EMPTY_SWITCH_DEFAULT_CASE()
934 		}
935 #endif
936 	}
937 
938 	return dup_proc_descriptor(redirect_to, &desc->childend, nindex);
939 }
940 
941 /* Process one item from `$descriptorspec` argument to `proc_open` */
set_proc_descriptor_from_array(zval * descitem,descriptorspec_item * descriptors,int ndesc,int nindex,int * pty_master_fd,int * pty_slave_fd)942 static zend_result set_proc_descriptor_from_array(zval *descitem, descriptorspec_item *descriptors,
943 	int ndesc, int nindex, int *pty_master_fd, int *pty_slave_fd) {
944 	zend_string *ztype = get_string_parameter(descitem, 0, "handle qualifier");
945 	if (!ztype) {
946 		return FAILURE;
947 	}
948 
949 	zend_string *zmode = NULL, *zfile = NULL;
950 	zend_result retval = FAILURE;
951 
952 	if (zend_string_equals_literal(ztype, "pipe")) {
953 		/* Set descriptor to pipe */
954 		zmode = get_string_parameter(descitem, 1, "mode parameter for 'pipe'");
955 		if (zmode == NULL) {
956 			goto finish;
957 		}
958 		retval = set_proc_descriptor_to_pipe(&descriptors[ndesc], zmode);
959 	} else if (zend_string_equals_literal(ztype, "socket")) {
960 		/* Set descriptor to socketpair */
961 		retval = set_proc_descriptor_to_socket(&descriptors[ndesc]);
962 	} else if (zend_string_equals_literal(ztype, "file")) {
963 		/* Set descriptor to file */
964 		if ((zfile = get_string_parameter(descitem, 1, "file name parameter for 'file'")) == NULL) {
965 			goto finish;
966 		}
967 		if ((zmode = get_string_parameter(descitem, 2, "mode parameter for 'file'")) == NULL) {
968 			goto finish;
969 		}
970 		retval = set_proc_descriptor_to_file(&descriptors[ndesc], zfile, zmode);
971 	} else if (zend_string_equals_literal(ztype, "redirect")) {
972 		/* Redirect descriptor to whatever another descriptor is set to */
973 		zval *ztarget = zend_hash_index_find_deref(Z_ARRVAL_P(descitem), 1);
974 		if (!ztarget) {
975 			zend_value_error("Missing redirection target");
976 			goto finish;
977 		}
978 		if (Z_TYPE_P(ztarget) != IS_LONG) {
979 			zend_value_error("Redirection target must be of type int, %s given", zend_zval_type_name(ztarget));
980 			goto finish;
981 		}
982 
983 		retval = redirect_proc_descriptor(
984 			&descriptors[ndesc], (int)Z_LVAL_P(ztarget), descriptors, ndesc, nindex);
985 	} else if (zend_string_equals_literal(ztype, "null")) {
986 		/* Set descriptor to blackhole (discard all data written) */
987 		retval = set_proc_descriptor_to_blackhole(&descriptors[ndesc]);
988 	} else if (zend_string_equals_literal(ztype, "pty")) {
989 		/* Set descriptor to slave end of PTY */
990 		retval = set_proc_descriptor_to_pty(&descriptors[ndesc], pty_master_fd, pty_slave_fd);
991 	} else {
992 		php_error_docref(NULL, E_WARNING, "%s is not a valid descriptor spec/mode", ZSTR_VAL(ztype));
993 		goto finish;
994 	}
995 
996 finish:
997 	if (zmode) zend_string_release(zmode);
998 	if (zfile) zend_string_release(zfile);
999 	zend_string_release(ztype);
1000 	return retval;
1001 }
1002 
set_proc_descriptor_from_resource(zval * resource,descriptorspec_item * desc,int nindex)1003 static zend_result set_proc_descriptor_from_resource(zval *resource, descriptorspec_item *desc, int nindex)
1004 {
1005 	/* Should be a stream - try and dup the descriptor */
1006 	php_stream *stream = (php_stream*)zend_fetch_resource(Z_RES_P(resource), "stream",
1007 		php_file_le_stream());
1008 	if (stream == NULL) {
1009 		return FAILURE;
1010 	}
1011 
1012 	php_socket_t fd;
1013 	zend_result status = php_stream_cast(stream, PHP_STREAM_AS_FD, (void **)&fd, REPORT_ERRORS);
1014 	if (status == FAILURE) {
1015 		return FAILURE;
1016 	}
1017 
1018 #ifdef PHP_WIN32
1019 	php_file_descriptor_t fd_t = (php_file_descriptor_t)_get_osfhandle(fd);
1020 #else
1021 	php_file_descriptor_t fd_t = fd;
1022 #endif
1023 	return dup_proc_descriptor(fd_t, &desc->childend, nindex);
1024 }
1025 
1026 #ifndef PHP_WIN32
close_parentends_of_pipes(descriptorspec_item * descriptors,int ndesc)1027 static zend_result close_parentends_of_pipes(descriptorspec_item *descriptors, int ndesc)
1028 {
1029 	/* We are running in child process
1030 	 * Close the 'parent end' of pipes which were opened before forking/spawning
1031 	 * Also, dup() the child end of all pipes as necessary so they will use the FD
1032 	 * number which the user requested */
1033 	for (int i = 0; i < ndesc; i++) {
1034 		if (descriptors[i].type != DESCRIPTOR_TYPE_STD) {
1035 			close(descriptors[i].parentend);
1036 		}
1037 		if (descriptors[i].childend != descriptors[i].index) {
1038 			if (dup2(descriptors[i].childend, descriptors[i].index) < 0) {
1039 				php_error_docref(NULL, E_WARNING, "Unable to copy file descriptor %d (for pipe) into " \
1040 					"file descriptor %d: %s", descriptors[i].childend, descriptors[i].index, strerror(errno));
1041 				return FAILURE;
1042 			}
1043 			close(descriptors[i].childend);
1044 		}
1045 	}
1046 
1047 	return SUCCESS;
1048 }
1049 #endif
1050 
close_all_descriptors(descriptorspec_item * descriptors,int ndesc)1051 static void close_all_descriptors(descriptorspec_item *descriptors, int ndesc)
1052 {
1053 	for (int i = 0; i < ndesc; i++) {
1054 		close_descriptor(descriptors[i].childend);
1055 		if (descriptors[i].parentend)
1056 			close_descriptor(descriptors[i].parentend);
1057 	}
1058 }
1059 
efree_argv(char ** argv)1060 static void efree_argv(char **argv)
1061 {
1062 	if (argv) {
1063 		char **arg = argv;
1064 		while (*arg != NULL) {
1065 			efree(*arg);
1066 			arg++;
1067 		}
1068 		efree(argv);
1069 	}
1070 }
1071 
1072 /* {{{ Execute a command, with specified files used for input/output */
PHP_FUNCTION(proc_open)1073 PHP_FUNCTION(proc_open)
1074 {
1075 	zend_string *command_str;
1076 	HashTable *command_ht;
1077 	HashTable *descriptorspec; /* Mandatory argument */
1078 	zval *pipes;               /* Mandatory argument */
1079 	char *cwd = NULL;                                /* Optional argument */
1080 	size_t cwd_len = 0;                              /* Optional argument */
1081 	zval *environment = NULL, *other_options = NULL; /* Optional arguments */
1082 
1083 	php_process_env env;
1084 	int ndesc = 0;
1085 	int i;
1086 	zval *descitem = NULL;
1087 	zend_string *str_index;
1088 	zend_ulong nindex;
1089 	descriptorspec_item *descriptors = NULL;
1090 #ifdef PHP_WIN32
1091 	PROCESS_INFORMATION pi;
1092 	HANDLE childHandle;
1093 	STARTUPINFOW si;
1094 	BOOL newprocok;
1095 	DWORD dwCreateFlags = 0;
1096 	UINT old_error_mode;
1097 	char cur_cwd[MAXPATHLEN];
1098 	wchar_t *cmdw = NULL, *cwdw = NULL, *envpw = NULL;
1099 	size_t cmdw_len;
1100 	bool suppress_errors = 0;
1101 	bool bypass_shell = 0;
1102 	bool blocking_pipes = 0;
1103 	bool create_process_group = 0;
1104 	bool create_new_console = 0;
1105 #else
1106 	char **argv = NULL;
1107 #endif
1108 	int pty_master_fd = -1, pty_slave_fd = -1;
1109 	php_process_id_t child;
1110 	php_process_handle *proc;
1111 
1112 	ZEND_PARSE_PARAMETERS_START(3, 6)
1113 		Z_PARAM_ARRAY_HT_OR_STR(command_ht, command_str)
1114 		Z_PARAM_ARRAY_HT(descriptorspec)
1115 		Z_PARAM_ZVAL(pipes)
1116 		Z_PARAM_OPTIONAL
1117 		Z_PARAM_STRING_OR_NULL(cwd, cwd_len)
1118 		Z_PARAM_ARRAY_OR_NULL(environment)
1119 		Z_PARAM_ARRAY_OR_NULL(other_options)
1120 	ZEND_PARSE_PARAMETERS_END();
1121 
1122 	memset(&env, 0, sizeof(env));
1123 
1124 	if (command_ht) {
1125 		uint32_t num_elems = zend_hash_num_elements(command_ht);
1126 		if (num_elems == 0) {
1127 			zend_argument_value_error(1, "must have at least one element");
1128 			RETURN_THROWS();
1129 		}
1130 
1131 #ifdef PHP_WIN32
1132 		/* Automatically bypass shell if command is given as an array */
1133 		bypass_shell = 1;
1134 		command_str = create_win_command_from_args(command_ht);
1135 #else
1136 		command_str = get_command_from_array(command_ht, &argv, num_elems);
1137 #endif
1138 
1139 		if (!command_str) {
1140 #ifndef PHP_WIN32
1141 			efree_argv(argv);
1142 #endif
1143 			RETURN_FALSE;
1144 		}
1145 	} else {
1146 		zend_string_addref(command_str);
1147 	}
1148 
1149 #ifdef PHP_WIN32
1150 	if (other_options) {
1151 		suppress_errors      = get_option(other_options, "suppress_errors", strlen("suppress_errors"));
1152 		/* TODO: Deprecate in favor of array command? */
1153 		bypass_shell         = bypass_shell || get_option(other_options, "bypass_shell", strlen("bypass_shell"));
1154 		blocking_pipes       = get_option(other_options, "blocking_pipes", strlen("blocking_pipes"));
1155 		create_process_group = get_option(other_options, "create_process_group", strlen("create_process_group"));
1156 		create_new_console   = get_option(other_options, "create_new_console", strlen("create_new_console"));
1157 	}
1158 #endif
1159 
1160 	if (environment) {
1161 		env = _php_array_to_envp(environment);
1162 	}
1163 
1164 	descriptors = alloc_descriptor_array(descriptorspec);
1165 
1166 	/* Walk the descriptor spec and set up files/pipes */
1167 	ZEND_HASH_FOREACH_KEY_VAL(descriptorspec, nindex, str_index, descitem) {
1168 		if (str_index) {
1169 			zend_argument_value_error(2, "must be an integer indexed array");
1170 			goto exit_fail;
1171 		}
1172 
1173 		descriptors[ndesc].index = (int)nindex;
1174 
1175 		ZVAL_DEREF(descitem);
1176 		if (Z_TYPE_P(descitem) == IS_RESOURCE) {
1177 			if (set_proc_descriptor_from_resource(descitem, &descriptors[ndesc], ndesc) == FAILURE) {
1178 				goto exit_fail;
1179 			}
1180 		} else if (Z_TYPE_P(descitem) == IS_ARRAY) {
1181 			if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, (int)nindex,
1182 				&pty_master_fd, &pty_slave_fd) == FAILURE) {
1183 				goto exit_fail;
1184 			}
1185 		} else {
1186 			zend_argument_value_error(2, "must only contain arrays and streams");
1187 			goto exit_fail;
1188 		}
1189 		ndesc++;
1190 	} ZEND_HASH_FOREACH_END();
1191 
1192 #ifdef PHP_WIN32
1193 	if (cwd == NULL) {
1194 		char *getcwd_result = VCWD_GETCWD(cur_cwd, MAXPATHLEN);
1195 		if (!getcwd_result) {
1196 			php_error_docref(NULL, E_WARNING, "Cannot get current directory");
1197 			goto exit_fail;
1198 		}
1199 		cwd = cur_cwd;
1200 	}
1201 	cwdw = php_win32_cp_any_to_w(cwd);
1202 	if (!cwdw) {
1203 		php_error_docref(NULL, E_WARNING, "CWD conversion failed");
1204 		goto exit_fail;
1205 	}
1206 
1207 	init_startup_info(&si, descriptors, ndesc);
1208 	init_process_info(&pi);
1209 
1210 	if (suppress_errors) {
1211 		old_error_mode = SetErrorMode(SEM_FAILCRITICALERRORS|SEM_NOGPFAULTERRORBOX);
1212 	}
1213 
1214 	dwCreateFlags = NORMAL_PRIORITY_CLASS;
1215 	if(strcmp(sapi_module.name, "cli") != 0) {
1216 		dwCreateFlags |= CREATE_NO_WINDOW;
1217 	}
1218 	if (create_process_group) {
1219 		dwCreateFlags |= CREATE_NEW_PROCESS_GROUP;
1220 	}
1221 	if (create_new_console) {
1222 		dwCreateFlags |= CREATE_NEW_CONSOLE;
1223 	}
1224 	envpw = php_win32_cp_env_any_to_w(env.envp);
1225 	if (envpw) {
1226 		dwCreateFlags |= CREATE_UNICODE_ENVIRONMENT;
1227 	} else  {
1228 		if (env.envp) {
1229 			php_error_docref(NULL, E_WARNING, "ENV conversion failed");
1230 			goto exit_fail;
1231 		}
1232 	}
1233 
1234 	cmdw = php_win32_cp_conv_any_to_w(ZSTR_VAL(command_str), ZSTR_LEN(command_str), &cmdw_len);
1235 	if (!cmdw) {
1236 		php_error_docref(NULL, E_WARNING, "Command conversion failed");
1237 		goto exit_fail;
1238 	}
1239 
1240 	if (!bypass_shell) {
1241 		if (convert_command_to_use_shell(&cmdw, cmdw_len) == FAILURE) {
1242 			goto exit_fail;
1243 		}
1244 	}
1245 	newprocok = CreateProcessW(NULL, cmdw, &php_proc_open_security,
1246 		&php_proc_open_security, TRUE, dwCreateFlags, envpw, cwdw, &si, &pi);
1247 
1248 	if (suppress_errors) {
1249 		SetErrorMode(old_error_mode);
1250 	}
1251 
1252 	if (newprocok == FALSE) {
1253 		DWORD dw = GetLastError();
1254 		close_all_descriptors(descriptors, ndesc);
1255 		php_error_docref(NULL, E_WARNING, "CreateProcess failed, error code: %u", dw);
1256 		goto exit_fail;
1257 	}
1258 
1259 	childHandle = pi.hProcess;
1260 	child       = pi.dwProcessId;
1261 	CloseHandle(pi.hThread);
1262 #elif HAVE_FORK
1263 	/* the Unix way */
1264 	child = fork();
1265 
1266 	if (child == 0) {
1267 		/* This is the child process */
1268 
1269 		if (close_parentends_of_pipes(descriptors, ndesc) == FAILURE) {
1270 			/* We are already in child process and can't do anything to make
1271 			 * `proc_open` return an error in the parent
1272 			 * All we can do is exit with a non-zero (error) exit code */
1273 			_exit(127);
1274 		}
1275 
1276 		if (cwd) {
1277 			php_ignore_value(chdir(cwd));
1278 		}
1279 
1280 		if (argv) {
1281 			/* execvpe() is non-portable, use environ instead. */
1282 			if (env.envarray) {
1283 				environ = env.envarray;
1284 			}
1285 			execvp(ZSTR_VAL(command_str), argv);
1286 		} else {
1287 			if (env.envarray) {
1288 				execle("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL, env.envarray);
1289 			} else {
1290 				execl("/bin/sh", "sh", "-c", ZSTR_VAL(command_str), NULL);
1291 			}
1292 		}
1293 
1294 		/* If execvp/execle/execl are successful, we will never reach here
1295 		 * Display error and exit with non-zero (error) status code */
1296 		php_error_docref(NULL, E_WARNING, "Exec failed: %s", strerror(errno));
1297 		_exit(127);
1298 	} else if (child < 0) {
1299 		/* Failed to fork() */
1300 		close_all_descriptors(descriptors, ndesc);
1301 		php_error_docref(NULL, E_WARNING, "Fork failed: %s", strerror(errno));
1302 		goto exit_fail;
1303 	}
1304 #else
1305 # error You lose (configure should not have let you get here)
1306 #endif
1307 
1308 	/* We forked/spawned and this is the parent */
1309 
1310 	pipes = zend_try_array_init(pipes);
1311 	if (!pipes) {
1312 		goto exit_fail;
1313 	}
1314 
1315 	proc = (php_process_handle*) emalloc(sizeof(php_process_handle));
1316 	proc->command = zend_string_copy(command_str);
1317 	proc->pipes = emalloc(sizeof(zend_resource *) * ndesc);
1318 	proc->npipes = ndesc;
1319 	proc->child = child;
1320 #ifdef PHP_WIN32
1321 	proc->childHandle = childHandle;
1322 #endif
1323 	proc->env = env;
1324 
1325 	/* Clean up all the child ends and then open streams on the parent
1326 	 *   ends, where appropriate */
1327 	for (i = 0; i < ndesc; i++) {
1328 		php_stream *stream = NULL;
1329 
1330 		close_descriptor(descriptors[i].childend);
1331 
1332 		if (descriptors[i].type == DESCRIPTOR_TYPE_PIPE) {
1333 			char *mode_string = NULL;
1334 
1335 			switch (descriptors[i].mode_flags) {
1336 #ifdef PHP_WIN32
1337 				case O_WRONLY|O_BINARY:
1338 					mode_string = "wb";
1339 					break;
1340 				case O_RDONLY|O_BINARY:
1341 					mode_string = "rb";
1342 					break;
1343 #endif
1344 				case O_WRONLY:
1345 					mode_string = "w";
1346 					break;
1347 				case O_RDONLY:
1348 					mode_string = "r";
1349 					break;
1350 				case O_RDWR:
1351 					mode_string = "r+";
1352 					break;
1353 			}
1354 
1355 #ifdef PHP_WIN32
1356 			stream = php_stream_fopen_from_fd(_open_osfhandle((zend_intptr_t)descriptors[i].parentend,
1357 						descriptors[i].mode_flags), mode_string, NULL);
1358 			php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, blocking_pipes, NULL);
1359 #else
1360 			stream = php_stream_fopen_from_fd(descriptors[i].parentend, mode_string, NULL);
1361 #endif
1362 		} else if (descriptors[i].type == DESCRIPTOR_TYPE_SOCKET) {
1363 			stream = php_stream_sock_open_from_socket((php_socket_t) descriptors[i].parentend, NULL);
1364 		} else {
1365 			proc->pipes[i] = NULL;
1366 		}
1367 
1368 		if (stream) {
1369 			zval retfp;
1370 
1371 			/* nasty hack; don't copy it */
1372 			stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
1373 
1374 			php_stream_to_zval(stream, &retfp);
1375 			add_index_zval(pipes, descriptors[i].index, &retfp);
1376 
1377 			proc->pipes[i] = Z_RES(retfp);
1378 			Z_ADDREF(retfp);
1379 		}
1380 	}
1381 
1382 	if (1) {
1383 		RETVAL_RES(zend_register_resource(proc, le_proc_open));
1384 	} else {
1385 exit_fail:
1386 		_php_free_envp(env);
1387 		RETVAL_FALSE;
1388 	}
1389 
1390 	zend_string_release_ex(command_str, false);
1391 #ifdef PHP_WIN32
1392 	free(cwdw);
1393 	free(cmdw);
1394 	free(envpw);
1395 #else
1396 	efree_argv(argv);
1397 #endif
1398 #if HAVE_OPENPTY
1399 	if (pty_master_fd != -1) {
1400 		close(pty_master_fd);
1401 	}
1402 	if (pty_slave_fd != -1) {
1403 		close(pty_slave_fd);
1404 	}
1405 #endif
1406 	if (descriptors) {
1407 		efree(descriptors);
1408 	}
1409 }
1410 /* }}} */
1411 
1412 #endif /* PHP_CAN_SUPPORT_PROC_OPEN */
1413