diff options
author | Nikita Popov <nikita.ppv@gmail.com> | 2019-10-09 19:17:07 +0200 |
---|---|---|
committer | Nikita Popov <nikita.ppv@gmail.com> | 2020-08-27 13:06:24 +0200 |
commit | c29838c561fdd6903d2ad8a5fa6820c1f26ab3c9 (patch) | |
tree | 736ef03db71e398731a519345d4f739026068430 /sapi | |
parent | 988fc94bbbfb44a63ef5aeb745d38c1a73e2db0f (diff) | |
download | php-git-c29838c561fdd6903d2ad8a5fa6820c1f26ab3c9.tar.gz |
Add experimental "execute" fuzzer
This is an end-to-end fuzzer that executes arbitrary PHP code.
We replace the executor with a finite-step executor to avoid
getting stuck in loops or recursion.
Diffstat (limited to 'sapi')
-rw-r--r-- | sapi/fuzzer/Makefile.frag | 3 | ||||
-rw-r--r-- | sapi/fuzzer/config.m4 | 1 | ||||
-rw-r--r-- | sapi/fuzzer/fuzzer-execute.c | 71 | ||||
-rw-r--r-- | sapi/fuzzer/fuzzer-parser.c | 8 | ||||
-rw-r--r-- | sapi/fuzzer/fuzzer-sapi.c | 94 | ||||
-rw-r--r-- | sapi/fuzzer/fuzzer-sapi.h | 3 | ||||
-rw-r--r-- | sapi/fuzzer/generate_execute_corpus.php | 38 |
7 files changed, 160 insertions, 58 deletions
diff --git a/sapi/fuzzer/Makefile.frag b/sapi/fuzzer/Makefile.frag index 75a387d3d4..50feac3a9a 100644 --- a/sapi/fuzzer/Makefile.frag +++ b/sapi/fuzzer/Makefile.frag @@ -5,6 +5,9 @@ FUZZER_BUILD = $(LIBTOOL) --mode=link $(FUZZING_CC) -export-dynamic $(CFLAGS_CLE $(SAPI_FUZZER_PATH)/php-fuzz-parser: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_FUZZER_PARSER_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_PARSER_OBJS) -o $@ +$(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-unserialize: $(PHP_GLOBAL_OBJS) $(PHP_SAPI_OBJS) $(PHP_FUZZER_UNSERIALIZE_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_UNSERIALIZE_OBJS) -o $@ diff --git a/sapi/fuzzer/config.m4 b/sapi/fuzzer/config.m4 index a148985df9..92327fc1d4 100644 --- a/sapi/fuzzer/config.m4 +++ b/sapi/fuzzer/config.m4 @@ -75,6 +75,7 @@ if test "$PHP_FUZZER" != "no"; then PHP_ADD_SOURCES_X([sapi/fuzzer], [fuzzer-sapi.c], [], FUZZER_COMMON_OBJS) PHP_FUZZER_TARGET([parser], PHP_FUZZER_PARSER_OBJS) + PHP_FUZZER_TARGET([execute], PHP_FUZZER_EXECUTE_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.c b/sapi/fuzzer/fuzzer-execute.c new file mode 100644 index 0000000000..bc903bbe63 --- /dev/null +++ b/sapi/fuzzer/fuzzer-execute.c @@ -0,0 +1,71 @@ +/* + +----------------------------------------------------------------------+ + | 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: | + | http://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 <nikic@php.net> | + +----------------------------------------------------------------------+ + */ + +#include <main/php.h> + +#include "fuzzer.h" +#include "fuzzer-sapi.h" + +#define MAX_STEPS 1000 +static uint32_t steps_left; + +void fuzzer_execute_ex(zend_execute_data *execute_data) { + while (1) { + int ret; + 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(); + } + + if ((ret = ((user_opcode_handler_t) EX(opline)->handler)(execute_data)) != 0) { + if (ret > 0) { + execute_data = EG(current_execute_data); + } else { + return; + } + } + } +} + +int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size > 16 * 1024) { + /* Large inputs have a large impact on fuzzer performance, + * but are unlikely to be necessary to reach new codepaths. */ + return 0; + } + + steps_left = MAX_STEPS; + fuzzer_do_request_from_buffer("/fuzzer.php", (const char *) Data, Size, /* execute */ 1); + + 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; + + /* fuzzer_shutdown_php(); */ + return 0; +} diff --git a/sapi/fuzzer/fuzzer-parser.c b/sapi/fuzzer/fuzzer-parser.c index 19f685f967..c81a6e1c24 100644 --- a/sapi/fuzzer/fuzzer-parser.c +++ b/sapi/fuzzer/fuzzer-parser.c @@ -26,20 +26,14 @@ #include "fuzzer-sapi.h" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { - char *s; if (Size > 32 * 1024) { /* Large inputs have a large impact on fuzzer performance, * but are unlikely to be necessary to reach new codepaths. */ return 0; } - s = malloc(Size+1); - memcpy(s, Data, Size); - s[Size] = '\0'; + fuzzer_do_request_from_buffer("fuzzer.php", (const char *) Data, Size, /* execute */ 0); - fuzzer_do_request_from_buffer("fuzzer.php", s, Size); - - /* Do not free s: fuzzer_do_request_from_buffer() takes ownership of the allocation. */ return 0; } diff --git a/sapi/fuzzer/fuzzer-sapi.c b/sapi/fuzzer/fuzzer-sapi.c index fdb4ff08b8..ff81073744 100644 --- a/sapi/fuzzer/fuzzer-sapi.c +++ b/sapi/fuzzer/fuzzer-sapi.c @@ -21,6 +21,7 @@ #include <ext/standard/info.h> #include <ext/standard/php_var.h> #include <main/php_variables.h> +#include <zend_exceptions.c> #ifdef __SANITIZE_ADDRESS__ # include "sanitizer/lsan_interface.h" @@ -34,9 +35,24 @@ const char HARDCODED_INI[] = "implicit_flush=1\n" "output_buffering=0\n" "error_reporting=0\n" + /* Let the timeout be enforced by libfuzzer, not PHP. */ + "max_execution_time=0\n" /* Reduce oniguruma limits to speed up fuzzing */ "mbstring.regex_stack_limit=10000\n" - "mbstring.regex_retry_limit=10000"; + "mbstring.regex_retry_limit=10000\n" + /* For the "execute" fuzzer disable some functions that are likely to have + * undesirable consequences (shell execution, file system writes). */ + "allow_url_include=0\n" + "allow_url_fopen=0\n" + "open_basedir=/tmp\n" + "disable_functions=dl,mail,mb_send_mail" + ",shell_exec,exec,system,proc_open,popen,passthru,pcntl_exec" + ",chgrp,chmod,chown,copy,file_put_contents,lchgrp,lchown,link,mkdir" + ",move_uploaded_file,rename,rmdir,symlink,tempname,touch,unlink,fopen" + ",fsockopen,stream_socket_pair,stream_socket_client" + /* openlog() has a known memory-management issue. */ + ",openlog" +; static int startup(sapi_module_struct *sapi_module) { @@ -158,16 +174,18 @@ int fuzzer_request_startup() void fuzzer_request_shutdown() { - /* Destroy thrown exceptions. This does not happen as part of request shutdown. */ - if (EG(exception)) { - zend_object_release(EG(exception)); - EG(exception) = NULL; - } + zend_try { + /* Destroy thrown exceptions. This does not happen as part of request shutdown. */ + if (EG(exception)) { + zend_object_release(EG(exception)); + EG(exception) = NULL; + } - /* Some fuzzers (like unserialize) may create circular structures. Make sure we free them. - * Two calls are performed to handle objects with destructors. */ - zend_gc_collect_cycles(); - zend_gc_collect_cycles(); + /* Some fuzzers (like unserialize) may create circular structures. Make sure we free them. + * Two calls are performed to handle objects with destructors. */ + zend_gc_collect_cycles(); + zend_gc_collect_cycles(); + } zend_end_try(); php_request_shutdown(NULL); } @@ -205,7 +223,8 @@ int fuzzer_shutdown_php() return SUCCESS; } -int fuzzer_do_request(zend_file_handle *file_handle, char *filename) +int fuzzer_do_request_from_buffer( + char *filename, const char *data, size_t data_len, zend_bool execute) { int retval = FAILURE; /* failure by default */ @@ -217,58 +236,33 @@ int fuzzer_do_request(zend_file_handle *file_handle, char *filename) return FAILURE; } - SG(headers_sent) = 1; - SG(request_info).no_headers = 1; + // Commented out to avoid leaking the header callback. + //SG(headers_sent) = 1; + //SG(request_info).no_headers = 1; php_register_variable("PHP_SELF", filename, NULL); zend_first_try { - zend_op_array *op_array = zend_compile_file(file_handle, ZEND_REQUIRE); + zend_file_handle file_handle; + zend_stream_init_filename(&file_handle, filename); + file_handle.buf = estrndup(data, data_len); + file_handle.len = data_len; + + zend_op_array *op_array = zend_compile_file(&file_handle, ZEND_REQUIRE); if (op_array) { + if (execute) { + zend_execute(op_array, NULL); + } 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); + CG(compiled_filename) = NULL; /* ??? */ + fuzzer_request_shutdown(); return (retval == SUCCESS) ? SUCCESS : FAILURE; } - -int fuzzer_do_request_f(char *filename) -{ - zend_file_handle file_handle; - file_handle.type = ZEND_HANDLE_FILENAME; - file_handle.filename = filename; - file_handle.handle.fp = NULL; - file_handle.opened_path = NULL; - - return fuzzer_do_request(&file_handle, filename); -} - -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; - file_handle.handle.stream.fsizer = NULL; - file_handle.handle.stream.isatty = 0; - file_handle.handle.stream.closer = NULL; - file_handle.buf = data; - file_handle.len = data_len; - file_handle.type = ZEND_HANDLE_STREAM; - - return fuzzer_do_request(&file_handle, filename); -} - // 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; diff --git a/sapi/fuzzer/fuzzer-sapi.h b/sapi/fuzzer/fuzzer-sapi.h index f079fbcc05..4eb050e357 100644 --- a/sapi/fuzzer/fuzzer-sapi.h +++ b/sapi/fuzzer/fuzzer-sapi.h @@ -21,4 +21,5 @@ 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, char *data, size_t data_len); +int fuzzer_do_request_from_buffer( + char *filename, const char *data, size_t data_len, zend_bool execute); diff --git a/sapi/fuzzer/generate_execute_corpus.php b/sapi/fuzzer/generate_execute_corpus.php new file mode 100644 index 0000000000..c1d8d05cb2 --- /dev/null +++ b/sapi/fuzzer/generate_execute_corpus.php @@ -0,0 +1,38 @@ +<?php + +if ($argc >= 2) { + $testsDir = $argv[1]; +} else { + $testsDir = __DIR__ . '/../../Zend/tests'; +} +if ($argc >= 3) { + $corpusDir = $argv[2]; +} else { + $corpusDir = __DIR__ . '/corpus/execute'; +} +if ($argc >= 4) { + $maxLen = (int) $argv[3]; +} else { + $maxLen = 16 * 1024; +} + +$it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($testsDir), + RecursiveIteratorIterator::LEAVES_ONLY +); + +@mkdir($corpusDir); + +foreach ($it as $file) { + if (!preg_match('/\.phpt$/', $file)) continue; + $fullCode = file_get_contents($file); + if (!preg_match('/--FILE--\R(.*?)\R--([_A-Z]+)--/s', $fullCode, $matches)) continue; + $code = $matches[1]; + if (strlen($code) > $maxLen) continue; + + $outFile = str_replace($testsDir, '', $file); + $outFile = str_replace('/', '_', $outFile); + if (!preg_match('/SKIP_SLOW_TESTS|SKIP_PERF_SENSITIVE|USE_ZEND_ALLOC/', $fullCode)) { + file_put_contents($corpusDir . '/' . $outFile, $code); + } +} |