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