diff --git a/CMakeLists.txt b/CMakeLists.txt index 1945b2cd3..e23376cf5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,8 @@ option(JSON_Install "Install CMake targets during install option(JSON_MultipleHeaders "Use non-amalgamated version of the library." ON) option(JSON_SystemInclude "Include as system headers (skip for clang-tidy)." OFF) +option(JSON_BuildFuzzers "Build fuzz testing binaries. Requires JSON_BuildTests=ON." OFF) + if (JSON_CI) include(ci) endif () @@ -145,7 +147,7 @@ CONFIGURE_FILE( ## ## TESTS -## create and configure the unit test target +## create and configure the unit test target; build fuzzers, if enabled ## if (JSON_BuildTests) include(CTest) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 65b610f0e..d98d3c9f7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -172,3 +172,11 @@ add_subdirectory(cmake_add_subdirectory) add_subdirectory(cmake_fetch_content) add_subdirectory(cmake_fetch_content2) add_subdirectory(cmake_target_include_directories) + +############################################################################# +# fuzz testing +############################################################################# + +if(JSON_BuildFuzzers) + add_subdirectory(fuzz) +endif() diff --git a/tests/fuzz/CMakeLists.txt b/tests/fuzz/CMakeLists.txt new file mode 100644 index 000000000..7adf4b8f2 --- /dev/null +++ b/tests/fuzz/CMakeLists.txt @@ -0,0 +1,104 @@ +set(JSON_FUZZ_ENGINE "afl++" CACHE STRING "The engine to use for fuzz testing.") +set(JSON_FUZZ_TARGETS "json;bjdata;bson;cbor;msgpack;ubjson" CACHE STRING "List of targets/formats to fuzz test.") +set(JSON_FUZZ_SANITIZERS "asan+cfisan+lsan+ubsan;msan" CACHE STRING "List of sanitizers/combinations of sanitizers to build fuzzers for.") +set(JSON_FUZZ_CORPUS_MAX_SIZE "5k" CACHE STRING "Maximum file size for corpus data.") +option(JSON_FUZZ_MINIMIZE_CORPUS "Minimize the corpa generated from test data." ON) +set(JSON_FUZZ_TEMP_DIR "" CACHE PATH "Path to temporary directory. Should be on a tmpfs or equivalent (AFL++).") +set(JSON_FUZZ_NUM_JOBS 8 CACHE STRING "Number of parallel fuzzing jobs.") +set(JSON_FUZZ_AFL_DIR "" CACHE PATH "Path to AFL++.") +set(JSON_FUZZ_AFL_INSTRUMENTATIONS "laf-intel;complog" CACHE STRING "List of AFL++ instrumentations to build fuzzers for.") + +option(JSON_FUZZ_AFL_EXIT_WHEN_DONE "Exit fuzzer when no new paths have been discovered for a while." ON) + +set(JSON_FUZZ_MAX_TIME "" ON) + +include(fuzz) + +# find_program() requires permission to execute but not to read +cmake_policy(SET CMP0109 NEW) + +############################################################################# +# validate settings +############################################################################# + +# check fuzz engine +string(TOLOWER "${JSON_FUZZ_ENGINE}" fuzz_engine) +if(NOT "${fuzz_engine}" STREQUAL "afl++") + message(FATAL_ERROR "Unsupoorted fuzz engine: ${fuzz_engine}") +endif() +set(JSON_FUZZ_ENGINE "${fuzz_engine}" CACHE STRING "" FORCE) + +if(${JSON_FUZZ_ENGINE} STREQUAL afl++) + # find compiler + find_program (JSON_FUZZ_CXX_COMPILER + NAMES afl-clang-lto++ afl-clang-fast++ + DOC "AFL++ C++ compiler" REQUIRED) + find_program (JSON_FUZZ_AFL_FUZZ + NAMES afl-fuzz + DOC "AFL++ fuzzer runner" REQUIRED) + if(JSON_FUZZ_MINIMIZE_CORPUS) + find_program (JSON_FUZZ_AFL_CMIN + NAMES afl-cmin + DOC "AFL++ corpus minimizer" REQUIRED) + endif() +elseif(${JSON_FUZZ_ENGINE} STREQUAL libfuzzer) + if(CMAKE_CXX_COMPILER_ID STREQUAL Clang) + set(JSON_FUZZ_CXX_COMPILER "${CMAKE_CXX_COMPILER}") + endif() + find_program (JSON_FUZZ_CXX_COMPILER + NAMES clang++ + DOC "Clang C++ compiler" REQUIRED) +endif() + +# TODO does libFuzzer hammer the disk as much as AFL++? +if(${JSON_FUZZ_ENGINE} STREQUAL afl++) + if(NOT JSON_FUZZ_TEMP_DIR) + # try to default AFL++ temp. directory to XDG_RUNTIME_DIR on Linux + if(UNIX AND NOT APPLE AND DEFINED ENV{XDG_RUNTIME_DIR}) + find_program(mount_program mount) + if(mount_program) + execute_process(COMMAND ${mount_program} + OUTPUT_VARIABLE mount_output + ERROR_QUIET) + string(REGEX MATCH "[^ ]+ on $ENV{XDG_RUNTIME_DIR}(/[^ ]*)? type tmpfs" mount_match "${mount_output}") + if(mount_match) + string(RANDOM suffix) + set(temp_dir "$ENV{XDG_RUNTIME_DIR}/json_fuzz_tmp.${suffix}") + set(JSON_FUZZ_TEMP_DIR "${temp_dir}" CACHE PATH "" FORCE) + endif() + endif() + endif() + endif() + + if(NOT JSON_FUZZ_TEMP_DIR) + message(WARNING "JSON_FUZZ_TEMP_DIR should point to a directory on an in-memory file system.") + string(RANDOM suffix) + set(temp_dir "$ENV{CMAKE_CURRENT_BINARY_DIR}/json_fuzz_tmp.${suffix}") + set(JSON_FUZZ_TEMP_DIR "${temp_dir}" CACHE PATH "" FORCE) + endif() + + message(STATUS "Temporary directory for fuzzing: ${JSON_FUZZ_TEMP_DIR}") +endif() + +############################################################################# +# set up fuzzing +############################################################################# + +foreach(target ${JSON_FUZZ_TARGETS}) + json_fuzz_add_fuzzers(${target} + SOURCES src/fuzzer-parse_${target}.cpp + ENGINE ${JSON_FUZZ_ENGINE} + SANITIZERS ${JSON_FUZZ_SANITIZERS} + INSTRUMENTATIONS ${JSON_FUZZ_AFL_INSTRUMENTATIONS}) + + json_fuzz_add_corpus(${target} GLOB "**/*.${target}" + MAX_SIZE ${JSON_FUZZ_CORPUS_MAX_SIZE}) + + if(JSON_FUZZ_MINIMIZE_CORPUS) + json_fuzz_minimize_corpus(${target} + ENGINE ${JSON_FUZZ_ENGINE}) + endif() + + json_fuzz_add_fuzz_run_target(${target} + TEMP_DIR "${JSON_FUZZ_TEMP_DIR}") +endforeach()