From f88d64bbe89cd9cb6f42f8e234fd42c472a973da Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 24 Aug 2026 15:10:17 +0100 Subject: [PATCH 1/4] Fuzz tree deserialisation in CI Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++++++ CMakeLists.txt | 1 + test/CMakeLists.txt | 38 +++++++++++++++++++++++++++++++ test/fuzz_tree_deserialise.cpp | 31 +++++++++++++++++++++++++ test/tree_deserialise_fuzzer.dict | 5 ++++ 5 files changed, 83 insertions(+) create mode 100644 test/fuzz_tree_deserialise.cpp create mode 100644 test/tree_deserialise_fuzzer.dict 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..54e656a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -68,3 +68,41 @@ 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-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=20000 + -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..abe89e2 --- /dev/null +++ b/test/fuzz_tree_deserialise.cpp @@ -0,0 +1,31 @@ +// 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); + } + catch (const std::runtime_error&) + {} + 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" From 12a5ca9c6dc41c76a8518a68af01189a9bf8aeb3 Mon Sep 17 00:00:00 2001 From: achamayou Date: Mon, 24 Aug 2026 22:09:31 +0100 Subject: [PATCH 2/4] Suppress expected fuzz rejection warnings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 73572868-1386-49b1-8849-ac9f9543ad3a --- test/fuzz_tree_deserialise.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/fuzz_tree_deserialise.cpp b/test/fuzz_tree_deserialise.cpp index abe89e2..98dee0e 100644 --- a/test/fuzz_tree_deserialise.cpp +++ b/test/fuzz_tree_deserialise.cpp @@ -22,8 +22,10 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) 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&) {} From 860cd95dfa4657d16ef230b510476a17c000786e Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Tue, 25 Aug 2026 13:23:39 +0100 Subject: [PATCH 3/4] Make fuzzing UBSan failures fatal Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 54e656a..3e7a09c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -85,7 +85,10 @@ if(FUZZING) ) target_compile_options( ${FUZZ_TREE_DESERIALISE_TARGET} - PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer + PRIVATE + -fsanitize=fuzzer,address,undefined + -fno-sanitize-recover=undefined + -fno-omit-frame-pointer ) target_link_options( ${FUZZ_TREE_DESERIALISE_TARGET} From 666f20b42d38b4f22cf30cffd390ef731464a6f0 Mon Sep 17 00:00:00 2001 From: achamayou Date: Tue, 25 Aug 2026 17:06:15 +0100 Subject: [PATCH 4/4] Increase tree deserialisation fuzz runs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3e7a09c..446af51 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -98,7 +98,7 @@ if(FUZZING) NAME ${FUZZ_TREE_DESERIALISE_TARGET} COMMAND ${FUZZ_TREE_DESERIALISE_TARGET} - -runs=20000 + -runs=200000 -seed=1 -max_len=4096 -timeout=5