diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index a50cf630b9c..b70ddcc3e40 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -2811,6 +2811,7 @@ static inline int accel_find_sapi(void) "apache2handler", "litespeed", "uwsgi", + "fuzzer", NULL }; const char **sapi_name; diff --git a/sapi/fuzzer/Makefile.frag b/sapi/fuzzer/Makefile.frag index 50feac3a9ab..fc8fa8410cb 100644 --- a/sapi/fuzzer/Makefile.frag +++ b/sapi/fuzzer/Makefile.frag @@ -8,6 +8,9 @@ $(SAPI_FUZZER_PATH)/php-fuzz-parser: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_F $(SAPI_FUZZER_PATH)/php-fuzz-execute: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_FUZZER_EXECUTE_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_EXECUTE_OBJS) -o $@ +$(SAPI_FUZZER_PATH)/php-fuzz-jit: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_FUZZER_JIT_OBJS) + $(FUZZER_BUILD) $(PHP_FUZZER_JIT_OBJS) -o $@ + $(SAPI_FUZZER_PATH)/php-fuzz-unserialize: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_FUZZER_UNSERIALIZE_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_UNSERIALIZE_OBJS) -o $@ diff --git a/sapi/fuzzer/README.md b/sapi/fuzzer/README.md index 3da8534187e..e8f60ac40ad 100644 --- a/sapi/fuzzer/README.md +++ b/sapi/fuzzer/README.md @@ -30,6 +30,7 @@ When running `make` it creates these binaries in `sapi/fuzzer/`: * `php-fuzz-exif`: Fuzzing `exif_read_data()` function (requires --enable-exif) * `php-fuzz-mbstring`: Fuzzing `mb_ereg[i]()` (requires --enable-mbstring) * `php-fuzz-execute`: Fuzzing the executor +* `php-fuzz-jit`: Fuzzing the function JIT (requires --enable-opcache) Some fuzzers have a seed corpus in `sapi/fuzzer/corpus`. You can use it as follows: diff --git a/sapi/fuzzer/config.m4 b/sapi/fuzzer/config.m4 index 55ad91c7a6b..f7125365d0e 100644 --- a/sapi/fuzzer/config.m4 +++ b/sapi/fuzzer/config.m4 @@ -54,6 +54,7 @@ if test "$PHP_FUZZER" != "no"; then PHP_FUZZER_TARGET([parser], PHP_FUZZER_PARSER_OBJS) PHP_FUZZER_TARGET([execute], PHP_FUZZER_EXECUTE_OBJS) + PHP_FUZZER_TARGET([jit], PHP_FUZZER_JIT_OBJS) PHP_FUZZER_TARGET([unserialize], PHP_FUZZER_UNSERIALIZE_OBJS) PHP_FUZZER_TARGET([unserializehash], PHP_FUZZER_UNSERIALIZEHASH_OBJS) PHP_FUZZER_TARGET([json], PHP_FUZZER_JSON_OBJS) diff --git a/sapi/fuzzer/fuzzer-execute-common.h b/sapi/fuzzer/fuzzer-execute-common.h new file mode 100644 index 00000000000..092a662c42f --- /dev/null +++ b/sapi/fuzzer/fuzzer-execute-common.h @@ -0,0 +1,99 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Nikita Popov | + +----------------------------------------------------------------------+ + */ + +#include
+ +#include "fuzzer.h" +#include "fuzzer-sapi.h" + +#define MAX_STEPS 1000 +#define MAX_SIZE (8 * 1024) +static uint32_t steps_left; +static bool bailed_out = false; + +/* Because the fuzzer is always compiled with clang, + * we can assume that we don't use global registers / hybrid VM. */ +typedef int (ZEND_FASTCALL *opcode_handler_t)(zend_execute_data *); + +static zend_always_inline void fuzzer_step(void) { + if (--steps_left == 0) { + /* Reset steps before bailing out, so code running after bailout (e.g. in + * destructors) will get another MAX_STEPS, rather than UINT32_MAX steps. */ + steps_left = MAX_STEPS; + bailed_out = true; + zend_bailout(); + } +} + +static void fuzzer_execute_ex(zend_execute_data *execute_data) { + while (1) { + int ret; + fuzzer_step(); + if ((ret = ((opcode_handler_t) EX(opline)->handler)(execute_data)) != 0) { + if (ret > 0) { + execute_data = EG(current_execute_data); + } else { + return; + } + } + } +} + +static zend_op_array *(*orig_compile_string)(zend_string *source_string, const char *filename); + +static zend_op_array *fuzzer_compile_string(zend_string *str, const char *filename) { + if (ZSTR_LEN(str) > MAX_SIZE) { + /* Avoid compiling huge inputs via eval(). */ + zend_bailout(); + } + + return orig_compile_string(str, filename); +} + +static void (*orig_execute_internal)(zend_execute_data *execute_data, zval *return_value); + +static void fuzzer_execute_internal(zend_execute_data *execute_data, zval *return_value) { + fuzzer_step(); + + uint32_t num_args = ZEND_CALL_NUM_ARGS(execute_data); + for (uint32_t i = 0; i < num_args; i++) { + /* Some internal functions like preg_replace() may be slow on large inputs. + * Limit the maximum size of string inputs. */ + zval *arg = ZEND_CALL_VAR_NUM(execute_data, i); + if (Z_TYPE_P(arg) == IS_STRING && Z_STRLEN_P(arg) > MAX_SIZE) { + zend_bailout(); + } + } + + orig_execute_internal(execute_data, return_value); +} + +static void fuzzer_init_php_for_execute(const char *extra_ini) { + /* Compilation will often trigger fatal errors. + * Use tracked allocation mode to avoid leaks in that case. */ + putenv("USE_TRACKED_ALLOC=1"); + + /* Just like other SAPIs, ignore SIGPIPEs. */ + signal(SIGPIPE, SIG_IGN); + + fuzzer_init_php(extra_ini); + + zend_execute_ex = fuzzer_execute_ex; + orig_execute_internal = zend_execute_internal ? zend_execute_internal : execute_internal; + zend_execute_internal = fuzzer_execute_internal; + orig_compile_string = zend_compile_string; + zend_compile_string = fuzzer_compile_string; +} diff --git a/sapi/fuzzer/fuzzer-execute.c b/sapi/fuzzer/fuzzer-execute.c index abc5ebff2ec..75bacf8e75f 100644 --- a/sapi/fuzzer/fuzzer-execute.c +++ b/sapi/fuzzer/fuzzer-execute.c @@ -14,70 +14,7 @@ +----------------------------------------------------------------------+ */ -#include
- -#include "fuzzer.h" -#include "fuzzer-sapi.h" - -#define MAX_STEPS 1000 -#define MAX_SIZE (8 * 1024) -static uint32_t steps_left; - -/* Because the fuzzer is always compiled with clang, - * we can assume that we don't use global registers / hybrid VM. */ -typedef int (ZEND_FASTCALL *opcode_handler_t)(zend_execute_data *); - -static zend_always_inline void fuzzer_step(void) { - if (--steps_left == 0) { - /* Reset steps before bailing out, so code running after bailout (e.g. in - * destructors) will get another MAX_STEPS, rather than UINT32_MAX steps. */ - steps_left = MAX_STEPS; - zend_bailout(); - } -} - -static void fuzzer_execute_ex(zend_execute_data *execute_data) { - while (1) { - int ret; - fuzzer_step(); - if ((ret = ((opcode_handler_t) EX(opline)->handler)(execute_data)) != 0) { - if (ret > 0) { - execute_data = EG(current_execute_data); - } else { - return; - } - } - } -} - -static zend_op_array *(*orig_compile_string)(zend_string *source_string, const char *filename); - -static zend_op_array *fuzzer_compile_string(zend_string *str, const char *filename) { - if (ZSTR_LEN(str) > MAX_SIZE) { - /* Avoid compiling huge inputs via eval(). */ - zend_bailout(); - } - - return orig_compile_string(str, filename); -} - -static void (*orig_execute_internal)(zend_execute_data *execute_data, zval *return_value); - -static void fuzzer_execute_internal(zend_execute_data *execute_data, zval *return_value) { - fuzzer_step(); - - uint32_t num_args = ZEND_CALL_NUM_ARGS(execute_data); - for (uint32_t i = 0; i < num_args; i++) { - /* Some internal functions like preg_replace() may be slow on large inputs. - * Limit the maximum size of string inputs. */ - zval *arg = ZEND_CALL_VAR_NUM(execute_data, i); - if (Z_TYPE_P(arg) == IS_STRING && Z_STRLEN_P(arg) > MAX_SIZE) { - zend_bailout(); - } - } - - orig_execute_internal(execute_data, return_value); -} +#include "fuzzer-execute-common.h" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { if (Size > MAX_SIZE) { @@ -87,27 +24,13 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { } steps_left = MAX_STEPS; - fuzzer_do_request_from_buffer("/fuzzer.php", (const char *) Data, Size, /* execute */ 1); + fuzzer_do_request_from_buffer( + "/fuzzer.php", (const char *) Data, Size, /* execute */ 1, /* before_shutdown */ NULL); return 0; } int LLVMFuzzerInitialize(int *argc, char ***argv) { - /* Compilation will often trigger fatal errors. - * Use tracked allocation mode to avoid leaks in that case. */ - putenv("USE_TRACKED_ALLOC=1"); - - /* Just like other SAPIs, ignore SIGPIPEs. */ - signal(SIGPIPE, SIG_IGN); - - fuzzer_init_php(); - - zend_execute_ex = fuzzer_execute_ex; - orig_execute_internal = zend_execute_internal ? zend_execute_internal : execute_internal; - zend_execute_internal = fuzzer_execute_internal; - orig_compile_string = zend_compile_string; - zend_compile_string = fuzzer_compile_string; - - /* fuzzer_shutdown_php(); */ + fuzzer_init_php_for_execute(NULL); return 0; } diff --git a/sapi/fuzzer/fuzzer-exif.c b/sapi/fuzzer/fuzzer-exif.c index 8cf1ce06752..cb16bb90233 100644 --- a/sapi/fuzzer/fuzzer-exif.c +++ b/sapi/fuzzer/fuzzer-exif.c @@ -64,7 +64,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { } int LLVMFuzzerInitialize(int *argc, char ***argv) { - fuzzer_init_php(); + fuzzer_init_php(NULL); /* fuzzer_shutdown_php(); */ return 0; diff --git a/sapi/fuzzer/fuzzer-jit.c b/sapi/fuzzer/fuzzer-jit.c new file mode 100644 index 00000000000..04313bf9b74 --- /dev/null +++ b/sapi/fuzzer/fuzzer-jit.c @@ -0,0 +1,86 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Nikita Popov | + +----------------------------------------------------------------------+ + */ + +#include "fuzzer-execute-common.h" +#include "zend_exceptions.h" + +static void opcache_invalidate(void) { + steps_left = MAX_STEPS; + zend_exception_save(); + zval retval, func, args[2]; + ZVAL_STRING(&func, "opcache_invalidate"); + ZVAL_STRING(&args[0], "/fuzzer.php"); + ZVAL_TRUE(&args[1]); + call_user_function(CG(function_table), NULL, &func, &retval, 2, args); + ZEND_ASSERT(Z_TYPE(retval) == IS_TRUE); + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&retval); + zval_ptr_dtor(&func); + zend_exception_restore(); +} + +int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size > MAX_SIZE) { + /* Large inputs have a large impact on fuzzer performance, + * but are unlikely to be necessary to reach new codepaths. */ + return 0; + } + + zend_string *jit_option = zend_string_init("opcache.jit", sizeof("opcache.jit") - 1, 1); + + /* First run without JIT to determine whether we bail out. We should not run JITed code if + * we bail out here, as the JIT code may loop infinitely. */ + steps_left = MAX_STEPS; + bailed_out = false; + zend_alter_ini_entry_chars( + jit_option, "off", sizeof("off")-1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + fuzzer_do_request_from_buffer( + "/fuzzer.php", (const char *) Data, Size, /* execute */ 1, opcache_invalidate); + + if (!bailed_out) { + steps_left = MAX_STEPS; + zend_alter_ini_entry_chars(jit_option, + "function", sizeof("function")-1, PHP_INI_USER, PHP_INI_STAGE_RUNTIME); + fuzzer_do_request_from_buffer( + "/fuzzer.php", (const char *) Data, Size, /* execute */ 1, opcache_invalidate); + } + + zend_string_release(jit_option); + + return 0; +} + +char *get_opcache_path(void) { + // TODO: Make this more general. + char *opcache_path = "modules/opcache.so"; + return realpath(opcache_path, NULL); +} + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + char *opcache_path = get_opcache_path(); + assert(opcache_path && "Failed to determine opcache path"); + + char ini_buf[256]; + snprintf(ini_buf, sizeof(ini_buf), + "zend_extension=%s\n" + "opcache.validate_timestamps=0\n" + "opcache.file_update_protection=0\n" + "opcache.jit_buffer_size=512M", + opcache_path); + free(opcache_path); + fuzzer_init_php_for_execute(ini_buf); + return 0; +} diff --git a/sapi/fuzzer/fuzzer-json.c b/sapi/fuzzer/fuzzer-json.c index a154bc3b8a5..4335598bc3c 100644 --- a/sapi/fuzzer/fuzzer-json.c +++ b/sapi/fuzzer/fuzzer-json.c @@ -55,7 +55,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { } int LLVMFuzzerInitialize(int *argc, char ***argv) { - fuzzer_init_php(); + fuzzer_init_php(NULL); /* fuzzer_shutdown_php(); */ return 0; diff --git a/sapi/fuzzer/fuzzer-mbstring.c b/sapi/fuzzer/fuzzer-mbstring.c index 142552a3337..970a7b5baee 100644 --- a/sapi/fuzzer/fuzzer-mbstring.c +++ b/sapi/fuzzer/fuzzer-mbstring.c @@ -68,7 +68,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { } int LLVMFuzzerInitialize(int *argc, char ***argv) { - fuzzer_init_php(); + fuzzer_init_php(NULL); /* The default parse depth limit allows stack overflows under asan. */ onig_set_parse_depth_limit(512); diff --git a/sapi/fuzzer/fuzzer-parser.c b/sapi/fuzzer/fuzzer-parser.c index 61cedcfc1cd..55669c70c02 100644 --- a/sapi/fuzzer/fuzzer-parser.c +++ b/sapi/fuzzer/fuzzer-parser.c @@ -32,7 +32,8 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { return 0; } - fuzzer_do_request_from_buffer("fuzzer.php", (const char *) Data, Size, /* execute */ 0); + fuzzer_do_request_from_buffer( + "fuzzer.php", (const char *) Data, Size, /* execute */ 0, /* before_shutdown */ NULL); return 0; } @@ -42,7 +43,7 @@ int LLVMFuzzerInitialize(int *argc, char ***argv) { * Use tracked allocation mode to avoid leaks in that case. */ putenv("USE_TRACKED_ALLOC=1"); - fuzzer_init_php(); + fuzzer_init_php(NULL); /* fuzzer_shutdown_php(); */ return 0; diff --git a/sapi/fuzzer/fuzzer-sapi.c b/sapi/fuzzer/fuzzer-sapi.c index 95c2b1ce956..7f019b4452c 100644 --- a/sapi/fuzzer/fuzzer-sapi.c +++ b/sapi/fuzzer/fuzzer-sapi.c @@ -131,7 +131,7 @@ static sapi_module_struct fuzzer_module = { STANDARD_SAPI_MODULE_PROPERTIES }; -int fuzzer_init_php() +int fuzzer_init_php(const char *extra_ini) { #ifdef __SANITIZE_ADDRESS__ /* We're going to leak all the memory allocated during startup, @@ -142,8 +142,20 @@ int fuzzer_init_php() sapi_startup(&fuzzer_module); fuzzer_module.phpinfo_as_text = 1; - fuzzer_module.ini_entries = malloc(sizeof(HARDCODED_INI)); - memcpy(fuzzer_module.ini_entries, HARDCODED_INI, sizeof(HARDCODED_INI)); + size_t ini_len = sizeof(HARDCODED_INI); + size_t extra_ini_len = extra_ini ? strlen(extra_ini) : 0; + if (extra_ini) { + ini_len += extra_ini_len + 1; + } + char *p = fuzzer_module.ini_entries = malloc(ini_len + 1); + memcpy(p, HARDCODED_INI, sizeof(HARDCODED_INI) - 1); + p += sizeof(HARDCODED_INI) - 1; + if (extra_ini) { + *p++ = '\n'; + memcpy(p, extra_ini, extra_ini_len); + p += extra_ini_len; + } + *p = '\0'; /* * TODO: we might want to test both Zend and malloc MM, but testing with malloc @@ -230,7 +242,8 @@ int fuzzer_shutdown_php(void) } int fuzzer_do_request_from_buffer( - char *filename, const char *data, size_t data_len, bool execute) + char *filename, const char *data, size_t data_len, bool execute, + void (*before_shutdown)(void)) { int retval = FAILURE; /* failure by default */ @@ -253,6 +266,8 @@ int fuzzer_do_request_from_buffer( file_handle.primary_script = 1; file_handle.buf = estrndup(data, data_len); file_handle.len = data_len; + /* Avoid ZEND_HANDLE_FILENAME for opcache. */ + file_handle.type = ZEND_HANDLE_STREAM; zend_op_array *op_array = zend_compile_file(&file_handle, ZEND_REQUIRE); zend_destroy_file_handle(&file_handle); @@ -267,6 +282,9 @@ int fuzzer_do_request_from_buffer( } zend_end_try(); CG(compiled_filename) = NULL; /* ??? */ + if (before_shutdown) { + before_shutdown(); + } fuzzer_request_shutdown(); return (retval == SUCCESS) ? SUCCESS : FAILURE; diff --git a/sapi/fuzzer/fuzzer-sapi.h b/sapi/fuzzer/fuzzer-sapi.h index 076a5714569..4ee2cb3fbc1 100644 --- a/sapi/fuzzer/fuzzer-sapi.h +++ b/sapi/fuzzer/fuzzer-sapi.h @@ -15,11 +15,12 @@ +----------------------------------------------------------------------+ */ -int fuzzer_init_php(void); +int fuzzer_init_php(const char *extra_ini); int fuzzer_request_startup(void); void fuzzer_request_shutdown(void); void fuzzer_setup_dummy_frame(void); void fuzzer_call_php_func(const char *func_name, int nargs, char **params); void fuzzer_call_php_func_zval(const char *func_name, int nargs, zval *args); int fuzzer_do_request_from_buffer( - char *filename, const char *data, size_t data_len, bool execute); + char *filename, const char *data, size_t data_len, bool execute, + void (*before_shutdown)(void)); diff --git a/sapi/fuzzer/fuzzer-unserialize.c b/sapi/fuzzer/fuzzer-unserialize.c index efae256a499..ff26e5b1e8d 100644 --- a/sapi/fuzzer/fuzzer-unserialize.c +++ b/sapi/fuzzer/fuzzer-unserialize.c @@ -60,7 +60,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { } int LLVMFuzzerInitialize(int *argc, char ***argv) { - fuzzer_init_php(); + fuzzer_init_php(NULL); /* fuzzer_shutdown_php(); */ return 0; diff --git a/sapi/fuzzer/fuzzer-unserializehash.c b/sapi/fuzzer/fuzzer-unserializehash.c index 0f7d81a3d5b..5d29eb5fb8c 100644 --- a/sapi/fuzzer/fuzzer-unserializehash.c +++ b/sapi/fuzzer/fuzzer-unserializehash.c @@ -75,7 +75,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t FullSize) { } int LLVMFuzzerInitialize(int *argc, char ***argv) { - fuzzer_init_php(); + fuzzer_init_php(NULL); /* fuzzer_shutdown_php(); */ return 0;