Coverage Report

Created: 2025-06-13 06:43

/src/php-src/ext/standard/exec.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright (c) The PHP Group                                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to version 3.01 of the PHP license,      |
6
   | that is bundled with this package in the file LICENSE, and is        |
7
   | available through the world-wide-web at the following url:           |
8
   | https://www.php.net/license/3_01.txt                                 |
9
   | If you did not receive a copy of the PHP license and are unable to   |
10
   | obtain it through the world-wide-web, please send a note to          |
11
   | license@php.net so we can mail you a copy immediately.               |
12
   +----------------------------------------------------------------------+
13
   | Author: Rasmus Lerdorf <rasmus@php.net>                              |
14
   |         Ilia Alshanetsky <iliaa@php.net>                             |
15
   +----------------------------------------------------------------------+
16
 */
17
18
#include <stdio.h>
19
#include "php.h"
20
#include <ctype.h>
21
#include "php_string.h"
22
#include "ext/standard/file.h"
23
#include "basic_functions.h"
24
#include "exec.h"
25
#include "SAPI.h"
26
27
#ifdef HAVE_SYS_WAIT_H
28
#include <sys/wait.h>
29
#endif
30
31
#include <signal.h>
32
33
#ifdef HAVE_SYS_TYPES_H
34
#include <sys/types.h>
35
#endif
36
#ifdef HAVE_SYS_STAT_H
37
#include <sys/stat.h>
38
#endif
39
#ifdef HAVE_FCNTL_H
40
#include <fcntl.h>
41
#endif
42
43
#ifdef HAVE_UNISTD_H
44
#include <unistd.h>
45
#endif
46
47
#include <limits.h>
48
49
#ifdef PHP_WIN32
50
# include "win32/nice.h"
51
#endif
52
53
static size_t cmd_max_len;
54
55
/* {{{ PHP_MINIT_FUNCTION(exec) */
56
PHP_MINIT_FUNCTION(exec)
57
16
{
58
16
#ifdef _SC_ARG_MAX
59
16
  cmd_max_len = sysconf(_SC_ARG_MAX);
60
16
  if ((size_t)-1 == cmd_max_len) {
61
0
#ifdef _POSIX_ARG_MAX
62
0
    cmd_max_len = _POSIX_ARG_MAX;
63
#else
64
    cmd_max_len = 4096;
65
#endif
66
0
  }
67
#elif defined(ARG_MAX)
68
  cmd_max_len = ARG_MAX;
69
#elif defined(PHP_WIN32)
70
  /* Executed commands will run through cmd.exe. As long as it's the case,
71
    it's just the constant limit.*/
72
  cmd_max_len = 8192;
73
#else
74
  /* This is just an arbitrary value for the fallback case. */
75
  cmd_max_len = 4096;
76
#endif
77
78
16
  return SUCCESS;
79
16
}
80
/* }}} */
81
82
0
static size_t strip_trailing_whitespace(char *buf, size_t bufl) {
83
0
  size_t l = bufl;
84
0
  while (l-- > 0 && isspace(((unsigned char *)buf)[l]));
85
0
  if (l != (bufl - 1)) {
86
0
    bufl = l + 1;
87
0
    buf[bufl] = '\0';
88
0
  }
89
0
  return bufl;
90
0
}
91
92
0
static size_t handle_line(int type, zval *array, char *buf, size_t bufl) {
93
0
  if (type == 1) {
94
0
    PHPWRITE(buf, bufl);
95
0
    if (php_output_get_level() < 1) {
96
0
      sapi_flush();
97
0
    }
98
0
  } else if (type == 2) {
99
0
    bufl = strip_trailing_whitespace(buf, bufl);
100
0
    add_next_index_stringl(array, buf, bufl);
101
0
  }
102
0
  return bufl;
103
0
}
104
105
/* {{{ php_exec
106
 * If type==0, only last line of output is returned (exec)
107
 * If type==1, all lines will be printed and last lined returned (system)
108
 * If type==2, all lines will be saved to given array (exec with &$array)
109
 * If type==3, output will be printed binary, no lines will be saved or returned (passthru)
110
 *
111
 */
