Skip to content

Commit f9a6f98

Browse files
Fix #672: detect and reject deeply-nested/recursive XML to prevent stack overflow (#1091)
Adds a depth limit (256) to the recursive XML parsing functions in VerifyXML() and recursivelyCreateSubtree(). Malformed XML with extreme nesting now throws a clear RuntimeError instead of crashing. Includes 11 new tests for malformed XML handling. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 2e72fb3 commit f9a6f98

2 files changed

Lines changed: 190 additions & 8 deletions

File tree

src/xml_parsing.cpp

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,16 @@ void VerifyXML(const std::string& xml_text,
530530
}
531531

532532
// function to be called recursively
533-
std::function<void(const XMLElement*)> recursiveStep;
533+
constexpr int kMaxNestingDepth = 256;
534+
std::function<void(const XMLElement*, int)> recursiveStep;
534535

535-
recursiveStep = [&](const XMLElement* node) {
536+
recursiveStep = [&](const XMLElement* node, int depth) {
537+
if(depth > kMaxNestingDepth)
538+
{
539+
ThrowError(node->GetLineNum(), "Maximum XML nesting depth exceeded (limit: " +
540+
std::to_string(kMaxNestingDepth) +
541+
"). The XML is too deeply nested.");
542+
}
536543
const int children_count = ChildrenCount(node);
537544
const std::string name = node->Name();
538545
const std::string ID = node->Attribute("ID") != nullptr ? node->Attribute("ID") : "";
@@ -661,14 +668,14 @@ void VerifyXML(const std::string& xml_text,
661668
for(auto child = node->FirstChildElement(); child != nullptr;
662669
child = child->NextSiblingElement())
663670
{
664-
recursiveStep(child);
671+
recursiveStep(child, depth + 1);
665672
}
666673
};
667674

668675
for(auto bt_root = xml_root->FirstChildElement("BehaviorTree"); bt_root != nullptr;
669676
bt_root = bt_root->NextSiblingElement("BehaviorTree"))
670677
{
671-
recursiveStep(bt_root);
678+
recursiveStep(bt_root, 0);
672679
}
673680
}
674681

@@ -1018,12 +1025,20 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(
10181025
throw RuntimeError("Recursive behavior tree cycle detected: tree '", tree_ID,
10191026
"' references itself (directly or indirectly)");
10201027
}
1028+
constexpr int kMaxNestingDepth = 256;
10211029
std::function<void(const TreeNode::Ptr&, Tree::Subtree::Ptr, std::string,
1022-
const XMLElement*)>
1030+
const XMLElement*, int)>
10231031
recursiveStep;
10241032

