diff --git a/modules/test/CMakeLists.txt b/modules/test/CMakeLists.txt index cc9bb62..7b7e9b0 100644 --- a/modules/test/CMakeLists.txt +++ b/modules/test/CMakeLists.txt @@ -1,2 +1,4 @@ add_library_module(test test.cpp entrypoint.cpp) +add_library_module(fuzz_test test.cpp fuzz.cpp) + add_test_module(test test.test.cpp) diff --git a/modules/test/private/fuzz.cpp b/modules/test/private/fuzz.cpp new file mode 100644 index 0000000..c43993c --- /dev/null +++ b/modules/test/private/fuzz.cpp @@ -0,0 +1,24 @@ +#include + +namespace lt::test { +auto process_fuzz_input(const uint8_t *data, size_t size) -> int32_t +try +{ + details::Registry::process_fuzz_input(data, size); + return EXIT_SUCCESS; +} +catch (const std::exception &exp) +{ + std::cout << "Fuzz input resulted in uncaught exception:\n"; + std::cout << "\texception.what: " << exp.what() << '\n'; + std::cout << "\tinput size: " << size << '\n'; + + return EXIT_FAILURE; +} + +}; // namespace lt::test + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + return lt::test::process_fuzz_input(data, size); +} diff --git a/modules/test/public/fuzz.hpp b/modules/test/public/fuzz.hpp new file mode 100644 index 0000000..6c3617c --- /dev/null +++ b/modules/test/public/fuzz.hpp @@ -0,0 +1,62 @@ +#include +#include + +namespace lt::test { + +class FuzzDataProvider +{ +public: + FuzzDataProvider(const uint8_t *data, size_t size): m_data(data, size) + { + } + + template + requires( + std::is_trivially_constructible_v // + && std::is_trivially_copy_constructible_v // + && std::is_trivially_copy_assignable_v + ) + + auto consume() -> std::optional + { + if (m_data.size() < sizeof(T)) + { + return std::nullopt; + } + + T value; + std::memcpy(&value, m_data.data(), sizeof(T)); + + m_data = m_data.subspan(sizeof(T)); + return value; + } + + auto consume_string(size_t size) -> std::optional + { + if (m_data.size() < size) + { + return std::nullopt; + } + + // NOLINTNEXTLINE + auto value = std::string { (const char *)m_data.data(), size }; + m_data = m_data.subspan(size); + + return value; + } + + auto consume_remaining_as_string() -> std::string + { + if (m_data.empty()) + { + return std::string {}; + } + + return { m_data.begin(), m_data.end() }; + }; + +private: + std::span m_data; +}; + +} // namespace lt::test diff --git a/modules/test/public/test.hpp b/modules/test/public/test.hpp index 92f7c18..b6c2172 100644 --- a/modules/test/public/test.hpp +++ b/modules/test/public/test.hpp @@ -27,13 +27,26 @@ namespace details { class Registry { public: - using Suite = void (*)(); + using FuzzFunction = int32_t (*)(const uint8_t *, size_t); + using SuiteFunction = void (*)(); - static void register_suite(Suite suite) + static void register_suite(SuiteFunction suite) { instance().m_suites.emplace_back(suite); } + static void register_fuzz_harness(FuzzFunction suite) + { + if (instance().m_fuzz_harness) + { + throw std::logic_error { + "Attempting to register fuzz harness while one is already registered", + }; + } + + instance().m_fuzz_harness = suite; + } + static auto run_all() -> int32_t { for (auto &test : instance().m_suites) @@ -49,6 +62,18 @@ public: return instance().m_failed_count; } + static auto process_fuzz_input(const uint8_t *data, size_t size) -> int32_t + { + if (!instance().m_fuzz_harness) + { + throw std::logic_error { + "Attempting to process fuzz input with no active harness", + }; + } + + return instance().m_fuzz_harness(data, size); + } + static void increment_passed_count() { ++instance().m_pasesed_count; @@ -71,7 +96,9 @@ private: return registry; } - std::vector m_suites; + std::vector m_suites; + + FuzzFunction m_fuzz_harness {}; int32_t m_pasesed_count {}; int32_t m_failed_count {}; @@ -85,7 +112,7 @@ struct Case auto operator=(std::invocable auto test) -> void // NOLINT { std::cout << "[Running-----------] --> "; - std::cout << name << '\n'; + std::cout << name << '\n'; try { @@ -117,6 +144,18 @@ struct TestSuite } }; +struct TestFuzzHarness +{ + template + constexpr TestFuzzHarness(TestFuzzHarness suite) + { +#ifndef LIGHT_SKIP_FUZZ_TESTS + details::Registry::register_fuzz_harness(+suite); +#endif + } +}; + using Suite = const TestSuite; +using FuzzHarness = const TestFuzzHarness; } // namespace lt::test diff --git a/tools/cmake/functions.cmake b/tools/cmake/functions.cmake index 5f3a208..f087045 100644 --- a/tools/cmake/functions.cmake +++ b/tools/cmake/functions.cmake @@ -113,6 +113,44 @@ function (add_test_module target_lib_name) ) endfunction () +function (add_fuzz_module target_lib_name) + if (NOT ${ENABLE_TESTS}) + return() + endif () + + set(source_files) + set(source_directory "${CMAKE_CURRENT_SOURCE_DIR}/private") + foreach (source_file ${ARGN}) + list(APPEND source_files "${source_directory}/${source_file}") + endforeach () + + message("Adding fuzz executable ${target_lib_name}_fuzz with source files: ${source_files}") + + set(PUBLIC_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/public_includes") + file(MAKE_DIRECTORY "${PUBLIC_INCLUDE_DIR}") + file(CREATE_LINK + "${CMAKE_CURRENT_SOURCE_DIR}/public/" + "${PUBLIC_INCLUDE_DIR}/${target_lib_name}" + SYMBOLIC + ) + set(PRIVATE_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/private_includes") + file(MAKE_DIRECTORY "${PRIVATE_INCLUDE_DIR}") + file(CREATE_LINK + "${CMAKE_CURRENT_SOURCE_DIR}/private/" + "${PRIVATE_INCLUDE_DIR}/${target_lib_name}" + SYMBOLIC + ) + + add_executable(${target_lib_name}_fuzz ${source_files}) + target_link_libraries(${target_lib_name}_fuzz PRIVATE ${target_lib_name} base fuzz_test) + target_link_options(${target_lib_name}_fuzz PRIVATE -fsanitize=fuzzer) + target_compile_options(${target_lib_name}_fuzz PRIVATE -fsanitize=fuzzer) + target_include_directories(${target_lib_name}_fuzz + PRIVATE ${PUBLIC_INCLUDE_DIR} + PRIVATE ${PRIVATE_INCLUDE_DIR} + ) +endfunction () + function (add_option option help) option(${option} ${help})