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