Coverage Report

Created: 2025-06-13 06:43

/src/php-src/sapi/fuzzer/fuzzer-sapi.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
   | Authors: Johannes Schlüter <johanes@php.net>                         |
14
   |          Stanislav Malyshev <stas@php.net>                           |
15
   +----------------------------------------------------------------------+
16
 */
17
18
#include <main/php.h>
19
#include <main/php_main.h>
20
#include <main/SAPI.h>
21
#include <ext/standard/info.h>
22
#include <ext/standard/php_var.h>
23
#include <main/php_variables.h>
24
#include <zend_exceptions.h>
25
26
#ifdef __SANITIZE_ADDRESS__
27
# include "sanitizer/lsan_interface.h"
28
#endif
29
30
#include "fuzzer.h"
31
#include "fuzzer-sapi.h"
32
33
static const char HARDCODED_INI[] =
34
  "html_errors=0\n"
35
  "implicit_flush=1\n"
36
  "output_buffering=0\n"
37
  "error_reporting=0\n"
38
  /* Let the timeout be enforced by libfuzzer, not PHP. */
39
  "max_execution_time=0\n"
40
  /* Reduce oniguruma limits to speed up fuzzing */
41
  "mbstring.regex_stack_limit=10000\n"
42
  "mbstring.regex_retry_limit=10000\n"
43
  /* For the "execute" fuzzer disable some functions that are likely to have
44
   * undesirable consequences (shell execution, file system writes). */
45
  "allow_url_include=0\n"
46
  "allow_url_fopen=0\n"
47
  "open_basedir=/tmp\n"
48
  "disable_functions=dl,mail,mb_send_mail"
49
  ",shell_exec,exec,system,proc_open,popen,passthru,pcntl_exec"
50
  ",chdir,chgrp,chmod,chown,copy,file_put_contents,lchgrp,lchown,link,mkdir"
51
  ",move_uploaded_file,rename,rmdir,symlink,tempname,touch,unlink,fopen"
52
  /* Networking code likes to wait and wait. */
53
  ",fsockopen,pfsockopen"
54
  ",stream_socket_pair,stream_socket_client,stream_socket_server"
55
  /* crypt() can be very slow. */
56
  ",crypt"
57
  /* openlog() has a known memory-management issue. */
58
  ",openlog"
59
  /* Can cause long loops that bypass the executor step limit. */
60
  "\ndisable_classes=InfiniteIterator"
