From: Nikita Popov Date: Fri, 13 Sep 2019 13:15:46 +0000 (+0200) Subject: Various improvements to fuzzer SAPIs X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=c4e2ca607f49d37564aaf34f5a48c5e59aca12a6;p=php Various improvements to fuzzer SAPIs --- diff --git a/configure.ac b/configure.ac index e15b83ca25..48756f3c6c 100644 --- a/configure.ac +++ b/configure.ac @@ -897,6 +897,18 @@ else ZEND_DEBUG=no fi +PHP_ARG_ENABLE([debug-assertions], + [whether to enable debug assertions in release mode], + [AS_HELP_STRING([--enable-debug-assertions], + [Compile with debug assertions even in release mode])], + [no], + [no]) + +if test "$PHP_DEBUG_ASSERTIONS" = "yes"; then + PHP_DEBUG=1 + ZEND_DEBUG=yes +fi + PHP_ARG_ENABLE([rtld-now], [whether to dlopen extensions with RTLD_NOW instead of RTLD_LAZY], [AS_HELP_STRING([--enable-rtld-now], diff --git a/sapi/fuzzer/README b/sapi/fuzzer/README deleted file mode 100644 index e0aafcaadf..0000000000 --- a/sapi/fuzzer/README +++ /dev/null @@ -1,13 +0,0 @@ -Fuzzing SAPI for PHP - -Enable fuzzing targets with --enable-fuzzer switch. - -Your compiler should support -fsanitize=address and you need -to have Fuzzer library around. - -When running `make` it creates these binaries in `sapi/fuzzer/`: -* php-fuzz-parser - fuzzing language parser -* php-fuzz-unserialize - fuzzing unserialize() function -* php-fuzz-json - fuzzing JSON parser -* php-fuzz-exif - fuzzing exif_read_data() function (use --enable-exif) -* php-fuzz-mbstring - fuzzing mb_ereg[i] (requires --enable-mbstring) diff --git a/sapi/fuzzer/README.md b/sapi/fuzzer/README.md new file mode 100644 index 0000000000..2cdbb1c5a3 --- /dev/null +++ b/sapi/fuzzer/README.md @@ -0,0 +1,50 @@ +Fuzzing SAPI for PHP +-------------------- + +The following `./configure` options can be used to enable the fuzzing SAPI, as well as all availablefuzzers. If you don't build the exif/json/mbstring extensions, fuzzers for these extensions will not be built. + +```sh +./configure \ + --enable-fuzzer \ + --with-pic \ + --enable-debug-assertions \ + --enable-exif \ + --enable-json \ + --enable-mbstring +``` + +The `--with-pic` option is required to avoid a linking failure. The `--enable-debug-assertions` option can be used to enable debug assertions despite the use of a release build. + +You will need a recent version of clang that supports the `-fsanitize=fuzzer-no-link` option. + +When running `make` it creates these binaries in `sapi/fuzzer/`: + +* `php-fuzz-parser`: Fuzzing language parser and compiler +* `php-fuzz-unserialize`: Fuzzing unserialize() function +* `php-fuzz-json`: Fuzzing JSON parser (requires --enable-json) +* `php-fuzz-exif`: Fuzzing `exif_read_data()` function (requires --enable-exif) +* `php-fuzz-mbstring`: fuzzing `mb_ereg[i]()` (requires --enable-mbstring) + +Some fuzzers have a seed corpus in `sapi/fuzzer/corpus`. You can use it as follows: + +```sh +cp -r sapi/fuzzer/corpus/exif ./my-exif-corpus +sapi/fuzzer/php-fuzz-exif ./my-exif-corpus +``` + +For the unserialize fuzzer, a dictionary of internal classes should be generated first: + +```sh +sapi/cli/php sapi/fuzzer/corpus/generate_unserialize_dict.php +cp -r sapi/fuzzer/corpus/unserialize ./my-unserialize-corpus +sapi/fuzzer/php-fuzz-unserialize -dict=$PWD/sapi/fuzzer/corpus/unserialize.dict ./my-unserialize-corpus +``` + +For the parser fuzzer, a corpus may be generated from Zend test files: + +```sh +sapi/cli/php sapi/fuzzer/corpus/generate_parser_corpus.php +mkdir ./my-parser-corpus +sapi/fuzzer/php-fuzz-parser -merge=1 ./my-parser-corpus sapi/fuzzer/corpus/parser +sapi/fuzzer/php-fuzz-parser -only_ascii=1 ./my-parser-corpus +``` diff --git a/sapi/fuzzer/config.m4 b/sapi/fuzzer/config.m4 index 12cf99bf86..bf08123bfb 100644 --- a/sapi/fuzzer/config.m4 +++ b/sapi/fuzzer/config.m4 @@ -14,7 +14,8 @@ dnl AC_DEFUN([PHP_FUZZER_TARGET], [ PHP_FUZZER_BINARIES="$PHP_FUZZER_BINARIES $SAPI_FUZZER_PATH/php-fuzz-$1" PHP_SUBST($2) - PHP_ADD_SOURCES_X([sapi/fuzzer],[fuzzer-$1.c fuzzer-sapi.c],[],$2) + PHP_ADD_SOURCES_X([sapi/fuzzer],[fuzzer-$1.c],[],$2) + $2="[$]$2 $FUZZER_COMMON_OBJS" ]) if test "$PHP_FUZZER" != "no"; then @@ -24,14 +25,20 @@ if test "$PHP_FUZZER" != "no"; then SAPI_FUZZER_PATH=sapi/fuzzer PHP_SUBST(SAPI_FUZZER_PATH) if test -z "$LIB_FUZZING_ENGINE"; then - FUZZING_LIB="-lFuzzer" + FUZZING_LIB="-fsanitize=fuzzer" FUZZING_CC="$CC" - AX_CHECK_COMPILE_FLAG([-fsanitize=address], [ - CFLAGS="$CFLAGS -fsanitize=address" - CXXFLAGS="$CXXFLAGS -fsanitize=address" - LDFLAGS="$LDFLAGS -fsanitize=address" + dnl Don't include -fundefined in CXXFLAGS, because that would also require linking + dnl with a C++ compiler. + AX_CHECK_COMPILE_FLAG([-fsanitize=fuzzer-no-link], [ + CFLAGS="$CFLAGS -fsanitize=fuzzer-no-link,address" + dnl Disable object-size sanitizer, because it is incompatible with our zend_function + dnl union, and this can't be easily fixed. + dnl We need to specify -fno-sanitize-recover=undefined here, otherwise ubsan warnings + dnl will not be considered failures by the fuzzer. + CFLAGS="$CFLAGS -fsanitize=undefined -fno-sanitize=object-size -fno-sanitize-recover=undefined" + CXXFLAGS="$CXXFLAGS -fsanitize=fuzzer-no-link,address" ],[ - AC_MSG_ERROR(compiler doesn't support -fsanitize flags) + AC_MSG_ERROR(Compiler doesn't support -fsanitize=fuzzer-no-link) ]) else FUZZING_LIB="-lFuzzingEngine" @@ -44,15 +51,21 @@ if test "$PHP_FUZZER" != "no"; then PHP_ADD_BUILD_DIR([sapi/fuzzer]) PHP_FUZZER_BINARIES="" + PHP_BINARIES="$PHP_BINARIES fuzzer" PHP_INSTALLED_SAPIS="$PHP_INSTALLED_SAPIS fuzzer" + PHP_ADD_SOURCES_X([sapi/fuzzer], [fuzzer-sapi.c], [], FUZZER_COMMON_OBJS) + PHP_FUZZER_TARGET([parser], PHP_FUZZER_PARSER_OBJS) PHP_FUZZER_TARGET([unserialize], PHP_FUZZER_UNSERIALIZE_OBJS) - PHP_FUZZER_TARGET([exif], PHP_FUZZER_EXIF_OBJS) - if test -n "$enable_json" && test "$enable_json" != "no"; then + dnl json extension is enabled by default + if (test -n "$enable_json" && test "$enable_json" != "no") || test -z "$PHP_ENABLE_ALL"; then PHP_FUZZER_TARGET([json], PHP_FUZZER_JSON_OBJS) fi + if test -n "$enable_exif" && test "$enable_exif" != "no"; then + PHP_FUZZER_TARGET([exif], PHP_FUZZER_EXIF_OBJS) + fi if test -n "$enable_mbstring" && test "$enable_mbstring" != "no"; then PHP_FUZZER_TARGET([mbstring], PHP_FUZZER_MBSTRING_OBJS) fi diff --git a/sapi/fuzzer/corpus/generate_parser_corpus.php b/sapi/fuzzer/corpus/generate_parser_corpus.php new file mode 100644 index 0000000000..7d9cdf98d1 --- /dev/null +++ b/sapi/fuzzer/corpus/generate_parser_corpus.php @@ -0,0 +1,22 @@ + #include #include
-#ifdef JO0 -#include -#endif #include "fuzzer.h" - #include "fuzzer-sapi.h" -int fuzzer_do_parse(zend_file_handle *file_handle, char *filename) -{ - int retval = FAILURE; /* failure by default */ - - SG(options) |= SAPI_OPTION_NO_CHDIR; - SG(request_info).argc=0; - SG(request_info).argv=NULL; - - if (php_request_startup(TSRMLS_C)==FAILURE) { - php_module_shutdown(TSRMLS_C); - return FAILURE; - } - - SG(headers_sent) = 1; - SG(request_info).no_headers = 1; - php_register_variable("PHP_SELF", filename, NULL TSRMLS_CC); - - zend_first_try { - zend_compile_file(file_handle, ZEND_REQUIRE); - //retval = php_execute_script(file_handle TSRMLS_CC); - } zend_end_try(); - - php_request_shutdown((void *) 0); - - return (retval == SUCCESS) ? SUCCESS : FAILURE; -} - -int fuzzer_do_request_d(char *filename, char *data, size_t data_len); - int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { char *s = malloc(Size+1); memcpy(s, Data, Size); s[Size] = '\0'; - fuzzer_do_request_d("fuzzer.php", Data, Size); - //fuzzer_do_parse(&file_handle, "fuzzer.php"); + fuzzer_do_request_from_buffer("fuzzer.php", s, Size); - free(s); + /* Do not free s: fuzzer_do_request_from_buffer() takes ownership of the allocation. */ 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"); + fuzzer_init_php(); /* fuzzer_shutdown_php(); */ diff --git a/sapi/fuzzer/fuzzer-sapi.c b/sapi/fuzzer/fuzzer-sapi.c index dd26c3c103..da1df37943 100644 --- a/sapi/fuzzer/fuzzer-sapi.c +++ b/sapi/fuzzer/fuzzer-sapi.c @@ -24,6 +24,10 @@ #include #include
+#ifdef __SANITIZE_ADDRESS__ +# include "sanitizer/lsan_interface.h" +#endif + #include "fuzzer.h" #include "fuzzer-sapi.h" @@ -31,7 +35,8 @@ const char HARDCODED_INI[] = "html_errors=0\n" "implicit_flush=1\n" "max_execution_time=20\n" - "output_buffering=0\n"; + "output_buffering=0\n" + "error_reporting=0"; static int startup(sapi_module_struct *sapi_module) { @@ -41,7 +46,7 @@ static int startup(sapi_module_struct *sapi_module) return SUCCESS; } -static size_t ub_write(const char *str, size_t str_length TSRMLS_DC) +static size_t ub_write(const char *str, size_t str_length) { /* quiet */ return str_length; @@ -52,22 +57,22 @@ static void fuzzer_flush(void *server_context) /* quiet */ } -static void send_header(sapi_header_struct *sapi_header, void *server_context TSRMLS_DC) +static void send_header(sapi_header_struct *sapi_header, void *server_context) { } -static char* read_cookies(TSRMLS_D) +static char* read_cookies() { /* TODO: fuzz these! */ return NULL; } -static void register_variables(zval *track_vars_array TSRMLS_DC) +static void register_variables(zval *track_vars_array) { - php_import_environment_variables(track_vars_array TSRMLS_CC); + php_import_environment_variables(track_vars_array); } -static void log_message(char *message, int level TSRMLS_DC) +static void log_message(char *message, int level) { } @@ -106,6 +111,12 @@ static sapi_module_struct fuzzer_module = { int fuzzer_init_php() { +#ifdef __SANITIZE_ADDRESS__ + /* We're going to leak all the memory allocated during startup, + * so disable lsan temporarily. */ + __lsan_disable(); +#endif + sapi_startup(&fuzzer_module); fuzzer_module.phpinfo_as_text = 1; @@ -118,15 +129,30 @@ int fuzzer_init_php() */ putenv("USE_ZEND_ALLOC=0"); + if (fuzzer_module.startup(&fuzzer_module)==FAILURE) { + return FAILURE; + } + #ifdef __SANITIZE_ADDRESS__ - /* Not very interested in memory leak detection, since Zend MM does that */ - __lsan_disable(); + __lsan_enable(); #endif - if (fuzzer_module.startup(&fuzzer_module)==FAILURE) { + return SUCCESS; +} + +int fuzzer_request_startup() +{ + if (php_request_startup() == FAILURE) { + php_module_shutdown(); return FAILURE; } +#ifdef ZEND_SIGNALS + /* Some signal handlers will be overriden, + * don't complain about them during shutdown. */ + SIGG(check) = 0; +#endif + return SUCCESS; } @@ -141,9 +167,7 @@ void fuzzer_set_ini_file(const char *file) int fuzzer_shutdown_php() { - TSRMLS_FETCH(); - - php_module_shutdown(TSRMLS_C); + php_module_shutdown(); sapi_shutdown(); free(fuzzer_module.ini_entries); @@ -158,18 +182,25 @@ int fuzzer_do_request(zend_file_handle *file_handle, char *filename) SG(request_info).argc=0; SG(request_info).argv=NULL; - if (php_request_startup(TSRMLS_C)==FAILURE) { - php_module_shutdown(TSRMLS_C); + if (fuzzer_request_startup() == FAILURE) { return FAILURE; } SG(headers_sent) = 1; SG(request_info).no_headers = 1; - php_register_variable("PHP_SELF", filename, NULL TSRMLS_CC); + php_register_variable("PHP_SELF", filename, NULL); zend_first_try { - zend_compile_file(file_handle, ZEND_REQUIRE); - /*retval = php_execute_script(file_handle TSRMLS_CC);*/ + zend_op_array *op_array = zend_compile_file(file_handle, ZEND_REQUIRE); + if (op_array) { + destroy_op_array(op_array); + efree(op_array); + } + if (EG(exception)) { + zend_object_release(EG(exception)); + EG(exception) = NULL; + } + /*retval = php_execute_script(file_handle);*/ } zend_end_try(); php_request_shutdown((void *) 0); @@ -189,10 +220,11 @@ int fuzzer_do_request_f(char *filename) return fuzzer_do_request(&file_handle, filename); } -int fuzzer_do_request_d(char *filename, char *data, size_t data_len) +int fuzzer_do_request_from_buffer(char *filename, char *data, size_t data_len) { zend_file_handle file_handle; file_handle.filename = filename; + file_handle.free_filename = 0; file_handle.opened_path = NULL; file_handle.handle.stream.handle = NULL; file_handle.handle.stream.reader = (zend_stream_reader_t)_php_stream_read; @@ -209,11 +241,10 @@ int fuzzer_do_request_d(char *filename, char *data, size_t data_len) // Call named PHP function with N zval arguments void fuzzer_call_php_func_zval(const char *func_name, int nargs, zval *args) { zval retval, func; - int result; ZVAL_STRING(&func, func_name); ZVAL_UNDEF(&retval); - result = call_user_function(CG(function_table), NULL, &func, &retval, nargs, args); + call_user_function(CG(function_table), NULL, &func, &retval, nargs, args); // TODO: check result? /* to ensure retval is not broken */ diff --git a/sapi/fuzzer/fuzzer-sapi.h b/sapi/fuzzer/fuzzer-sapi.h index 92ce95b86a..cce8080b2c 100644 --- a/sapi/fuzzer/fuzzer-sapi.h +++ b/sapi/fuzzer/fuzzer-sapi.h @@ -18,5 +18,7 @@ */ int fuzzer_init_php(); +int fuzzer_request_startup(); 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, char *data, size_t data_len); diff --git a/sapi/fuzzer/fuzzer-unserialize.c b/sapi/fuzzer/fuzzer-unserialize.c index 9b843f25fe..9cf040944a 100644 --- a/sapi/fuzzer/fuzzer-unserialize.c +++ b/sapi/fuzzer/fuzzer-unserialize.c @@ -32,28 +32,54 @@ #include "ext/standard/php_var.h" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { - unsigned char *data = malloc(Size+1); + unsigned char *orig_data = malloc(Size+1); + zend_execute_data execute_data; + zend_function func; - memcpy(data, Data, Size); - data[Size] = '\0'; + memcpy(orig_data, Data, Size); + orig_data[Size] = '\0'; - if (php_request_startup()==FAILURE) { - php_module_shutdown(); + if (fuzzer_request_startup()==FAILURE) { return 0; } - zval result; + /* Set up a dummy stack frame so that exceptions may be thrown. */ + { + memset(&execute_data, 0, sizeof(zend_execute_data)); + memset(&func, 0, sizeof(zend_function)); - php_unserialize_data_t var_hash; - PHP_VAR_UNSERIALIZE_INIT(var_hash); - php_var_unserialize(&result, &data, data + Size, &var_hash); - PHP_VAR_UNSERIALIZE_DESTROY(var_hash); + func.type = ZEND_INTERNAL_FUNCTION; + func.common.function_name = ZSTR_EMPTY_ALLOC(); + execute_data.func = &func; + EG(current_execute_data) = &execute_data; + } + + { + const unsigned char *data = orig_data; + zval result; + ZVAL_UNDEF(&result); + + php_unserialize_data_t var_hash; + PHP_VAR_UNSERIALIZE_INIT(var_hash); + php_var_unserialize(&result, (const unsigned char **) &data, data + Size, &var_hash); + PHP_VAR_UNSERIALIZE_DESTROY(var_hash); - zval_ptr_dtor(&result); + zval_ptr_dtor(&result); + + /* Destroy any thrown exception. */ + if (EG(exception)) { + zend_object_release(EG(exception)); + EG(exception) = NULL; + } + } + /* Unserialize may create circular structure. Make sure we free them. + * Two calls are performed to handle objects with destructors. */ + zend_gc_collect_cycles(); + zend_gc_collect_cycles(); php_request_shutdown(NULL); - free(data); + free(orig_data); return 0; }