summaryrefslogtreecommitdiff
path: root/sapi
diff options
context:
space:
mode:
authorNikita Popov <nikita.ppv@gmail.com>2019-10-09 19:17:07 +0200
committerNikita Popov <nikita.ppv@gmail.com>2020-08-27 13:06:24 +0200
commitc29838c561fdd6903d2ad8a5fa6820c1f26ab3c9 (patch)
tree736ef03db71e398731a519345d4f739026068430 /sapi
parent988fc94bbbfb44a63ef5aeb745d38c1a73e2db0f (diff)
downloadphp-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.frag3
-rw-r--r--sapi/fuzzer/config.m41
-rw-r--r--sapi/fuzzer/fuzzer-execute.c71
-rw-r--r--sapi/fuzzer/fuzzer-parser.c8
-rw-r--r--sapi/fuzzer/fuzzer-sapi.c94
-rw-r--r--sapi/fuzzer/fuzzer-sapi.h3
-rw-r--r--sapi/fuzzer/generate_execute_corpus.php38
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);
+ }
+}