61
;
62
63
static int startup(sapi_module_struct *sapi_module)
64
16
{
65
16
  return php_module_startup(sapi_module, NULL);
66
16
}
67
68
static size_t ub_write(const char *str, size_t str_length)
69
55.6M
{
70
  /* quiet */
71
55.6M
  return str_length;
72
55.6M
}
73
74
static void fuzzer_flush(void *server_context)
75
55.6M
{
76
  /* quiet */
77
55.6M
}
78
79
static void send_header(sapi_header_struct *sapi_header, void *server_context)
80
1.20M
{
81
1.20M
}
82
83
static char* read_cookies(void)
84
0
{
85
  /* TODO: fuzz these! */
86
0
  return NULL;
87
0
}
88
89
static void register_variables(zval *track_vars_array)
90
71
{
91
71
  php_import_environment_variables(track_vars_array);
92
71
}
93
94
static void log_message(const char *message, int level)
95
7
{
96
7
}
97
98
99
static sapi_module_struct fuzzer_module = {
100
  "fuzzer",               /* name */
101
  "clang fuzzer", /* pretty name */
102
103
  startup,             /* startup */
104
  php_module_shutdown_wrapper,   /* shutdown */
105
106
  NULL,                          /* activate */
107
  NULL,                          /* deactivate */
108
109
  ub_write,            /* unbuffered write */
110
  fuzzer_flush,               /* flush */
111
  NULL,                          /* get uid */
112
  NULL,                          /* getenv */
113
114
  php_error,                     /* error handler */
115
116
  NULL,                          /* header handler */
117
  NULL,                          /* send headers handler */
118
  send_header,         /* send header handler */
119
120
  NULL,                          /* read POST data */
121
  read_cookies,        /* read Cookies */
122
123
  register_variables,  /* register server variables */
124
  log_message,         /* Log message */
125
  NULL,                          /* Get request time */
126
  NULL,                          /* Child terminate */
127
128
  STANDARD_SAPI_MODULE_PROPERTIES
129
};
130
131
int fuzzer_init_php(const char *extra_ini)
132
16
{
133
#ifdef __SANITIZE_ADDRESS__
134
  /* We're going to leak all the memory allocated during startup,
135
   * so disable lsan temporarily. */
136
  __lsan_disable();
137
#endif
138
139
16
  sapi_startup(&fuzzer_module);
140
16
  fuzzer_module.phpinfo_as_text = 1;
141
142
16
  size_t ini_len = sizeof(HARDCODED_INI);
143
16
  size_t extra_ini_len = extra_ini ? strlen(extra_ini) : 0;
144
16
  if (extra_ini) {
145
4
    ini_len += extra_ini_len + 1;
146
4
  }
147
16
  char *p = malloc(ini_len + 1);
148
16
  fuzzer_module.ini_entries = p;
149
16
  p = zend_mempcpy(p, HARDCODED_INI, sizeof(HARDCODED_INI) - 1);
150
16
  if (extra_ini) {
151
4
    *p++ = '\n';
152
4
    p = zend_mempcpy(p, extra_ini, extra_ini_len);
153
4
  }
154
16
  *p = '\0';
155
156
  /*
157
   * TODO: we might want to test both Zend and malloc MM, but testing with malloc
158
   * is more likely to find bugs, so use that for now.
159
   */
160
16
  putenv("USE_ZEND_ALLOC=0");
161
162
16
  if (fuzzer_module.startup(&fuzzer_module)==FAILURE) {
163
0
    return FAILURE;
164
0
  }
165
166
#ifdef __SANITIZE_ADDRESS__
167
  __lsan_enable();
168
#endif
169
170
16
  return SUCCESS;
171
16
}
172
173
int fuzzer_request_startup(void)
174
300k
{
175
300k
  if (php_request_startup() == FAILURE) {
176
0
    php_module_shutdown();
177
0
    return FAILURE;
178
0
  }
179
180
300k
#ifdef ZEND_SIGNALS
181
  /* Some signal handlers will be overridden,
182
   * don't complain about them during shutdown. */
183
300k
  SIGG(check) = 0;
184
300k
#endif
185
186
300k
  return SUCCESS;
187
300k
}
188
189
void fuzzer_request_shutdown(void)
190
287k
{
191
287k
  zend_try {
192
    /* Destroy thrown exceptions. This does not happen as part of request shutdown. */
193
287k
    if (EG(exception)) {
194
183k
      zend_object_release(EG(exception));
195
183k
      EG(exception) = NULL;
196
183k
    }
197
198
    /* Some fuzzers (like unserialize) may create circular structures. Make sure we free them.
199
     * Two calls are performed to handle objects with destructors. */
200
287k
    zend_gc_collect_cycles();
201
287k
    zend_gc_collect_cycles();
202
287k
  } zend_end_try();
203
204
287k
  php_request_shutdown(NULL);
205
287k
}
206
207
/* Set up a dummy stack frame so that exceptions may be thrown. */
208
void fuzzer_setup_dummy_frame(void)
209
130k
{
210
130k
  static zend_execute_data execute_data;
211
130k
  static zend_function func;
212
213
130k
  memset(&execute_data, 0, sizeof(zend_execute_data));
214
130k
  memset(&func, 0, sizeof(zend_function));
215
216
130k
  func.type = ZEND_INTERNAL_FUNCTION;
217
130k
  func.common.function_name = ZSTR_EMPTY_ALLOC();
218
130k
  execute_data.func = &func;
219
130k
  EG(current_execute_data) = &execute_data;
220
130k
}
221
222
void fuzzer_set_ini_file(const char *file)
223
0
{
224
0
  if (fuzzer_module.php_ini_path_override) {
225
0
    free(fuzzer_module.php_ini_path_override);
226
0
  }
227
0
  fuzzer_module.php_ini_path_override = strdup(file);
228
0
}
229
230
231
int fuzzer_shutdown_php(void)
232
0
{
233
0
  php_module_shutdown();
234
0
  sapi_shutdown();
235
236
0
  free((void *)fuzzer_module.ini_entries);
237
0
  return SUCCESS;
238
0
}
239
240
int fuzzer_do_request_from_buffer(
241
    char *filename, const char *data, size_t data_len, bool execute,
242
    void (*before_shutdown)(void))
