diff --git a/.gitignore b/.gitignore index 32636268ba..0bf6a0760c 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,7 @@ libr/include/sdb **/d/*.out **/d/*.inc # Artifacts -/dist/artifacts \ No newline at end of file +/dist/artifacts +# libFuzzer +crash-* +corpus* diff --git a/libr/util/sys.c b/libr/util/sys.c index 59786f927d..dfaccf5609 100644 --- a/libr/util/sys.c +++ b/libr/util/sys.c @@ -622,6 +622,10 @@ R_API bool r_sys_aslr(int val) { #if __UNIX__ && HAVE_SYSTEM R_API int r_sys_cmd_str_full(const char *cmd, const char *input, int ilen, char **output, int *len, char **sterr) { + if (!r_sandbox_check (R_SANDBOX_GRAIN_EXEC)) { + return false; + } + char *mysterr = NULL; if (!sterr) { sterr = &mysterr; diff --git a/meson.build b/meson.build index 54260e8394..3a854b5f0c 100644 --- a/meson.build +++ b/meson.build @@ -645,6 +645,7 @@ if get_option('use_webui') endif subdir('test/unit') +subdir('test/fuzz') install_data( 'doc/fortunes.fun', diff --git a/meson_options.txt b/meson_options.txt index 25a5028936..8dff0a9dba 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -47,4 +47,5 @@ option('want_ptrace_wrap', type: 'boolean', value: true) option('nogpl', type: 'boolean', value: false) option('use_webui', type: 'boolean', value: true, description: 'install different WebUIs for radare2') option('enable_tests', type: 'boolean', value: true, description: 'Build unit tests in test/unit') +option('enable_libfuzzer', type: 'boolean', value: false, description: 'Build libFuzzer targets in test/fuzz') option('enable_r2r', type: 'boolean', value: true, description: 'Build r2r executable for regression ') diff --git a/sys/meson.py b/sys/meson.py index d23c36d3ff..2e0050f83c 100755 --- a/sys/meson.py +++ b/sys/meson.py @@ -168,6 +168,8 @@ def build(args): options.append('-Duse_webui=true') if args.local: options.append('-Dlocal=true') + if args.fuzz: + options.append('-Denable_libfuzzer=true') if not os.path.exists(r2_builddir): meson('setup', builddir=r2_builddir, prefix=args.prefix, backend=args.backend, release=args.release, shared=args.shared, options=options) @@ -198,6 +200,8 @@ def main(): parser.add_argument('--sanitize', nargs='?', const='address,undefined,signed-integer-overflow', metavar='sanitizers', help='Build radare2 with sanitizer support (default: %(const)s)') + parser.add_argument('--fuzz', action='store_true', + help='Build radare2 with libFuzzer support') parser.add_argument('--project', action='store_true', help='Create a visual studio project and do not build.') parser.add_argument('--release', action='store_true', @@ -237,15 +241,18 @@ def main(): if os.uname().sysname == 'OpenBSD': log.error("Sanitizers unsupported under OpenBSD") sys.exit(1) + sanitizers = args.sanitize + if args.fuzz and 'fuzzer' not in sanitizers: + sanitizers = "fuzzer," + sanitizers cflags = os.environ.get('CFLAGS') if not cflags: cflags = '' - os.environ['CFLAGS'] = cflags + ' -fsanitize=' + args.sanitize + os.environ['CFLAGS'] = cflags + ' -fsanitize=' + sanitizers if os.uname().sysname != 'Darwin': ldflags = os.environ.get('LDFLAGS') if not ldflags: ldflags = '' - os.environ['LDFLAGS'] = ldflags + ' -fsanitize=' + args.sanitize + os.environ['LDFLAGS'] = ldflags + ' -fsanitize=' + sanitizers # Check arguments if args.pull: diff --git a/test/fuzz/README.md b/test/fuzz/README.md new file mode 100644 index 0000000000..9721684608 --- /dev/null +++ b/test/fuzz/README.md @@ -0,0 +1,61 @@ +# libFuzzer tests + +## Setup + +Get libFuzzer-capable clang + +```shell +# Linux +export CC=clang-14 +# macOS +export CC="$(brew --prefix llvm@14)/bin/clang" +``` + +Clean project + +```shell +rm -rf build +``` + +Build project with libFuzzer and sanitizers + +```shell +# If you want to debug crashes +export CFLAGS="-g" +# Build project with test/fuzz +python3 ./sys/meson.py --fuzz --sanitize address,leak +``` + +## Run + +Refer to https://llvm.org/docs/LibFuzzer.html + +**Show help** + +``` +./build/test/fuzz/fuzz_r_run_parseline -help=1 +``` + +**Run fuzzer** + +``` +mkdir corpus_parseline +./build/test/fuzz/fuzz_r_run_parseline \ + -workers=1 -runs=50000 -timeout=3 \ + corpus_parseline +``` + +**Replay crashes** + +``` +./build/test/fuzz/fuzz_r_run_parseline crash-* +``` + +## Adding a new target + +- add your test to /test/fuzz/meson.build +- add `/test/fuzz/fuzz_.c` file + - add system setup to `LLVMFuzzerInitialize` (disable logging, enable sandbox, etc) + - add fuzz target to `LLVMFuzzerTestOneInput` + - make sure input is short (ideally no longer than 256 bytes) + - make sure no memory leaks are present diff --git a/test/fuzz/fuzz_r_run_parseline.c b/test/fuzz/fuzz_r_run_parseline.c new file mode 100644 index 0000000000..5278154bcc --- /dev/null +++ b/test/fuzz/fuzz_r_run_parseline.c @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include +#include +#include + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + r_sys_clearenv (); + r_sandbox_enable (true); + r_sandbox_grain (R_SANDBOX_GRAIN_NONE); + r_log_set_quiet (true); + return 0; +} + +int LLVMFuzzerTestOneInput(const ut8 *data, size_t len) { + r_sys_clearenv (); + r_sandbox_enable (true); + + char *str = malloc (len + 1); + memcpy (str, data, len); + str[len] = 0; + + RRunProfile *p = r_run_new (NULL); + r_run_parseline (p, str); + free (str); + r_run_free (p); + r_sys_clearenv (); + + return 0; +} diff --git a/test/fuzz/meson.build b/test/fuzz/meson.build new file mode 100644 index 0000000000..a10b329dc2 --- /dev/null +++ b/test/fuzz/meson.build @@ -0,0 +1,17 @@ +if get_option('enable_libfuzzer') + targets = [ + 'r_run_parseline', + ] + + foreach target : targets + exe = executable('fuzz_@0@'.format(target), 'fuzz_@0@.c'.format(target), + include_directories: [platform_inc], + dependencies: [ + r_util_dep, + r_socket_dep, + ], + install: false, + implicit_include_directories: false, + ) + endforeach +endif