From 14589e589c5049fbb7f1fb30ccaa3efc66c84f18 Mon Sep 17 00:00:00 2001 From: ckrah <153164927+cktii@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:38:35 +0400 Subject: [PATCH] feat: add fuzzing harnesses (#925) --- .gitignore | 2 + CMakeLists.txt | 78 ++++--- cmake/fuzzing_build.cmake | 153 ++++++++++++++ fuzzing/README.md | 21 ++ fuzzing/bb_fuzzer.cpp | 270 +++++++++++++++++++++++++ fuzzing/bt_fuzzer.cpp | 125 ++++++++++++ fuzzing/corpus/bb_corpus/basic.json | 6 + fuzzing/corpus/bb_corpus/edge.json | 7 + fuzzing/corpus/bb_corpus/multiple.json | 8 + fuzzing/corpus/bb_corpus/nested.json | 11 + fuzzing/corpus/bb_corpus/special.json | 6 + fuzzing/corpus/bt_corpus/corpus1.xml | 7 + fuzzing/corpus/bt_corpus/corpus2.xml | 14 ++ fuzzing/corpus/bt_corpus/corpus3.xml | 8 + fuzzing/corpus/bt_corpus/corpus4.xml | 9 + fuzzing/corpus/bt_corpus/sample1 | 1 + fuzzing/corpus/script_corpus/sample1 | 3 + fuzzing/corpus/script_corpus/sample2 | 3 + fuzzing/corpus/script_corpus/sample3 | 2 + fuzzing/script_fuzzer.cpp | 70 +++++++ 20 files changed, 780 insertions(+), 24 deletions(-) create mode 100644 cmake/fuzzing_build.cmake create mode 100644 fuzzing/README.md create mode 100644 fuzzing/bb_fuzzer.cpp create mode 100644 fuzzing/bt_fuzzer.cpp create mode 100644 fuzzing/corpus/bb_corpus/basic.json create mode 100644 fuzzing/corpus/bb_corpus/edge.json create mode 100644 fuzzing/corpus/bb_corpus/multiple.json create mode 100644 fuzzing/corpus/bb_corpus/nested.json create mode 100644 fuzzing/corpus/bb_corpus/special.json create mode 100644 fuzzing/corpus/bt_corpus/corpus1.xml create mode 100644 fuzzing/corpus/bt_corpus/corpus2.xml create mode 100644 fuzzing/corpus/bt_corpus/corpus3.xml create mode 100644 fuzzing/corpus/bt_corpus/corpus4.xml create mode 100644 fuzzing/corpus/bt_corpus/sample1 create mode 100644 fuzzing/corpus/script_corpus/sample1 create mode 100644 fuzzing/corpus/script_corpus/sample2 create mode 100644 fuzzing/corpus/script_corpus/sample3 create mode 100644 fuzzing/script_fuzzer.cpp diff --git a/.gitignore b/.gitignore index cba22ead7..9d5bd4326 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ CMakeSettings.json .pixi CMakeUserPresets.json + +tags diff --git a/CMakeLists.txt b/CMakeLists.txt index 54451c6a9..f4e63c131 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,30 +2,9 @@ cmake_minimum_required(VERSION 3.16.3) # version on Ubuntu Focal project(behaviortree_cpp VERSION 4.6.2 LANGUAGES C CXX) -set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CONFIG_PATH}") - -set(BTCPP_LIBRARY ${PROJECT_NAME}) - -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - message(STATUS "Setting build type to 'Release' as none was specified.") - set(CMAKE_BUILD_TYPE "Release" CACHE - STRING "Choose the type of build." FORCE) - # Set the possible values of build type for cmake-gui - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Release" "MinSizeRel" "RelWithDebInfo") -endif() - -if(MSVC) - add_definitions(-D_CRT_SECURE_NO_WARNINGS -DWIN32_LEAN_AND_MEAN) -else() - add_definitions(-Wpedantic -fno-omit-frame-pointer) -endif() - # create compile_commands.json set(CMAKE_EXPORT_COMPILE_COMMANDS ON) - #---- project configuration ---- option(BTCPP_SHARED_LIBS "Build shared libraries" ON) option(BTCPP_BUILD_TOOLS "Build commandline tools" ON) @@ -35,6 +14,48 @@ option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON) option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON) option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF) +option(ENABLE_FUZZING "Enable fuzzing builds" OFF) +option(USE_AFLPLUSPLUS "Use AFL++ instead of libFuzzer" OFF) +option(ENABLE_DEBUG "Enable debug build with full symbols" OFF) +option(FORCE_STATIC_LINKING "Force static linking of all dependencies" OFF) + +set(BASE_FLAGS "") + +if(ENABLE_DEBUG) + list(APPEND BASE_FLAGS + -g3 + -ggdb3 + -O0 + -fno-omit-frame-pointer + ) +endif() + +# Include fuzzing configuration if enabled +if(ENABLE_FUZZING) + include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/fuzzing_build.cmake) +else() + # Apply base flags for non-fuzzing builds + add_compile_options(${BASE_FLAGS}) + add_link_options(${BASE_FLAGS}) +endif() + +set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CONFIG_PATH}") + +set(BTCPP_LIBRARY ${PROJECT_NAME}) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() + +if(MSVC) + add_definitions(-D_CRT_SECURE_NO_WARNINGS -DWIN32_LEAN_AND_MEAN) +else() + add_definitions(-Wpedantic -fno-omit-frame-pointer) +endif() if(USE_V3_COMPATIBLE_NAMES) add_definitions(-DUSE_BTCPP3_OLD_NAMES) @@ -189,20 +210,29 @@ target_compile_definitions(${BTCPP_LIBRARY} PUBLIC BTCPP_LIBRARY_VERSION="${CMAK target_compile_features(${BTCPP_LIBRARY} PUBLIC cxx_std_17) if(MSVC) - target_compile_options(${BTCPP_LIBRARY} PRIVATE "/source-charset:utf-8") + target_compile_options(${BTCPP_LIBRARY} PRIVATE "/source-charset:utf-8") else() - target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra) + if(ENABLE_DEBUG) + target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra -g3 -ggdb3 -O0 -fno-omit-frame-pointer) + else() + target_compile_options(${BTCPP_LIBRARY} PRIVATE -Wall -Wextra) + endif() endif() add_library(BT::${BTCPP_LIBRARY} ALIAS ${BTCPP_LIBRARY}) +# Add fuzzing targets +if(ENABLE_FUZZING) + add_fuzzing_targets() +endif() + ############################################################# message( STATUS "BTCPP_LIB_DESTINATION: ${BTCPP_LIB_DESTINATION} " ) message( STATUS "BTCPP_INCLUDE_DESTINATION: ${BTCPP_INCLUDE_DESTINATION} " ) message( STATUS "BTCPP_UNIT_TESTS: ${BTCPP_UNIT_TESTS} " ) if (BTCPP_UNIT_TESTS OR BTCPP_EXAMPLES) -add_subdirectory(sample_nodes) + add_subdirectory(sample_nodes) endif() ###################################################### diff --git a/cmake/fuzzing_build.cmake b/cmake/fuzzing_build.cmake new file mode 100644 index 000000000..43f367001 --- /dev/null +++ b/cmake/fuzzing_build.cmake @@ -0,0 +1,153 @@ +# Fuzzing configuration +# Supports both local fuzzing and OSS-Fuzz integration + +# Detect if we're running in OSS-Fuzz environment +if(DEFINED ENV{LIB_FUZZING_ENGINE}) + set(OSS_FUZZ ON) + message(STATUS "OSS-Fuzz environment detected") +else() + set(OSS_FUZZ OFF) +endif() + +# Auto-detect AFL++ compiler if not in OSS-Fuzz mode +if(NOT OSS_FUZZ AND (CMAKE_C_COMPILER MATCHES ".*afl-.*" OR CMAKE_CXX_COMPILER MATCHES ".*afl-.*")) + set(USE_AFLPLUSPLUS ON CACHE BOOL "Use AFL++ instead of libFuzzer" FORCE) + message(STATUS "AFL++ compiler detected - automatically enabling AFL++ mode") +endif() + +# When building for fuzzing, we want static library by default +set(BTCPP_SHARED_LIBS OFF CACHE BOOL "Build static library for fuzzing" FORCE) + +# Only apply static linking settings if explicitly requested +if(FORCE_STATIC_LINKING) + set(CMAKE_FIND_LIBRARY_SUFFIXES .a ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(BUILD_SHARED_LIBS OFF) + + # Force static linking for dependencies + if(BTCPP_GROOT_INTERFACE) + set(ZeroMQ_USE_STATIC_LIBS ON) + set(ZEROMQ_STATIC_LIBRARY ON) + endif() + + if(BTCPP_SQLITE_LOGGING) + set(SQLite3_USE_STATIC_LIBS ON) + endif() +endif() + +# Set up flags for local fuzzing (not used for OSS-Fuzz) +if(NOT OSS_FUZZ) + list(APPEND BASE_FLAGS -O2) + + if(USE_AFLPLUSPLUS) + set(SANITIZER_FLAGS + -fsanitize=address,undefined + ) + else() + # For libFuzzer, use fuzzer-no-link for the library + set(SANITIZER_FLAGS + -fsanitize=address,undefined,fuzzer-no-link + ) + endif() + + # Apply sanitizer flags to the base library + list(APPEND BASE_FLAGS ${SANITIZER_FLAGS}) + + add_compile_options(${BASE_FLAGS}) + add_link_options(${BASE_FLAGS}) +endif() + +# Disable certain features during fuzzing +set(BTCPP_EXAMPLES OFF CACHE BOOL "Disable examples during fuzzing" FORCE) +set(BTCPP_BUILD_TOOLS OFF CACHE BOOL "Disable tools during fuzzing" FORCE) +set(BTCPP_UNIT_TESTS OFF CACHE BOOL "Disable tests during fuzzing" FORCE) +set(BTCPP_SHARED_LIBS OFF CACHE BOOL "Build static library for fuzzing" FORCE) + +# Function to apply fuzzing flags for local development builds +function(apply_local_fuzzing_flags target) + target_compile_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + ) + + if(FORCE_STATIC_LINKING) + if(USE_AFLPLUSPLUS) + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + -static-libstdc++ + -static-libgcc + -fsanitize=fuzzer + ) + else() + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + -fsanitize=fuzzer + ${SANITIZER_FLAGS} + -static-libstdc++ + -static-libgcc + ) + endif() + else() + if(USE_AFLPLUSPLUS) + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + ${SANITIZER_FLAGS} + -fsanitize=fuzzer + ) + else() + target_link_options(${target} PRIVATE + ${BASE_FLAGS} + -fsanitize=fuzzer + ${SANITIZER_FLAGS} + ) + endif() + endif() +endfunction() + +# Function to add fuzzing targets - compatible with both local and OSS-Fuzz builds +function(add_fuzzing_targets) + set(FUZZERS bt_fuzzer script_fuzzer bb_fuzzer) + + foreach(fuzzer ${FUZZERS}) + add_executable(${fuzzer} fuzzing/${fuzzer}.cpp) + + if(OSS_FUZZ) + # For OSS-Fuzz environment, we rely on environment variables + # like $CC, $CXX, $CFLAGS, $CXXFLAGS, and $LIB_FUZZING_ENGINE + target_link_libraries(${fuzzer} PRIVATE + ${BTCPP_LIBRARY} + ${BTCPP_EXTRA_LIBRARIES} + $ENV{LIB_FUZZING_ENGINE} + ) + else() + # For local development, use our own flags + apply_local_fuzzing_flags(${fuzzer}) + target_link_libraries(${fuzzer} PRIVATE + ${BTCPP_LIBRARY} + ${BTCPP_EXTRA_LIBRARIES} + ) + endif() + + # Setup corpus directories (useful for both environments) + set(CORPUS_DIR ${CMAKE_BINARY_DIR}/corpus/${fuzzer}) + file(MAKE_DIRECTORY ${CORPUS_DIR}) + endforeach() + + # Copy corpus files if they exist (useful for local testing) + # OSS-Fuzz provides its own corpus handling + if(NOT OSS_FUZZ) + file(GLOB BT_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/bt_corpus/*") + file(GLOB SCRIPT_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/script_corpus/*") + file(GLOB BB_CORPUS_FILES "${CMAKE_SOURCE_DIR}/fuzzing/corpus/bb_corpus/*") + + if(BT_CORPUS_FILES) + file(COPY ${BT_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/bt_fuzzer) + endif() + if(SCRIPT_CORPUS_FILES) + file(COPY ${SCRIPT_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/script_fuzzer) + endif() + if(BB_CORPUS_FILES) + file(COPY ${BB_CORPUS_FILES} DESTINATION ${CMAKE_BINARY_DIR}/corpus/bb_fuzzer) + endif() + endif() +endfunction() diff --git a/fuzzing/README.md b/fuzzing/README.md new file mode 100644 index 000000000..e42a75bd0 --- /dev/null +++ b/fuzzing/README.md @@ -0,0 +1,21 @@ +# Fuzzing BehaviorTree.CPP + +You can build the existing harnesses either for libfuzzer or AFL++. +Building the fuzzers requires `clang` (libfuzzer) or an installed version +of [AFL++](https://github.com/AFLplusplus/AFLplusplus). + +## libfuzzer + +```bash +mkdir build_libfuzzer && cd build_libfuzzer +cmake -DENABLE_FUZZING=ON .. +``` + +## AFL++ + +```bash +export CC=afl-clang-fast +export CXX=afl-clang-fast++ +mkdir build_afl && cd build_afl +cmake -DENABLE_FUZZING=ON -DUSE_AFLPLUSPLUS=ON .. +``` diff --git a/fuzzing/bb_fuzzer.cpp b/fuzzing/bb_fuzzer.cpp new file mode 100644 index 000000000..5667d8ffb --- /dev/null +++ b/fuzzing/bb_fuzzer.cpp @@ -0,0 +1,270 @@ +#include "behaviortree_cpp/blackboard.h" +#include +#include +#include +#include +#include + +#include + +class ExceptionFilter +{ +public: + static bool isExpectedException(const std::exception& e) + { + const std::string what = e.what(); + const std::vector expected_patterns = { "Blackboard::set", + "once declared, the type of a " + "port shall not change", + "Missing key", + "hasn't been initialized", + "Missing parent blackboard", + "Floating point truncated", + "Value outside the max " + "numerical limit", + "Value outside the lovest " + "numerical limit", + "Value is negative and can't be " + "converted to unsigned", + "Implicit casting to bool is " + "not allowed" }; + + for(const auto& pattern : expected_patterns) + { + if(what.find(pattern) != std::string::npos) + { + return true; + } + } + return false; + } +}; + +class BlackboardFuzzer +{ +private: + std::vector blackboards_; + std::vector generated_keys_; + FuzzedDataProvider& fuzz_data_; + + std::string generateKey() + { + const std::string key_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01" + "23456789_@"; + size_t length = fuzz_data_.ConsumeIntegralInRange(1, 32); + std::string key; + for(size_t i = 0; i < length; ++i) + { + key += + key_chars[fuzz_data_.ConsumeIntegralInRange(0, key_chars.length() - 1)]; + } + generated_keys_.push_back(key); + return key; + } + + void fuzzSingleBB(BT::Blackboard::Ptr bb) + { + if(!bb) + return; + + try + { + // Create random entry + std::string key = generateKey(); + switch(fuzz_data_.ConsumeIntegralInRange(0, 6)) + { + case 0: + bb->set(key, fuzz_data_.ConsumeIntegral()); + break; + case 1: + bb->set(key, fuzz_data_.ConsumeFloatingPoint()); + break; + case 2: + bb->set(key, fuzz_data_.ConsumeRandomLengthString()); + break; + case 3: + bb->set(key, fuzz_data_.ConsumeBool()); + break; + case 4: + bb->set(key, fuzz_data_.ConsumeIntegral()); + break; + case 5: + bb->set(key, fuzz_data_.ConsumeFloatingPoint()); + break; + case 6: { + // Try to get non-existent key + bb->get(generateKey()); + break; + } + } + + // Random operations on existing keys + if(!generated_keys_.empty()) + { + const auto& existing_key = + generated_keys_[fuzz_data_.ConsumeIntegralInRange( + 0, generated_keys_.size() - 1)]; + + switch(fuzz_data_.ConsumeIntegralInRange(0, 4)) + { + case 0: + bb->unset(existing_key); + break; + case 1: + bb->getEntry(existing_key); + break; + case 2: + bb->get(existing_key); + break; + case 3: + bb->get(existing_key); + break; + case 4: + bb->get(existing_key); + break; + } + } + + // Random remapping operations + if(generated_keys_.size() >= 2) + { + size_t idx1 = + fuzz_data_.ConsumeIntegralInRange(0, generated_keys_.size() - 1); + size_t idx2 = + fuzz_data_.ConsumeIntegralInRange(0, generated_keys_.size() - 1); + bb->addSubtreeRemapping(generated_keys_[idx1], generated_keys_[idx2]); + } + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + throw; + } + } + } + + void createBlackboardHierarchy() + { + if(blackboards_.empty()) + return; + + auto parent = blackboards_[fuzz_data_.ConsumeIntegralInRange( + 0, blackboards_.size() - 1)]; + + auto child = BT::Blackboard::create(parent); + if(fuzz_data_.ConsumeBool()) + { + child->enableAutoRemapping(true); + } + + blackboards_.push_back(child); + } + + void fuzzJsonOperations(BT::Blackboard::Ptr bb) + { + try + { + auto json = BT::ExportBlackboardToJSON(*bb); + if(fuzz_data_.ConsumeBool()) + { + std::string json_str = json.dump(); + size_t pos = fuzz_data_.ConsumeIntegralInRange(0, json_str.length()); + json_str.insert(pos, fuzz_data_.ConsumeRandomLengthString()); + json = nlohmann::json::parse(json_str); + } + BT::ImportBlackboardFromJSON(json, *bb); + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + throw; + } + } + } + +public: + explicit BlackboardFuzzer(FuzzedDataProvider& provider) : fuzz_data_(provider) + { + blackboards_.push_back(BT::Blackboard::create()); + } + + void fuzz() + { + size_t num_operations = fuzz_data_.ConsumeIntegralInRange(50, 200); + + for(size_t i = 0; i < num_operations && !blackboards_.empty(); ++i) + { + try + { + // Randomly select a blackboard to operate on + size_t bb_idx = + fuzz_data_.ConsumeIntegralInRange(0, blackboards_.size() - 1); + auto bb = blackboards_[bb_idx]; + + switch(fuzz_data_.ConsumeIntegralInRange(0, 3)) + { + case 0: + // Fuzz single blackboard operations + fuzzSingleBB(bb); + break; + + case 1: + // Create new blackboards in hierarchy + if(fuzz_data_.ConsumeBool()) + { + createBlackboardHierarchy(); + } + break; + + case 2: + // JSON operations + fuzzJsonOperations(bb); + break; + + case 3: + // Cleanup operations + if(fuzz_data_.ConsumeBool() && blackboards_.size() > 1) + { + size_t remove_idx = + fuzz_data_.ConsumeIntegralInRange(0, blackboards_.size() - 1); + blackboards_.erase(blackboards_.begin() + remove_idx); + } + break; + } + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + std::cerr << "Unexpected exception: " << e.what() << std::endl; + throw; + } + } + } + } +}; + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 64) + return 0; + + try + { + FuzzedDataProvider fuzz_data(data, size); + BlackboardFuzzer fuzzer(fuzz_data); + fuzzer.fuzz(); + } + catch(const std::exception& e) + { + if(!ExceptionFilter::isExpectedException(e)) + { + std::cerr << "Unexpected top-level exception: " << e.what() << std::endl; + return 1; + } + } + + return 0; +} diff --git a/fuzzing/bt_fuzzer.cpp b/fuzzing/bt_fuzzer.cpp new file mode 100644 index 000000000..7411109cc --- /dev/null +++ b/fuzzing/bt_fuzzer.cpp @@ -0,0 +1,125 @@ +#include +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp/xml_parsing.h" +#include + +// List of valid node types we can use to construct valid-ish XML +constexpr const char* NODE_TYPES[] = { + "Sequence", "Fallback", "ParallelAll", + "ReactiveSequence", "ReactiveFallback", "IfThenElse", + "WhileDoElse", "Inverter", "RetryUntilSuccessful", + "Repeat", "Timeout", "Delay", + "ForceSuccess", "ForceFailure", "AlwaysSuccess", + "AlwaysFailure", "SetBlackboard", "SubTree" +}; + +// Attributes that can be added to nodes +constexpr const char* NODE_ATTRIBUTES[] = { "name", "ID", "port_1", + "port_2", "timeout_ms", "delay_ms", + "threshold", "max_repeats" }; + +std::string generateFuzzedNodeXML(FuzzedDataProvider& fdp, int depth = 0) +{ + // Prevent stack overflow with max depth + if(depth > 6) + { // Reasonable limit for XML tree depth + return ""; + } + + std::string xml; + const std::string node_type = fdp.PickValueInArray(NODE_TYPES); + + xml += "<" + node_type; + + size_t num_attributes = fdp.ConsumeIntegralInRange(0, 3); + for(size_t i = 0; i < num_attributes; i++) + { + const std::string attr = fdp.PickValueInArray(NODE_ATTRIBUTES); + std::string value = fdp.ConsumeRandomLengthString(10); + xml += " " + attr + "=\"" + value + "\""; + } + + if(depth > 3 || fdp.ConsumeBool()) + { + xml += "/>"; + } + else + { + xml += ">"; + // Add some child nodes recursively with depth limit + size_t num_children = fdp.ConsumeIntegralInRange(0, 2); + for(size_t i = 0; i < num_children; i++) + { + xml += generateFuzzedNodeXML(fdp, depth + 1); + } + xml += ""; + } + + return xml; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 4) + { + return 0; + } + + FuzzedDataProvider fdp(data, size); + BT::BehaviorTreeFactory factory; + + try + { + // Strategy 1: Test with completely random data + if(fdp.ConsumeBool()) + { + std::string random_xml = fdp.ConsumeRandomLengthString(size - 1); + try + { + factory.createTreeFromText(random_xml); + } + catch(const std::exception&) + {} + } + // Strategy 2: Generate semi-valid XML + else + { + std::string xml = R"( + + )"; + + size_t num_nodes = fdp.ConsumeIntegralInRange(1, 5); + for(size_t i = 0; i < num_nodes; i++) + { + xml += generateFuzzedNodeXML(fdp); + } + + xml += R"( + + )"; + + auto blackboard = BT::Blackboard::create(); + + switch(fdp.ConsumeIntegralInRange(0, 2)) + { + case 0: + factory.createTreeFromText(xml, blackboard); + break; + case 1: + BT::VerifyXML(xml, {}); + break; + case 2: + factory.registerBehaviorTreeFromText(xml); + if(!factory.registeredBehaviorTrees().empty()) + { + factory.createTree(factory.registeredBehaviorTrees().front(), blackboard); + } + break; + } + } + } + catch(const std::exception&) + {} + + return 0; +} diff --git a/fuzzing/corpus/bb_corpus/basic.json b/fuzzing/corpus/bb_corpus/basic.json new file mode 100644 index 000000000..9121b1350 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/basic.json @@ -0,0 +1,6 @@ +{ + "int_key": 42, + "double_key": 3.14159, + "string_key": "test_string", + "bool_key": true +} diff --git a/fuzzing/corpus/bb_corpus/edge.json b/fuzzing/corpus/bb_corpus/edge.json new file mode 100644 index 000000000..8008ef811 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/edge.json @@ -0,0 +1,7 @@ +{ + "max_int": 2147483647, + "min_int": -2147483648, + "zero": 0, + "empty_string": "", + "null_value": null +} diff --git a/fuzzing/corpus/bb_corpus/multiple.json b/fuzzing/corpus/bb_corpus/multiple.json new file mode 100644 index 000000000..cf5cfa76b --- /dev/null +++ b/fuzzing/corpus/bb_corpus/multiple.json @@ -0,0 +1,8 @@ +{ + "key1": 42, + "key2": "string", + "key3": 3.14, + "key4": true, + "key5": -123, + "key6": 99.99 +} diff --git a/fuzzing/corpus/bb_corpus/nested.json b/fuzzing/corpus/bb_corpus/nested.json new file mode 100644 index 000000000..39c4b000e --- /dev/null +++ b/fuzzing/corpus/bb_corpus/nested.json @@ -0,0 +1,11 @@ +{ + "outer_key": { + "inner_int": 100, + "inner_string": "nested" + }, + "array_key": [ + 1, + 2, + 3 + ] +} diff --git a/fuzzing/corpus/bb_corpus/special.json b/fuzzing/corpus/bb_corpus/special.json new file mode 100644 index 000000000..76a6a4b70 --- /dev/null +++ b/fuzzing/corpus/bb_corpus/special.json @@ -0,0 +1,6 @@ +{ + "special@key": 123, + "key_with_underscore": "value", + "KeyWithCaps": true, + "123numeric_start": 456 +} diff --git a/fuzzing/corpus/bt_corpus/corpus1.xml b/fuzzing/corpus/bt_corpus/corpus1.xml new file mode 100644 index 000000000..e625cb41b --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus1.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus2.xml b/fuzzing/corpus/bt_corpus/corpus2.xml new file mode 100644 index 000000000..461e53c64 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus3.xml b/fuzzing/corpus/bt_corpus/corpus3.xml new file mode 100644 index 000000000..e21ea8adf --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus3.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/corpus4.xml b/fuzzing/corpus/bt_corpus/corpus4.xml new file mode 100644 index 000000000..3111d8be4 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/corpus4.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fuzzing/corpus/bt_corpus/sample1 b/fuzzing/corpus/bt_corpus/sample1 new file mode 100644 index 000000000..932d7e835 --- /dev/null +++ b/fuzzing/corpus/bt_corpus/sample1 @@ -0,0 +1 @@ + diff --git a/fuzzing/corpus/script_corpus/sample1 b/fuzzing/corpus/script_corpus/sample1 new file mode 100644 index 000000000..36c81d57f --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample1 @@ -0,0 +1,3 @@ +a : = 1; +b : = 2; +a + b diff --git a/fuzzing/corpus/script_corpus/sample2 b/fuzzing/corpus/script_corpus/sample2 new file mode 100644 index 000000000..dc1d11e2c --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample2 @@ -0,0 +1,3 @@ +x : = true; +y : = false; +x and y diff --git a/fuzzing/corpus/script_corpus/sample3 b/fuzzing/corpus/script_corpus/sample3 new file mode 100644 index 000000000..39ce18d0c --- /dev/null +++ b/fuzzing/corpus/script_corpus/sample3 @@ -0,0 +1,2 @@ +str : = 'hello'; +len(str) diff --git a/fuzzing/script_fuzzer.cpp b/fuzzing/script_fuzzer.cpp new file mode 100644 index 000000000..b507dcf17 --- /dev/null +++ b/fuzzing/script_fuzzer.cpp @@ -0,0 +1,70 @@ +#include +#include "behaviortree_cpp/scripting/script_parser.hpp" +#include "behaviortree_cpp/blackboard.h" +#include "behaviortree_cpp/basic_types.h" +#include +#include +#include +#include + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 4) + { + return 0; + } + + FuzzedDataProvider fuzz_data(data, size); + + try + { + BT::Ast::Environment env; + env.vars = BT::Blackboard::create(); + env.enums = std::make_shared(); + + // Add some test variables to the blackboard + env.vars->set("test_int", 42); + env.vars->set("test_double", 3.14); + env.vars->set("test_bool", true); + env.vars->set("test_string", std::string("test")); + + // Add some test enums + (*env.enums)["RUNNING"] = 0; + (*env.enums)["SUCCESS"] = 1; + (*env.enums)["FAILURE"] = 2; + + std::string script = fuzz_data.ConsumeRandomLengthString(); + + auto validation_result = BT::ValidateScript(script); + + if(!validation_result) + { + auto parsed_script = BT::ParseScript(script); + if(parsed_script) + { + try + { + auto result = parsed_script.value()(env); + + if(result.isNumber()) + { + volatile auto num = result.cast(); + } + + env.vars->set("result", result); + + BT::Any read_back; + env.vars->get("result", read_back); + } + catch(const BT::RuntimeError&) + {} + } + } + + BT::ParseScriptAndExecute(env, script); + } + catch(const std::exception&) + {} + + return 0; +}