Various improvements to fuzzer SAPIs

This commit is contained in:
Nikita Popov 2019-09-13 15:15:46 +02:00
parent 41f45647f9
commit c4e2ca607f
25 changed files with 322 additions and 102 deletions

View file

@ -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],

View file

@ -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)

50
sapi/fuzzer/README.md Normal file
View file

@ -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
```

View file

@ -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

View file

@ -0,0 +1,22 @@
<?php
$testsDir = __DIR__ . '/../../../Zend/tests/';
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($testsDir),
RecursiveIteratorIterator::LEAVES_ONLY
);
$corpusDir = __DIR__ . '/parser';
@mkdir($corpusDir);
foreach ($it as $file) {
if (!preg_match('/\.phpt$/', $file)) continue;
$code = file_get_contents($file);
if (!preg_match('/--FILE--(.*)--EXPECT/s', $code, $matches)) continue;
$code = $matches[1];
$outFile = str_replace($testsDir, '', $file);
$outFile = str_replace('/', '_', $outFile);
$outFile = $corpusDir . '/' . $outFile;
file_put_contents($outFile, $code);
}

View file

@ -0,0 +1,9 @@
<?php
$dict = "";
foreach (get_declared_classes() as $class) {
$len = strlen($class);
$dict .= "\"$len:\\\"$class\\\"\"\n";
}
file_put_contents(__DIR__ . "/unserialize.dict", $dict);

View file

@ -0,0 +1,85 @@
"exit"
"die"
"fn"
"function"
"const"
"return"
"yield"
"yield from"
"try"
"catch"
"finally"
"throw"
"if"
"elseif"
"endif"
"else"
"while"
"endwhile"
"do"
"for"
"endfor"
"foreach"
"endforeach"
"declare"
"enddeclare"
"instanceof"
"as"
"switch"
"endswitch"
"case"
"default"
"break"
"continue"
"goto"
"echo"
"print"
"class"
"interface"
"trait"
"extends"
"implements"
"new"
"clone"
"var"
"int"
"integer"
"float"
"double"
"real"
"string"
"binary"
"array"
"object"
"bool"
"boolean"
"unset"
"eval"
"include"
"include_once"
"require"
"require_once"
"namespace"
"use"
"insteadof"
"global"
"isset"
"empty"
"__halt_compiler"
"static"
"abstract"
"final"
"private"
"protected"
"public"
"unset"
"list"
"callable"
"__class__"
"__trait__"
"__function__"
"__method__"
"__line__"
"__file__"
"__dir__"
"__namespace__"

View file

@ -0,0 +1 @@
O:13:"ArrayIterator":2:{i:0;i:0;s:1:"x";R:2;}

View file

@ -0,0 +1 @@
C:11:"ArrayObject":11:{x:i:0;r:3;X}

View file

@ -0,0 +1 @@
C:16:"SplObjectStorage":113:{x:i:2;O:8:"stdClass":0:{},a:2:{s:4:"prev";i:2;s:4:"next";O:8:"stdClass":0:{}};r:7;,R:2;s:4:"next";;r:3;};m:a:0:{}}

View file

@ -0,0 +1 @@
a:2:{i:0;O:1:"0":2:0s:1:"0";i:0;s:1:"0";a:1:{i:0;C:11:"ArrayObject":7:{x:i:0;r}

View file

@ -0,0 +1 @@
C:11:"ArrayObject":34:{x:i:1;O:8:"stdClass":1:{};m:a:0:{}}

View file

@ -0,0 +1 @@
O:8:"00000000":

View file

@ -0,0 +1 @@
O:9:"Exception":799999999999999999999999999997:0i:0;a:0:{}i:2;i:0;i:0;R:2;

View file

@ -0,0 +1 @@
a:7:{i:0;i:04;s:1:"a";i:2;i:9617006;i:4;s:1:"a";i:4;s:1:"a";R:5;s:1:"7";R:3;s:1:"a";R:5;;s:18;}}

View file

@ -0,0 +1 @@
O:8:"stdClass":00000000

View file

@ -0,0 +1 @@
a:3020000000000000000000000000000001:{i:0;a:0:{}i:1;i:2;i:2;i:3;i:3;i:4;i:4;i:5;i:5;i:6;i:6;i:7;i:7;i:8;i:8;R:2;}

View file

@ -0,0 +1 @@
a:9:{i:0;s:4:"0000";i:0;s:4:"0000";i:0;R:2;s:4:"5003";R:2;s:4:"0000";R:2;s:4:"0000";R:2;s:4:"000";R:2;s:4:"0000";d:0;s:4:"0000";a:9:{s:4:"0000";

View file

@ -33,11 +33,11 @@
#include "fuzzer-sapi.h"
int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
#if HAVE_EXIF
char *filename;
int filedes;
if (php_request_startup()==FAILURE) {
php_module_shutdown();
if (fuzzer_request_startup() == FAILURE) {
return 0;
}
@ -54,6 +54,10 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
php_request_shutdown(NULL);
return 0;
#else
fprintf(stderr, "\n\nERROR:\nPHP built without EXIF, recompile with --enable-exif to use this fuzzer\n");
exit(1);
#endif
}
int LLVMFuzzerInitialize(int *argc, char ***argv) {

View file

@ -41,8 +41,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
memcpy(data, Data, Size);
data[Size] = '\0';
if (php_request_startup()==FAILURE) {
php_module_shutdown();
if (fuzzer_request_startup() == FAILURE) {
return 0;
}
@ -50,9 +49,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
zval result;
php_json_parser parser;
php_json_parser_init(&parser, &result, data, Size, option, 10);
php_json_yyparse(&parser);
ZVAL_UNDEF(&result);
if (php_json_yyparse(&parser) == SUCCESS) {
zval_ptr_dtor(&result);
}
}
php_request_shutdown(NULL);

View file

@ -36,8 +36,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
memcpy(data, Data, Size);
data[Size] = '\0';
if (php_request_startup()==FAILURE) {
php_module_shutdown();
if (fuzzer_request_startup() == FAILURE) {
return 0;
}

View file

@ -23,56 +23,26 @@
#include <ext/standard/info.h>
#include <ext/standard/php_var.h>
#include <main/php_variables.h>
#ifdef JO0
#include <ext/standard/php_smart_str.h>
#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(); */

View file

@ -24,6 +24,10 @@
#include <ext/standard/php_var.h>
#include <main/php_variables.h>
#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");
#ifdef __SANITIZE_ADDRESS__
/* Not very interested in memory leak detection, since Zend MM does that */
__lsan_disable();
#endif
if (fuzzer_module.startup(&fuzzer_module)==FAILURE) {
return FAILURE;
}
#ifdef __SANITIZE_ADDRESS__
__lsan_enable();
#endif
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 */

View file

@ -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);

View file

@ -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;
}
zval_ptr_dtor(&result);
{
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);
/* 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;
}