243
156k
{
244
156k
  int retval = FAILURE; /* failure by default */
245
246
156k
  SG(options) |= SAPI_OPTION_NO_CHDIR;
247
156k
  SG(request_info).argc=0;
248
156k
  SG(request_info).argv=NULL;
249
250
156k
  if (fuzzer_request_startup() == FAILURE) {
251
0
    return FAILURE;
252
0
  }
253
254
  // Commented out to avoid leaking the header callback.
255
  //SG(headers_sent) = 1;
256
  //SG(request_info).no_headers = 1;
257
156k
  php_register_variable("PHP_SELF", filename, NULL);
258
259
156k
  zend_first_try {
260
156k
    zend_file_handle file_handle;
261
156k
    zend_stream_init_filename(&file_handle, filename);
262
156k
    file_handle.primary_script = 1;
263
156k
    file_handle.buf = emalloc(data_len + ZEND_MMAP_AHEAD);
264
156k
    memcpy(file_handle.buf, data, data_len);
265
156k
    memset(file_handle.buf + data_len, 0, ZEND_MMAP_AHEAD);
266
156k
    file_handle.len = data_len;
267
    /* Avoid ZEND_HANDLE_FILENAME for opcache. */
268
156k
    file_handle.type = ZEND_HANDLE_STREAM;
269
270
156k
    zend_op_array *op_array = zend_compile_file(&file_handle, ZEND_REQUIRE);
271
156k
    zend_destroy_file_handle(&file_handle);
272
156k
    if (op_array) {
273
107k
      if (execute) {
274
83.6k
        zend_execute(op_array, NULL);
275
83.6k
      }
276
107k
      zend_destroy_static_vars(op_array);
277
107k
      destroy_op_array(op_array);
278
107k
      efree(op_array);
279
107k
    }
280
156k
  } zend_end_try();
281
282
156k
  CG(compiled_filename) = NULL; /* ??? */
283
156k
  if (before_shutdown) {
284
81.0k
    zend_try {
285
81.0k
      before_shutdown();
286
81.0k
    } zend_end_try();
287
81.0k
  }
288
156k
  fuzzer_request_shutdown();
289
290
156k
  return (retval == SUCCESS) ? SUCCESS : FAILURE;
291
156k
}
292
293
// Call named PHP function with N zval arguments
294
10.8k
void fuzzer_call_php_func_zval(const char *func_name, int nargs, zval *args) {
295
10.8k
  zval retval, func;
296
297
10.8k
  ZVAL_STRING(&func, func_name);
298
10.8k
  ZVAL_UNDEF(&retval);
299
10.8k
  call_user_function(CG(function_table), NULL, &func, &retval, nargs, args);
300
301
  // TODO: check result?
302
  /* to ensure retval is not broken */
303
10.8k
  php_var_dump(&retval, 0);
304
305
  /* cleanup */
306
10.8k
  zval_ptr_dtor(&retval);
307
10.8k
  zval_ptr_dtor(&func);
308
10.8k
}
309
310
// Call named PHP function with N string arguments
311
0
void fuzzer_call_php_func(const char *func_name, int nargs, char **params) {
312
0
  zval args[nargs];
313
0
  int i;
314
315
0
  for(i=0;i<nargs;i++) {
316
0
    ZVAL_STRING(&args[i], params[i]);
317
0
  }
318
319
0
  fuzzer_call_php_func_zval(func_name, nargs, args);
320
321
0
  for(i=0;i<nargs;i++) {
322
0
    zval_ptr_dtor(&args[i]);
323
0
    ZVAL_UNDEF(&args[i]);
324
0
  }
325
0
}