diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4250b64..6140af4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,13 @@ jobs: long_tests=ON fi + fuzzing=OFF + if [ "${{ matrix.build_type }}" == "Debug" ] \ + && [ "${{ matrix.compiler }}" == "clang++" ] \ + && [ "${{ matrix.openssl }}" == "OFF" ]; then + fuzzing=ON + fi + linux_options=() if [ "$RUNNER_OS" == "Linux" ]; then linux_options=( @@ -75,6 +82,7 @@ jobs: "-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}" \ "-DLONG_TESTS=$long_tests" \ "-DOPENSSL=${{ matrix.openssl }}" \ + "-DFUZZING=$fuzzing" \ "${linux_options[@]}" - name: Build diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ae08fa..9ab7179 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ option(OPENSSL "enable OpenSSL" OFF) option(TRACE "enable debug traces" OFF) option(CLANG_TIDY "enable clang-tidy checks during build" OFF) option(LONG_TESTS "enable long-running tests" OFF) +option(FUZZING "build LLVM libFuzzer tests" OFF) if(CLANG_TIDY) find_program(CLANG_TIDY_PROGRAM clang-tidy) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a8c5a68..446af51 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -68,3 +68,44 @@ if(OPENSSL) endif() add_merklecpp_test(unit_tests unit_tests.cpp) + +if(FUZZING) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR "FUZZING requires Clang") + endif() + + set(FUZZ_TREE_DESERIALISE_TARGET + ${MERKLECPP_TEST_PREFIX}fuzz_tree_deserialise) + add_executable( + ${FUZZ_TREE_DESERIALISE_TARGET} + fuzz_tree_deserialise.cpp + ) + target_link_libraries( + ${FUZZ_TREE_DESERIALISE_TARGET} PRIVATE merklecpp + ) + target_compile_options( + ${FUZZ_TREE_DESERIALISE_TARGET} + PRIVATE + -fsanitize=fuzzer,address,undefined + -fno-sanitize-recover=undefined + -fno-omit-frame-pointer + ) + target_link_options( + ${FUZZ_TREE_DESERIALISE_TARGET} + PRIVATE -fsanitize=fuzzer,address,undefined + ) + add_test( + NAME ${FUZZ_TREE_DESERIALISE_TARGET} + COMMAND + ${FUZZ_TREE_DESERIALISE_TARGET} + -runs=200000 + -seed=1 + -max_len=4096 + -timeout=5 + -dict=${CMAKE_CURRENT_SOURCE_DIR}/tree_deserialise_fuzzer.dict + ) + set_tests_properties( + ${FUZZ_TREE_DESERIALISE_TARGET} + PROPERTIES LABELS "merklecpp;fuzz" TIMEOUT 60 + ) +endif() diff --git a/test/fuzz_tree_deserialise.cpp b/test/fuzz_tree_deserialise.cpp new file mode 100644 index 0000000..98dee0e --- /dev/null +++ b/test/fuzz_tree_deserialise.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include +#include + +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + std::vector bytes; + if (size != 0) + { + bytes.assign(data, data + size); + } + + try + { + size_t position = 0; + merkle::Tree tree; + tree.deserialise(bytes, position); + } + // NOLINTNEXTLINE(bugprone-empty-catch) -- expected malformed input rejection + catch (const std::runtime_error&) + {} + // NOLINTNEXTLINE(bugprone-empty-catch) -- expected malformed input rejection + catch (const std::out_of_range&) + {} + + return 0; +} diff --git a/test/tree_deserialise_fuzzer.dict b/test/tree_deserialise_fuzzer.dict new file mode 100644 index 0000000..b29f9ea --- /dev/null +++ b/test/tree_deserialise_fuzzer.dict @@ -0,0 +1,5 @@ +empty_tree="\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +one_leaf="\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" +max_leaf_count="\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00" +high_flushed_count="\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00" +zero_hash="\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"