Coverage Report

Created: 2025-09-27 06:26

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