diff --git a/cmake/fuzzing_build.cmake b/cmake/fuzzing_build.cmake index 43f367001..7c7cb0f2c 100644 --- a/cmake/fuzzing_build.cmake +++ b/cmake/fuzzing_build.cmake @@ -106,7 +106,7 @@ 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) + set(FUZZERS bt_fuzzer script_fuzzer bb_fuzzer bt_convert_fuzzer) foreach(fuzzer ${FUZZERS}) add_executable(${fuzzer} fuzzing/${fuzzer}.cpp) diff --git a/fuzzing/bt_convert_fuzzer.cpp b/fuzzing/bt_convert_fuzzer.cpp new file mode 100644 index 000000000..e5dfea93d --- /dev/null +++ b/fuzzing/bt_convert_fuzzer.cpp @@ -0,0 +1,379 @@ +// Fuzz target: bt_convert_fuzzer +// +// Threat model: every XML attribute, script literal and blackboard value that +// enters a BehaviorTree.CPP application passes through BT::convertFromString +// (port parsing), SafeAny/Any type-erased casts, or typed blackboard access. +// These string->type and type->type conversions are the numeric- and +// string-parsing funnel for untrusted data (attacker-supplied XML documents, +// Groot files, scripting expressions). polymorphic_cast_registry powers +// shared_ptr port compatibility (Issue #943) and is dark in production +// coverage (220 lines, 0.9%). +// +// Strategy (component-API harness, typed inputs): +// 1. convertFromString over the full builtin type matrix with a fuzzed +// string (numerics incl. int8..64/uint8..64/float/double overflow and +// underflow, bool, enums NodeStatus/NodeType/PortDirection, split +// containers vector, SharedQueue), +// 2. Any value storage + tryCast/cast cross-type matrix, +// 3. PolymorphicCastRegistry upcasts/downcasts/transitive chains over a +// harness-defined class hierarchy (Animal/Cat/Dog/Sphynx), including via +// a blackboard with the registry attached, +// 4. blackboard typed set/get mismatch matrix. +// +// All thrown BT exceptions (RuntimeError/LogicError on invalid input) are +// expected and caught; the point is exercising the conversion code paths, not +// crashing on them. +// +// Expected bug classes: integer/float overflow and truncation handling, +// out-of-bounds reads in split-string parsing, enum parsing mismatches, +// type-confusion in Any casts, registry lookup errors on shared_ptr ports. + +#include "behaviortree_cpp/basic_types.h" +#include "behaviortree_cpp/blackboard.h" +#include "behaviortree_cpp/decorators/loop_node.h" +#include "behaviortree_cpp/utils/polymorphic_cast_registry.hpp" +#include "behaviortree_cpp/utils/safe_any.hpp" + +#include +#include +#include + +#include + +namespace +{ + +// Harness-defined hierarchy for the polymorphic cast registry. No upstream +// product code is involved: the registry API is public and standalone. +struct Animal +{ + virtual ~Animal() = default; + virtual const char* species() const + { + return "Animal"; + } +}; +struct Cat : Animal +{ + const char* species() const override + { + return "Cat"; + } +}; +struct Dog : Animal +{ + const char* species() const override + { + return "Dog"; + } +}; +struct Sphynx : Cat +{ + const char* species() const override + { + return "Sphynx"; + } +}; + +using AnimalPtr = std::shared_ptr; +using CatPtr = std::shared_ptr; +using DogPtr = std::shared_ptr; +using SphynxPtr = std::shared_ptr; + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + if(size < 8) + { + return 0; + } + FuzzedDataProvider fdp(data, size); + + const std::string text = fdp.ConsumeRandomLengthString(); + const BT::StringView sv(text); + + // ---- Stage 1: convertFromString over the builtin type matrix + try + { + switch(fdp.ConsumeIntegralInRange(0, 19)) + { + case 0: + (void)BT::convertFromString(sv); + break; + case 1: + (void)BT::convertFromString(sv); + break; + case 2: + (void)BT::convertFromString(sv); + break; + case 3: + (void)BT::convertFromString(sv); + break; + case 4: + (void)BT::convertFromString(sv); + break; + case 5: + (void)BT::convertFromString(sv); + break; + case 6: + (void)BT::convertFromString(sv); + break; + case 7: + (void)BT::convertFromString(sv); + break; + case 8: + (void)BT::convertFromString(sv); + break; + case 9: + (void)BT::convertFromString(sv); + break; + case 10: + (void)BT::convertFromString(sv); + break; + case 11: + (void)BT::convertFromString(sv); + break; + case 12: + (void)BT::convertFromString(sv); + break; + case 13: + (void)BT::convertFromString(sv); + break; + case 14: + (void)BT::convertFromString(sv); + break; + case 15: + (void)BT::convertFromString>(sv); + break; + case 16: + (void)BT::convertFromString>(sv); + break; + case 17: + (void)BT::convertFromString>(sv); + break; + case 18: + (void)BT::convertFromString>(sv); + break; + default: + (void)BT::convertFromString>(sv); + break; + } + } + catch(const std::exception&) + { + // invalid numeric/enum input raises RuntimeError: expected + } + + // Stages 2-4 are wrapped so that BT exceptions raised by the conversion + // APIs are contained per-iteration and never escape LLVMFuzzerTestOneInput. + try + { + // ---- Stage 2: Any storage + cross-type cast matrix + { + BT::Any any; + switch(fdp.ConsumeIntegralInRange(0, 2)) + { + case 0: + any = BT::Any(text); + break; + case 1: + any = BT::Any(static_cast(text.size()) * 1000000); + break; + default: + any = BT::Any(static_cast(text.size()) * 1.5); + break; + } + + (void)any.isNumber(); + (void)any.isIntegral(); + (void)any.isString(); + + switch(fdp.ConsumeIntegralInRange(0, 5)) + { + case 0: { + auto res = any.tryCast(); + (void)res.has_value(); + break; + } + case 1: { + auto res = any.tryCast(); + (void)res.has_value(); + break; + } + case 2: { + auto res = any.tryCast(); + (void)res.has_value(); + break; + } + case 3: { + auto res = any.tryCast(); + (void)res.has_value(); + break; + } + case 4: { + auto res = any.tryCast(); + (void)res.has_value(); + break; + } + default: { + // cast() throws RuntimeError on a lossy/invalid cast + try + { + (void)any.cast(); + } + catch(const std::exception&) + { + // expected on invalid casts + } + break; + } + } + } + + // ---- Stage 3: polymorphic cast registry + { + BT::PolymorphicCastRegistry registry; + registry.registerCast(); + registry.registerCast(); + registry.registerCast(); + + BT::Any animal_any; + switch(fdp.ConsumeIntegralInRange(0, 3)) + { + case 0: + animal_any = BT::Any(AnimalPtr(std::make_shared())); + break; + case 1: + animal_any = BT::Any(CatPtr(std::make_shared())); + break; + case 2: + animal_any = BT::Any(DogPtr(std::make_shared())); + break; + default: + animal_any = BT::Any(SphynxPtr(std::make_shared())); + break; + } + + switch(fdp.ConsumeIntegralInRange(0, 4)) + { + case 0: { + auto res = animal_any.tryCastWithRegistry(registry); + (void)res.has_value(); + break; + } + case 1: { + auto res = animal_any.tryCastWithRegistry(registry); + (void)res.has_value(); + break; + } + case 2: { + auto res = animal_any.tryCastWithRegistry(registry); + (void)res.has_value(); + break; + } + case 3: { + auto res = animal_any.tryCastWithRegistry(registry); + (void)res.has_value(); + break; + } + default: { + try + { + auto p = animal_any.cast(); + (void)p; + } + catch(const std::exception&) + { + // expected when the stored type is not Cat-compatible + } + break; + } + } + + // Blackboard with an attached registry: typed set/get through the + // polymorphic path (Issue #943 usage pattern). + auto reg2 = std::make_shared(); + reg2->registerCast(); + reg2->registerCast(); + auto bb_poly = BT::Blackboard::create(); + bb_poly->setPolymorphicCastRegistry(reg2); + bb_poly->set("obj", CatPtr(std::make_shared())); + try + { + auto up = bb_poly->get("obj"); + (void)up; + } + catch(const std::exception&) + { + // expected when the registry cannot satisfy the cast + } + try + { + auto down = bb_poly->get("obj"); + (void)down; + } + catch(const std::exception&) + { + // expected on unsupported downcasts + } + } + + // ---- Stage 4: blackboard typed set/get mismatch matrix + { + auto bb = BT::Blackboard::create(); + switch(fdp.ConsumeIntegralInRange(0, 3)) + { + case 0: + bb->set("k", static_cast(text.size())); + break; + case 1: + bb->set("k", static_cast(text.size())); + break; + case 2: + bb->set("k", text); + break; + default: + bb->set("k", !text.empty()); + break; + } + const auto fetch = [&bb](auto type_tag) { + using T = decltype(type_tag); + try + { + // typed get: mismatched entry type raises RuntimeError (expected) + (void)bb->get("k"); + } + catch(const std::exception&) + { + // expected: type mismatch or missing key + } + }; + switch(fdp.ConsumeIntegralInRange(0, 4)) + { + case 0: + fetch(int64_t{}); + break; + case 1: + fetch(double{}); + break; + case 2: + fetch(std::string{}); + break; + case 3: + fetch(bool{}); + break; + default: + fetch(int64_t{}); // on a missing key: RuntimeError path + break; + } + } + } + catch(const std::exception&) + { + // expected: Any/registry/blackboard APIs raise RuntimeError on + // lossy or mismatched conversions + } + + return 0; +} diff --git a/fuzzing/corpus/bt_convert_fuzzer/bool-yes b/fuzzing/corpus/bt_convert_fuzzer/bool-yes new file mode 100644 index 000000000..e433dab88 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/bool-yes differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/double-nan b/fuzzing/corpus/bt_convert_fuzzer/double-nan new file mode 100644 index 000000000..7c4a54e40 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/double-nan differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/enum-nodetype b/fuzzing/corpus/bt_convert_fuzzer/enum-nodetype new file mode 100644 index 000000000..e2a72a170 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/enum-nodetype differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/enum-status b/fuzzing/corpus/bt_convert_fuzzer/enum-status new file mode 100644 index 000000000..8da63d33c Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/enum-status differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/float-overflow b/fuzzing/corpus/bt_convert_fuzzer/float-overflow new file mode 100644 index 000000000..cd90582b2 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/float-overflow differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/int-max b/fuzzing/corpus/bt_convert_fuzzer/int-max new file mode 100644 index 000000000..21e6b0689 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/int-max differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/int-underflow b/fuzzing/corpus/bt_convert_fuzzer/int-underflow new file mode 100644 index 000000000..461be1bad Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/int-underflow differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/junk-mixed b/fuzzing/corpus/bt_convert_fuzzer/junk-mixed new file mode 100644 index 000000000..f01c25001 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/junk-mixed differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/queue-int b/fuzzing/corpus/bt_convert_fuzzer/queue-int new file mode 100644 index 000000000..9c0cbaac6 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/queue-int differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/uint-max b/fuzzing/corpus/bt_convert_fuzzer/uint-max new file mode 100644 index 000000000..cf8762053 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/uint-max differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/vec-bool b/fuzzing/corpus/bt_convert_fuzzer/vec-bool new file mode 100644 index 000000000..31d85e669 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/vec-bool differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/vec-double b/fuzzing/corpus/bt_convert_fuzzer/vec-double new file mode 100644 index 000000000..8a228aab9 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/vec-double differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/vec-int b/fuzzing/corpus/bt_convert_fuzzer/vec-int new file mode 100644 index 000000000..81af62b80 Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/vec-int differ diff --git a/fuzzing/corpus/bt_convert_fuzzer/vec-str b/fuzzing/corpus/bt_convert_fuzzer/vec-str new file mode 100644 index 000000000..ce9ab17fb Binary files /dev/null and b/fuzzing/corpus/bt_convert_fuzzer/vec-str differ diff --git a/fuzzing/dictionaries/bt_convert_fuzzer.dict b/fuzzing/dictionaries/bt_convert_fuzzer.dict new file mode 100644 index 000000000..4abcc6d60 --- /dev/null +++ b/fuzzing/dictionaries/bt_convert_fuzzer.dict @@ -0,0 +1,50 @@ +# Dictionary for bt_convert_fuzzer: numeric forms, enum names, separators. +"9223372036854775807" +"-9223372036854775808" +"18446744073709551615" +"0" +"-1" +"+1" +"007" +"0x1F" +"0b101" +"1e10" +"1E-10" +"1e999" +"-1e999" +"nan" +"inf" +"-inf" +"3.14159" +".5" +"5." +"1.0e-400" +"true" +"false" +"TRUE" +"False" +"1;2;3" +"-4;5;6" +"1.5;2.5" +"a;b;c" +"true;false" +"SUCCESS" +"FAILURE" +"RUNNING" +"SKIPPED" +"IDLE" +"Action" +"Condition" +"Control" +"Decorator" +"SubTree" +"UNDEFINED" +"INPUT" +"OUTPUT" +"BIDIRECTIONAL" +" " +" 42 " +";;" +"--7" +"12abc" +"1;2;3;"