112
PHPAPI int php_exec(int type, const char *cmd, zval *array, zval *return_value)
113
0
{
114
0
  FILE *fp;
115
0
  char *buf;
116
0
  int pclose_return;
117
0
  char *b, *d=NULL;
118
0
  php_stream *stream;
119
0
  size_t buflen, bufl = 0;
120
#if PHP_SIGCHILD
121
  void (*sig_handler)(int) = signal(SIGCHLD, SIG_DFL);
122
#endif
123
124
#ifdef PHP_WIN32
125
  fp = VCWD_POPEN(cmd, "rb");
126
#else
127
0
  fp = VCWD_POPEN(cmd, "r");
128
0
#endif
129
0
  if (!fp) {
130
0
    php_error_docref(NULL, E_WARNING, "Unable to fork [%s]", cmd);
131
0
    goto err;
132
0
  }
133
134
0
  stream = php_stream_fopen_from_pipe(fp, "rb");
135
136
0
  buf = (char *) emalloc(EXEC_INPUT_BUF);
137
0
  buflen = EXEC_INPUT_BUF;
138
139
0
  if (type != 3) {
140
0
    b = buf;
141
142
0
    while (php_stream_get_line(stream, b, EXEC_INPUT_BUF, &bufl)) {
143
      /* no new line found, let's read some more */
144
0
      if (b[bufl - 1] != '\n' && !php_stream_eof(stream)) {
145
0
        if (buflen < (bufl + (b - buf) + EXEC_INPUT_BUF)) {
146
0
          bufl += b - buf;
147
0
          buflen = bufl + EXEC_INPUT_BUF;
148
0
          buf = erealloc(buf, buflen);
149
0
          b = buf + bufl;
150
0
        } else {
151
0
          b += bufl;
152
0
        }
153
0
        continue;
154
0
      } else if (b != buf) {
155
0
        bufl += b - buf;
156
0
      }
157
158
0
      bufl = handle_line(type, array, buf, bufl);
159
0
      b = buf;
160
0
    }
161
0
    if (bufl) {
162
0
      if (buf != b) {
163
        /* Process remaining output */
164
0
        bufl = handle_line(type, array, buf, bufl);
165
0
      }
166
167
      /* Return last line from the shell command */
168
0
      bufl = strip_trailing_whitespace(buf, bufl);
169
0
      RETVAL_STRINGL(buf, bufl);
170
0
    } else { /* should return NULL, but for BC we return "" */
171
0
      RETVAL_EMPTY_STRING();
172
0
    }
173
0
  } else {
174
0
    ssize_t read;
175
0
    while ((read = php_stream_read(stream, buf, EXEC_INPUT_BUF)) > 0) {
176
0
      PHPWRITE(buf, read);
177
0
    }
178
0
  }
179
180
0
  pclose_return = php_stream_close(stream);
181
0
  efree(buf);
182
183
0
done:
184
#if PHP_SIGCHILD
185
  if (sig_handler) {
186
    signal(SIGCHLD, sig_handler);
187
  }
188
#endif
189
0
  if (d) {
190
0
    efree(d);
191
0
  }
192
0
  return pclose_return;
193
0
err:
194
0
  pclose_return = -1;
195
0
  RETVAL_FALSE;
196
0
  goto done;
197
0
}
198
/* }}} */
199
200
static void php_exec_ex(INTERNAL_FUNCTION_PARAMETERS, int mode) /* {{{ */
201
0
{
202
0
  char *cmd;
203
0
  size_t cmd_len;
204
0
  zval *ret_code=NULL, *ret_array=NULL;
205
0
  int ret;
206
207
0
  ZEND_PARSE_PARAMETERS_START(1, (mode ? 2 : 3))
208
0
    Z_PARAM_STRING(cmd, cmd_len)
209
0
    Z_PARAM_OPTIONAL
210
0
    if (!mode) {
211
0
      Z_PARAM_ZVAL(ret_array)
212
0
    }
213
0
    Z_PARAM_ZVAL(ret_code)
214
0
  ZEND_PARSE_PARAMETERS_END();
215
216
0
  if (!cmd_len) {
217
0
    zend_argument_must_not_be_empty_error(1);
218
0
    RETURN_THROWS();
219
0
  }
220
0
  if (strlen(cmd) != cmd_len) {
221
0
    zend_argument_value_error(1, "must not contain any null bytes");
222
0
    RETURN_THROWS();
223
0
  }
224
225
0
  if (!ret_array) {
226
0
    ret = php_exec(mode, cmd, NULL, return_value);
227
0
  } else {
228
0
    if (Z_TYPE_P(Z_REFVAL_P(ret_array)) == IS_ARRAY) {
229
0
      ZVAL_DEREF(ret_array);
230
0
      SEPARATE_ARRAY(ret_array);
231
0
    } else {
232
0
      ret_array = zend_try_array_init(ret_array);
233
0
      if (!ret_array) {
234
0
        RETURN_THROWS();
235
0
      }
236
0
    }
237
238
0
    ret = php_exec(2, cmd, ret_array, return_value);
239
0
  }
240
0
  if (ret_code) {
241
0
    ZEND_TRY_ASSIGN_REF_LONG(ret_code, ret);
242
0
  }
243
0
}
244
/* }}} */
245
246
/* {{{ Execute an external program */
247
PHP_FUNCTION(exec)
248
0
{
249
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
250
0
}
251
/* }}} */
252
253
/* {{{ Execute an external program and display output */
254
PHP_FUNCTION(system)
255
0
{
256
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
257
0
}
258
/* }}} */
259
260
/* {{{ Execute an external program and display raw output */
261
PHP_FUNCTION(passthru)
262
0
{
263
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 3);
264
0
}
265
/* }}} */
266
267
/* {{{ php_escape_shell_cmd
268
   Escape all chars that could possibly be used to
269
   break out of a shell command
270
271
   This function returns an owned zend_string, remember to release it when done.
272
273
   *NOT* safe for binary strings
274
*/
275
PHPAPI zend_string *php_escape_shell_cmd(const zend_string *unescaped_cmd)
276
0
{
277
0
  size_t x, y;
278
0
  zend_string *cmd;
279
0
#ifndef PHP_WIN32
280
0
  char *p = NULL;
281
0
#endif
282
283
0
  ZEND_ASSERT(ZSTR_LEN(unescaped_cmd) == strlen(ZSTR_VAL(unescaped_cmd)) && "Must be a binary safe string");
284
0
  size_t l = ZSTR_LEN(unescaped_cmd);
285
0
  const char *str = ZSTR_VAL(unescaped_cmd);
286
287
0
  uint64_t estimate = (2 * (uint64_t)l) + 1;
288
289
  /* max command line length - two single quotes - \0 byte length */
290
0
  if (l > cmd_max_len - 2 - 1) {
291
0
    zend_value_error("Command exceeds the allowed length of %zu bytes", cmd_max_len);
292
0
    return ZSTR_EMPTY_ALLOC();
293
0
  }
294
295
0
  cmd = zend_string_safe_alloc(2, l, 0, 0);
296
297
0
  for (x = 0, y = 0; x < l; x++) {
298
0
    int mb_len = php_mblen(str + x, (l - x));
299
300
    /* skip non-valid multibyte characters */
301
0
    if (mb_len < 0) {
302
0
      continue;
303
0
    } else if (mb_len > 1) {
304
0
      memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
305
0
      y += mb_len;
306
0
      x += mb_len - 1;
307
0
      continue;
308
0
    }
309
310
0
    switch (str[x]) {
311
0
#ifndef PHP_WIN32
312
0
      case '"':
313
0
      case '\'':
314
0
        if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {
315
          /* noop */
316
0
        } else if (p && *p == str[x]) {
317
0
          p = NULL;
318
0
        } else {
319
0
          ZSTR_VAL(cmd)[y++] = '\\';
320
0
        }
321
0
        ZSTR_VAL(cmd)[y++] = str[x];
322
0
        break;
323
#else
324
      /* % is Windows specific for environmental variables, ^%PATH% will
325
        output PATH while ^%PATH^% will not. escapeshellcmd->val will escape all % and !.
326
      */
327
      case '%':
328
      case '!':
329
      case '"':
330
      case '\'':
331
#endif
332
0
      case '#': /* This is character-set independent */
333
0
      case '&':
334
0
      case ';':
335
0
      case '`':
336
0
      case '|':
337
0
      case '*':
338
0
      case '?':
339
0
      case '~':
340
0
      case '<':
341
0
      case '>':
342
0
      case '^':
343
0
      case '(':
344
0
      case ')':
345
0
      case '[':
346
0
      case ']':
347
0
      case '{':
348
0
      case '}':
349
0
      case '$':
350
0
      case '\\':
351
0
      case '\x0A': /* excluding these two */
352
0
      case '\xFF':
353
#ifdef PHP_WIN32
354
        ZSTR_VAL(cmd)[y++] = '^';
355
#else
356
0
        ZSTR_VAL(cmd)[y++] = '\\';
357
0
#endif
358
0
        ZEND_FALLTHROUGH;
359
0
      default:
360
0
        ZSTR_VAL(cmd)[y++] = str[x];
361
362
0
    }
363
0
  }
364
0
  ZSTR_VAL(cmd)[y] = '\0';
365
366
0
  if (y > cmd_max_len + 1) {
367
0
    zend_value_error("Escaped command exceeds the allowed length of %zu bytes", cmd_max_len);
368
0
    zend_string_release_ex(cmd, 0);
369
0
    return ZSTR_EMPTY_ALLOC();
370
0
  }
371
372
0
  if ((estimate - y) > 4096) {
373
    /* realloc if the estimate was way overill
374
     * Arbitrary cutoff point of 4096 */
375
0
    cmd = zend_string_truncate(cmd, y, 0);
376
0
  }
377
378
0
  ZSTR_LEN(cmd) = y;
379
380
0
  return cmd;
381
0
}
382
/* }}} */
383
384
/* {{{ php_escape_shell_arg */
385
PHPAPI zend_string *php_escape_shell_arg(const zend_string *unescaped_arg)
386
15
{
387
15
  size_t x, y = 0;
388
15
  zend_string *cmd;
389
390
15
  ZEND_ASSERT(ZSTR_LEN(unescaped_arg) == strlen(ZSTR_VAL(unescaped_arg)) && "Must be a binary safe string");
391
15
  size_t l = ZSTR_LEN(unescaped_arg);
392
15
  const char *str = ZSTR_VAL(unescaped_arg);
393
394
15
  uint64_t estimate = (4 * (uint64_t)l) + 3;
395
396
  /* max command line length - two single quotes - \0 byte length */
397
15
  if (l > cmd_max_len - 2 - 1) {
398
0
    zend_value_error("Argument exceeds the allowed length of %zu bytes", cmd_max_len);
399
0
    return ZSTR_EMPTY_ALLOC();
400
0
  }
401
402
15
  cmd = zend_string_safe_alloc(4, l, 2, 0); /* worst case */
403
404
#ifdef PHP_WIN32
405
  ZSTR_VAL(cmd)[y++] = '"';
406
#else
407
15
  ZSTR_VAL(cmd)[y++] = '\'';
408
15
#endif
409
410
413
  for (x = 0; x < l; x++) {
411
398
    int mb_len = php_mblen(str + x, (l - x));
412
413
    /* skip non-valid multibyte characters */
414
398
    if (mb_len < 0) {
415
12
      continue;
416
386
    } else if (mb_len > 1) {
417
0
      memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
418
0
      y += mb_len;
419
0
      x += mb_len - 1;
420
0
      continue;
421
0
    }
422
423
386
    switch (str[x]) {
424
#ifdef PHP_WIN32
425
    case '"':
426
    case '%':
427
    case '!':
428
      ZSTR_VAL(cmd)[y++] = ' ';
429
      break;
430
#else
431
0
    case '\'':
432
0
      ZSTR_VAL(cmd)[y++] = '\'';
433
0
      ZSTR_VAL(cmd)[y++] = '\\';
434
0
      ZSTR_VAL(cmd)[y++] = '\'';
435
0
#endif
436
0
      ZEND_FALLTHROUGH;
437
386
    default:
438
386
      ZSTR_VAL(cmd)[y++] = str[x];
439
386
    }
440
386
  }
441
#ifdef PHP_WIN32
442
  if (y > 0 && '\\' == ZSTR_VAL(cmd)[y - 1]) {
443
    int k = 0, n = y - 1;
444
    for (; n >= 0 && '\\' == ZSTR_VAL(cmd)[n]; n--, k++);
445
    if (k % 2) {
446
      ZSTR_VAL(cmd)[y++] = '\\';
447
    }
448
  }
449
450
  ZSTR_VAL(cmd)[y++] = '"';
451
#else
452
15
  ZSTR_VAL(cmd)[y++] = '\'';
453
15
#endif
454
15
  ZSTR_VAL(cmd)[y] = '\0';
455
456
15
  if (y > cmd_max_len + 1) {
457
0
    zend_value_error("Escaped argument exceeds the allowed length of %zu bytes", cmd_max_len);
458
0
    zend_string_release_ex(cmd, 0);
459
0
    return ZSTR_EMPTY_ALLOC();
460
0
  }
461
462
15
  if ((estimate - y) > 4096) {
463
    /* realloc if the estimate was way overill
464
     * Arbitrary cutoff point of 4096 */
465
0
    cmd = zend_string_truncate(cmd, y, 0);
466
0
  }
467
15
  ZSTR_LEN(cmd) = y;
468
15
  return cmd;
469
15
}
470
/* }}} */
471
472
/* {{{ Escape shell metacharacters */
473
PHP_FUNCTION(escapeshellcmd)
474
0
{
475
0
  zend_string *command;
476
477
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
478
0
    Z_PARAM_PATH_STR(command)
479
0
  ZEND_PARSE_PARAMETERS_END();
480
481
0
  if (ZSTR_LEN(command)) {
482
0
    RETVAL_STR(php_escape_shell_cmd(command));
483
0
  } else {
484
0
    RETVAL_EMPTY_STRING();
485
0
  }
486
0
}
487
/* }}} */
488
489
/* {{{ Quote and escape an argument for use in a shell command */
490
PHP_FUNCTION(escapeshellarg)
491
15
{
492
15
  zend_string *argument;
493
494
45
  ZEND_PARSE_PARAMETERS_START(1, 1)
495
60
    Z_PARAM_PATH_STR(argument)
496
15
  ZEND_PARSE_PARAMETERS_END();
497
498
15
  RETVAL_STR(php_escape_shell_arg(argument));
499
15
}
500
/* }}} */
501
502
/* {{{ Execute command via shell and return complete output as string */
503
PHP_FUNCTION(shell_exec)
504
0
{
505
0
  FILE *in;
506
0
  char *command;
507
0
  size_t command_len;
508
0
  zend_string *ret;
509
0
  php_stream *stream;
510
511
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
512
0
    Z_PARAM_PATH(command, command_len)
513
0
  ZEND_PARSE_PARAMETERS_END();
514
515
0
  if (!command_len) {
516
0
    zend_argument_must_not_be_empty_error(1);
517
0
    RETURN_THROWS();
518
0
  }
519
520
#ifdef PHP_WIN32
521
  if ((in=VCWD_POPEN(command, "rt"))==NULL) {
522
#else
523
0
  if ((in=VCWD_POPEN(command, "r"))==NULL) {
524
0
#endif
525
0
    php_error_docref(NULL, E_WARNING, "Unable to execute '%s'", command);
526
0
    RETURN_FALSE;
527
0
  }
528
529
0
  stream = php_stream_fopen_from_pipe(in, "rb");
530
0
  ret = php_stream_copy_to_mem(stream, PHP_STREAM_COPY_ALL, 0);
531
0
  php_stream_close(stream);
532
533
0
  if (ret && ZSTR_LEN(ret) > 0) {
534
0
    RETVAL_STR(ret);
535
0
  }
536
0
}
537
/* }}} */
538
539
#ifdef HAVE_NICE
540
/* {{{ Change the priority of the current process */
541
PHP_FUNCTION(proc_nice)
542
0
{
543
0
  zend_long pri;
544
545
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
546
0
    Z_PARAM_LONG(pri)
547
0
  ZEND_PARSE_PARAMETERS_END();
548
549
0
  errno = 0;
550
0
  php_ignore_value(nice(pri));
551
0
  if (errno) {
552
#ifdef PHP_WIN32
553
    char *err = php_win_err();
554
    php_error_docref(NULL, E_WARNING, "%s", err);
555
    php_win_err_free(err);
556
#else
557
0
    php_error_docref(NULL, E_WARNING, "Only a super user may attempt to increase the priority of a process");
558
0
#endif
559
0
    RETURN_FALSE;
560
0
  }
561
562
0
  RETURN_TRUE;
563
0
}
564
/* }}} */
565
#endif