Coverage Report

Created: 2025-06-13 06:43

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