Coverage Report

Created: 2026-06-02 06:36

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