Coverage Report

Created: 2026-07-25 06:39

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