Coverage Report

Created: 2026-04-01 06:49

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