Coverage Report

Created: 2026-06-02 06:39

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