Coverage Report

Created: 2025-09-27 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/php-src/ext/standard/exec.c
Line
Count
Source
1
/*
2
   +----------------------------------------------------------------------+
3
   | Copyright (c) The PHP Group                                          |
4
   +----------------------------------------------------------------------+
5
   | This source file is subject to version 3.01 of the PHP license,      |
6
   | that is bundled with this package in the file LICENSE, and is        |
7
   | available through the world-wide-web at the following url:           |
8
   | https://www.php.net/license/3_01.txt                                 |
9
   | If you did not receive a copy of the PHP license and are unable to   |
10
   | obtain it through the world-wide-web, please send a note to          |
11
   | license@php.net so we can mail you a copy immediately.               |
12
   +----------------------------------------------------------------------+
13
   | Author: 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
  zend_string *cmd;
203
0
  zval *ret_code=NULL, *ret_array=NULL;
204
0
  int ret;
205
206
0
  ZEND_PARSE_PARAMETERS_START(1, (mode ? 2 : 3))
207
0
    Z_PARAM_PATH_STR(cmd)
208
0
    Z_PARAM_OPTIONAL
209
0
    if (!mode) {
210
0
      Z_PARAM_ZVAL(ret_array)
211
0
    }
212
0
    Z_PARAM_ZVAL(ret_code)
213
0
  ZEND_PARSE_PARAMETERS_END();
214
215
0
  if (UNEXPECTED(!ZSTR_LEN(cmd))) {
216
0
    zend_argument_must_not_be_empty_error(1);
217
0
    RETURN_THROWS();
218
0
  }
219
220
0
  if (!ret_array) {
221
0
    ret = php_exec(mode, ZSTR_VAL(cmd), NULL, return_value);
222
0
  } else {
223
0
    if (Z_TYPE_P(Z_REFVAL_P(ret_array)) == IS_ARRAY) {
224
0
      ZVAL_DEREF(ret_array);
225
0
      SEPARATE_ARRAY(ret_array);
226
0
    } else {
227
0
      ret_array = zend_try_array_init(ret_array);
228
0
      if (!ret_array) {
229
0
        RETURN_THROWS();
230
0
      }
231
0
    }
232
233
0
    ret = php_exec(2, ZSTR_VAL(cmd), ret_array, return_value);
234
0
  }
235
0
  if (ret_code) {
236
0
    ZEND_TRY_ASSIGN_REF_LONG(ret_code, ret);
237
0
  }
238
0
}
239
/* }}} */
240
241
/* {{{ Execute an external program */
242
PHP_FUNCTION(exec)
243
0
{
244
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
245
0
}
246
/* }}} */
247
248
/* {{{ Execute an external program and display output */
249
PHP_FUNCTION(system)
250
0
{
251
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
252
0
}
253
/* }}} */
254
255
/* {{{ Execute an external program and display raw output */
256
PHP_FUNCTION(passthru)
257
0
{
258
0
  php_exec_ex(INTERNAL_FUNCTION_PARAM_PASSTHRU, 3);
259
0
}
260
/* }}} */
261
262
/* {{{ php_escape_shell_cmd
263
   Escape all chars that could possibly be used to
264
   break out of a shell command
265
266
   This function returns an owned zend_string, remember to release it when done.
267
268
   *NOT* safe for binary strings
269
*/
270
PHPAPI zend_string *php_escape_shell_cmd(const zend_string *unescaped_cmd)
271
0
{
272
0
  size_t x, y;
273
0
  zend_string *cmd;
274
0
#ifndef PHP_WIN32
275
0
  char *p = NULL;
276
0
#endif
277
278
0
  ZEND_ASSERT(!zend_str_has_nul_byte(unescaped_cmd) && "Must be a binary safe string");
279
0
  size_t l = ZSTR_LEN(unescaped_cmd);
280
0
  const char *str = ZSTR_VAL(unescaped_cmd);
281
282
0
  uint64_t estimate = (2 * (uint64_t)l) + 1;
283
284
  /* max command line length - two single quotes - \0 byte length */
285
0
  if (l > cmd_max_len - 2 - 1) {
286
0
    zend_value_error("Command exceeds the allowed length of %zu bytes", cmd_max_len);
287
0
    return ZSTR_EMPTY_ALLOC();
288
0
  }
289
290
0
  cmd = zend_string_safe_alloc(2, l, 0, 0);
291
292
0
  for (x = 0, y = 0; x < l; x++) {
293
0
    int mb_len = php_mblen(str + x, (l - x));
294
295
    /* skip non-valid multibyte characters */
296
0
    if (mb_len < 0) {
297
0
      continue;
298
0
    } else if (mb_len > 1) {
299
0
      memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
300
0
      y += mb_len;
301
0
      x += mb_len - 1;
302
0
      continue;
303
0
    }
304
305
0
    switch (str[x]) {
306
0
#ifndef PHP_WIN32
307
0
      case '"':
308
0
      case '\'':
309
0
        if (!p && (p = memchr(str + x + 1, str[x], l - x - 1))) {
310
          /* noop */
311
0
        } else if (p && *p == str[x]) {
312
0
          p = NULL;
313
0
        } else {
314
0
          ZSTR_VAL(cmd)[y++] = '\\';
315
0
        }
316
0
        ZSTR_VAL(cmd)[y++] = str[x];
317
0
        break;
318
#else
319
      /* % is Windows specific for environmental variables, ^%PATH% will
320
        output PATH while ^%PATH^% will not. escapeshellcmd->val will escape all % and !.
321
      */
322
      case '%':
323
      case '!':
324
      case '"':
325
      case '\'':
326
#endif
327
0
      case '#': /* This is character-set independent */
328
0
      case '&':
329
0
      case ';':
330
0
      case '`':
331
0
      case '|':
332
0
      case '*':
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 '\x0A': /* excluding these two */
347
0
      case '\xFF':
348
#ifdef PHP_WIN32
349
        ZSTR_VAL(cmd)[y++] = '^';
350
#else
351
0
        ZSTR_VAL(cmd)[y++] = '\\';
352
0
#endif
353
0
        ZEND_FALLTHROUGH;
354
0
      default:
355
0
        ZSTR_VAL(cmd)[y++] = str[x];
356
357
0
    }
358
0
  }
359
0
  ZSTR_VAL(cmd)[y] = '\0';
360
361
0
  if (y > cmd_max_len + 1) {
362
0
    zend_value_error("Escaped command exceeds the allowed length of %zu bytes", cmd_max_len);
363
0
    zend_string_release_ex(cmd, 0);
364
0
    return ZSTR_EMPTY_ALLOC();
365
0
  }
366
367
0
  if ((estimate - y) > 4096) {
368
    /* realloc if the estimate was way overill
369
     * Arbitrary cutoff point of 4096 */
370
0
    cmd = zend_string_truncate(cmd, y, 0);
371
0
  }
372
373
0
  ZSTR_LEN(cmd) = y;
374
375
0
  return cmd;
376
0
}
377
/* }}} */
378
379
/* {{{ php_escape_shell_arg */
380
PHPAPI zend_string *php_escape_shell_arg(const zend_string *unescaped_arg)
381
35
{
382
35
  size_t x, y = 0;
383
35
  zend_string *cmd;
384
385
35
  ZEND_ASSERT(!zend_str_has_nul_byte(unescaped_arg) && "Must be a binary safe string");
386
35
  size_t l = ZSTR_LEN(unescaped_arg);
387
35
  const char *str = ZSTR_VAL(unescaped_arg);
388
389
35
  uint64_t estimate = (4 * (uint64_t)l) + 3;
390
391
  /* max command line length - two single quotes - \0 byte length */
392
35
  if (l > cmd_max_len - 2 - 1) {
393
0
    zend_value_error("Argument exceeds the allowed length of %zu bytes", cmd_max_len);
394
0
    return ZSTR_EMPTY_ALLOC();
395
0
  }
396
397
35
  cmd = zend_string_safe_alloc(4, l, 2, 0); /* worst case */
398
399
#ifdef PHP_WIN32
400
  ZSTR_VAL(cmd)[y++] = '"';
401
#else
402
35
  ZSTR_VAL(cmd)[y++] = '\'';
403
35
#endif
404
405
1.13k
  for (x = 0; x < l; x++) {
406
1.10k
    int mb_len = php_mblen(str + x, (l - x));
407
408
    /* skip non-valid multibyte characters */
409
1.10k
    if (mb_len < 0) {
410
143
      continue;
411
958
    } else if (mb_len > 1) {
412
0
      memcpy(ZSTR_VAL(cmd) + y, str + x, mb_len);
413
0
      y += mb_len;
414
0
      x += mb_len - 1;
415
0
      continue;
416
0
    }
417
418
958
    switch (str[x]) {
419
#ifdef PHP_WIN32
420
    case '"':
421
    case '%':
422
    case '!':
423
      ZSTR_VAL(cmd)[y++] = ' ';
424
      break;
425
#else
426
0
    case '\'':
427
0
      ZSTR_VAL(cmd)[y++] = '\'';
428
0
      ZSTR_VAL(cmd)[y++] = '\\';
429
0
      ZSTR_VAL(cmd)[y++] = '\'';
430
0
#endif
431
0
      ZEND_FALLTHROUGH;
432
958
    default:
433
958
      ZSTR_VAL(cmd)[y++] = str[x];
434
958
    }
435
958
  }
436
#ifdef PHP_WIN32
437
  if (y > 0 && '\\' == ZSTR_VAL(cmd)[y - 1]) {
438
    int k = 0, n = y - 1;
439
    for (; n >= 0 && '\\' == ZSTR_VAL(cmd)[n]; n--, k++);
440
    if (k % 2) {
441
      ZSTR_VAL(cmd)[y++] = '\\';
442
    }
443
  }
444
445
  ZSTR_VAL(cmd)[y++] = '"';
446
#else
447
35
  ZSTR_VAL(cmd)[y++] = '\'';
448
35
#endif
449
35
  ZSTR_VAL(cmd)[y] = '\0';
450
451
35
  if (y > cmd_max_len + 1) {
452
0
    zend_value_error("Escaped argument exceeds the allowed length of %zu bytes", cmd_max_len);
453
0
    zend_string_release_ex(cmd, 0);
454
0
    return ZSTR_EMPTY_ALLOC();
455
0
  }
456
457
35
  if ((estimate - y) > 4096) {
458
    /* realloc if the estimate was way overill
459
     * Arbitrary cutoff point of 4096 */
460
0
    cmd = zend_string_truncate(cmd, y, 0);
461
0
  }
462
35
  ZSTR_LEN(cmd) = y;
463
35
  return cmd;
464
35
}
465
/* }}} */
466
467
/* {{{ Escape shell metacharacters */
468
PHP_FUNCTION(escapeshellcmd)
469
0
{
470
0
  zend_string *command;
471
472
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
473
0
    Z_PARAM_PATH_STR(command)
474
0
  ZEND_PARSE_PARAMETERS_END();
475
476
0
  if (ZSTR_LEN(command)) {
477
0
    RETVAL_STR(php_escape_shell_cmd(command));
478
0
  } else {
479
0
    RETVAL_EMPTY_STRING();
480
0
  }
481
0
}
482
/* }}} */
483
484
/* {{{ Quote and escape an argument for use in a shell command */
485
PHP_FUNCTION(escapeshellarg)
486
38
{
487
38
  zend_string *argument;
488
489
114
  ZEND_PARSE_PARAMETERS_START(1, 1)
490
152
    Z_PARAM_PATH_STR(argument)
491
38
  ZEND_PARSE_PARAMETERS_END();
492
493
35
  RETVAL_STR(php_escape_shell_arg(argument));
494
35
}
495
/* }}} */
496
497
/* {{{ Execute command via shell and return complete output as string */
498
PHP_FUNCTION(shell_exec)
499
0
{
500
0
  FILE *in;
501
0
  char *command;
502
0
  size_t command_len;
503
0
  zend_string *ret;
504
0
  php_stream *stream;
505
506
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
507
0
    Z_PARAM_PATH(command, command_len)
508
0
  ZEND_PARSE_PARAMETERS_END();
509
510
0
  if (!command_len) {
511
0
    zend_argument_must_not_be_empty_error(1);
512
0
    RETURN_THROWS();
513
0
  }
514
515
#ifdef PHP_WIN32
516
  if ((in=VCWD_POPEN(command, "rt"))==NULL) {
517
#else
518
0
  if ((in=VCWD_POPEN(command, "r"))==NULL) {
519
0
#endif
520
0
    php_error_docref(NULL, E_WARNING, "Unable to execute '%s'", command);
521
0
    RETURN_FALSE;
522
0
  }
523
524
0
  stream = php_stream_fopen_from_pipe(in, "rb");
525
0
  ret = php_stream_copy_to_mem(stream, PHP_STREAM_COPY_ALL, 0);
526
0
  php_stream_close(stream);
527
528
0
  if (ret && ZSTR_LEN(ret) > 0) {
529
0
    RETVAL_STR(ret);
530
0
  }
531
0
}
532
/* }}} */
533
534
#ifdef HAVE_NICE
535
/* {{{ Change the priority of the current process */
536
PHP_FUNCTION(proc_nice)
537
0
{
538
0
  zend_long pri;
539
540
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
541
0
    Z_PARAM_LONG(pri)
542
0
  ZEND_PARSE_PARAMETERS_END();
543
544
0
  errno = 0;
545
0
  php_ignore_value(nice(pri));
546
0
  if (errno) {
547
#ifdef PHP_WIN32
548
    char *err = php_win_err();
549
    php_error_docref(NULL, E_WARNING, "%s", err);
550
    php_win_err_free(err);
551
#else
552
0
    php_error_docref(NULL, E_WARNING, "Only a super user may attempt to increase the priority of a process");
553
0
#endif
554
0
    RETURN_FALSE;
555
0
  }
556
557
0
  RETURN_TRUE;
558
0
}
559
/* }}} */
560
#endif