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