10251033
recursiveStep = [&](TreeNode::Ptr parent_node, Tree::Subtree::Ptr subtree,
1026-
std::string prefix, const XMLElement* element) {
1034+
std::string prefix, const XMLElement* element, int depth) {
1035+
if(depth > kMaxNestingDepth)
1036+
{
1037+
throw RuntimeError("Maximum XML nesting depth exceeded during tree "
1038+
"instantiation (limit: ",
1039+
std::to_string(kMaxNestingDepth),
1040+
"). The XML is too deeply nested.");
1041+
}
10271042
// create the node
10281043
auto node = createNodeFromXML(element, blackboard, parent_node, prefix, output_tree);
10291044
subtree->nodes.push_back(node);
@@ -1034,7 +1049,7 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(
10341049
for(auto child_element = element->FirstChildElement(); child_element != nullptr;
10351050
child_element = child_element->NextSiblingElement())
10361051
{
1037-
recursiveStep(node, subtree, prefix, child_element);
1052+
recursiveStep(node, subtree, prefix, child_element, depth + 1);
10381053
}
10391054
}
10401055
else // special case: SubTreeNode
@@ -1167,7 +1182,7 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(
11671182
new_tree->tree_ID = tree_ID;
11681183
output_tree.subtrees.push_back(new_tree);
11691184

1170-
recursiveStep(root_node, new_tree, prefix_path, root_element);
1185+
recursiveStep(root_node, new_tree, prefix_path, root_element, 0);
11711186
ancestors.erase(tree_ID);
11721187
}
11731188

tests/gtest_factory.cpp

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,3 +563,170 @@ TEST(BehaviorTreeFactory, FactoryDestroyedBeforeTick)
563563
// when getInput() looked up the port in the manifest.
564564
EXPECT_EQ(BT::NodeStatus::SUCCESS, tree.tickWhileRunning());
565565
}
566+
567+
// Regression tests for issue #672: stack buffer overflow in
568+
// xml_parsing.cpp when parsing malformed/pathological XML.
569+
// In v3 a fuzz test triggered a stack-buffer-overflow via ASAN in
570+
// the BehaviorTree element iteration loop with recursiveStep.
571+
// In v4 the parser was rewritten, but the recursive validation and
572+
// instantiation paths can still overflow the stack with deeply nested
573+
// input. The fix adds a depth limit.
574+
575+
TEST(BehaviorTreeFactory, MalformedXML_InvalidRoot)
576+
{
577+
// XML that is not valid XML at all
578+
BehaviorTreeFactory factory;
579+
EXPECT_ANY_THROW(factory.createTreeFromText("<not valid xml!!!"));
580+
}
581+
582+
TEST(BehaviorTreeFactory, MalformedXML_MissingRootElement)
583+
{
584+
// Well-formed XML but missing <root> element
585+
const char* xml = R"(
586+
<something BTCPP_format="4">
587+
<BehaviorTree ID="Main">
588+
<AlwaysSuccess/>
589+
</BehaviorTree>
590+
</something>)";
591+
592+
BehaviorTreeFactory factory;
593+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
594+
}
595+
596+
TEST(BehaviorTreeFactory, MalformedXML_EmptyBehaviorTree)
597+
{
598+
// BehaviorTree element with no children
599+
const char* xml = R"(
600+
<root BTCPP_format="4">
601+
<BehaviorTree ID="Main">
602+
</BehaviorTree>
603+
</root>)";
604+
605+
BehaviorTreeFactory factory;
606+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
607+
}
608+
609+
TEST(BehaviorTreeFactory, MalformedXML_EmptyBehaviorTreeID)
610+
{
611+
// BehaviorTree element with empty ID when multiple trees exist
612+
const char* xml = R"(
613+
<root BTCPP_format="4">
614+
<BehaviorTree ID="">
615+
<AlwaysSuccess/>
616+
</BehaviorTree>
617+
<BehaviorTree ID="Other">
618+
<AlwaysSuccess/>
619+
</BehaviorTree>
620+
</root>)";
621+
622+
BehaviorTreeFactory factory;
623+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
624+
}
625+
626+
TEST(BehaviorTreeFactory, MalformedXML_MissingBehaviorTreeID)
627+
{
628+
// Multiple BehaviorTree elements without IDs
629+
const char* xml = R"(
630+
<root BTCPP_format="4">
631+
<BehaviorTree>
632+
<AlwaysSuccess/>
633+
</BehaviorTree>
634+
<BehaviorTree>
635+
<AlwaysFailure/>
636+
</BehaviorTree>
637+
</root>)";
638+
639+
BehaviorTreeFactory factory;
640+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
641+
}
642+
643+
TEST(BehaviorTreeFactory, MalformedXML_DeeplyNestedElements)
644+
{
645+
// Generate XML with nesting depth that exceeds the limit (256).
646+
// This should throw a readable exception rather than crash with
647+
// a stack overflow.
648+
std::string xml = R"(<root BTCPP_format="4"><BehaviorTree ID="Main">)";
649+
const int depth = 300;
650+
for(int i = 0; i < depth; i++)
651+
{
652+
xml += "<Sequence>";
653+
}
654+
xml += "<AlwaysSuccess/>";
655+
for(int i = 0; i < depth; i++)
656+
{
657+
xml += "</Sequence>";
658+
}
659+
xml += "</BehaviorTree></root>";
660+
661+
BehaviorTreeFactory factory;
662+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
663+
}
664+
665+
TEST(BehaviorTreeFactory, MalformedXML_ModerateNestingIsOK)
666+
{
667+
// Nesting depth well within the limit should succeed.
668+
std::string xml = R"(<root BTCPP_format="4"><BehaviorTree ID="Main">)";
669+
const int depth = 50;
670+
for(int i = 0; i < depth; i++)
671+
{
672+
xml += "<Sequence>";
673+
}
674+
xml += "<AlwaysSuccess/>";
675+
for(int i = 0; i < depth; i++)
676+
{
677+
xml += "</Sequence>";
678+
}
679+
xml += "</BehaviorTree></root>";
680+
681+
BehaviorTreeFactory factory;
682+
// Should not throw
683+
EXPECT_NO_THROW(factory.createTreeFromText(xml));
684+
}
685+
686+
TEST(BehaviorTreeFactory, MalformedXML_MultipleBTChildElements)
687+
{
688+
// BehaviorTree with more than one child element
689+
const char* xml = R"(
690+
<root BTCPP_format="4">
691+
<BehaviorTree ID="Main">
692+
<AlwaysSuccess/>
693+
<AlwaysFailure/>
694+
</BehaviorTree>
695+
</root>)";
696+
697+
BehaviorTreeFactory factory;
698+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
699+
}
700+
701+
TEST(BehaviorTreeFactory, MalformedXML_CompletelyEmpty)
702+
{
703+
// Completely empty string
704+
BehaviorTreeFactory factory;
705+
EXPECT_ANY_THROW(factory.createTreeFromText(""));
706+
}
707+
708+
TEST(BehaviorTreeFactory, MalformedXML_EmptyRoot)
709+
{
710+
// Root element with no children at all
711+
const char* xml = R"(<root BTCPP_format="4"></root>)";
712+
713+
BehaviorTreeFactory factory;
714+
// No BehaviorTree elements: registering succeeds but creating
715+
// a tree should fail because there is nothing to instantiate.
716+
factory.registerBehaviorTreeFromText(xml);
717+
EXPECT_ANY_THROW(factory.createTree("MainTree"));
718+
}
719+
720+
TEST(BehaviorTreeFactory, MalformedXML_UnknownNodeType)
721+
{
722+
// Reference to a node type that is not registered
723+
const char* xml = R"(
724+
<root BTCPP_format="4">
725+
<BehaviorTree ID="Main">
726+
<NonExistentNodeType/>
727+
</BehaviorTree>
728+
</root>)";
729+
730+
BehaviorTreeFactory factory;
731+
EXPECT_THROW(factory.createTreeFromText(xml), RuntimeError);
732+
}

0 commit comments

Comments
 (0)