Coverage Report

Created: 2026-06-02 06:39

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