Skip to content

Commit d53a8ea

Browse files
Fix #1046: use-after-free when factory destroyed before tree (#1081)
* Fix #1046: prevent use-after-free when factory is destroyed before tree tick After createTree(), node manifest pointers pointed into the factory's internal map. If the factory was destroyed before ticking, these became dangling pointers causing heap-use-after-free on manifest lookups. Add Tree::remapManifestPointers() which re-points each node's NodeConfig::manifest from the factory's map to the tree's own copy, called in all three createTree* methods after copying manifests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix clang-tidy readability-implicit-bool-conversion warnings Use explicit != nullptr comparisons for pointer-to-bool conversions flagged by clang-tidy. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 25407ad commit d53a8ea

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

include/behaviortree_cpp/bt_factory.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,8 @@ class Tree
192192
}
193193

194194
private:
195+
friend class BehaviorTreeFactory;
196+
195197
std::shared_ptr<WakeUpSignal> wake_up_;
196198

197199
enum TickOption
@@ -203,6 +205,11 @@ class Tree
203205

204206
NodeStatus tickRoot(TickOption opt, std::chrono::milliseconds sleep_time);
205207

208+
// Fix #1046: re-point each node's NodeConfig::manifest from the
209+
// factory's map to the tree's own copy so the pointers remain
210+
// valid after the factory is destroyed.
211+
void remapManifestPointers();
212+
206213
uint16_t uid_counter_ = 0;
207214
};
208215

src/bt_factory.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ Tree BehaviorTreeFactory::createTreeFromText(const std::string& text,
354354
parser.loadFromText(text);
355355
auto tree = parser.instantiateTree(blackboard);
356356
tree.manifests = this->manifests();
357+
tree.remapManifestPointers();
357358
return tree;
358359
}
359360

@@ -373,6 +374,7 @@ Tree BehaviorTreeFactory::createTreeFromFile(const std::filesystem::path& file_p
373374
parser.loadFromFile(file_path);
374375
auto tree = parser.instantiateTree(blackboard);
375376
tree.manifests = this->manifests();
377+
tree.remapManifestPointers();
376378
return tree;
377379
}
378380

@@ -381,6 +383,7 @@ Tree BehaviorTreeFactory::createTree(const std::string& tree_name,
381383
{
382384
auto tree = _p->parser->instantiateTree(blackboard, tree_name);
383385
tree.manifests = this->manifests();
386+
tree.remapManifestPointers();
384387
return tree;
385388
}
386389

@@ -480,6 +483,25 @@ BehaviorTreeFactory::substitutionRules() const
480483

481484
Tree::Tree() = default;
482485

486+
void Tree::remapManifestPointers()
487+
{
488+
for(auto& subtree : subtrees)
489+
{
490+
for(auto& node : subtree->nodes)
491+
{
492+
const auto* old_manifest = node->config().manifest;
493+
if(old_manifest != nullptr)
494+
{
495+
auto it = manifests.find(old_manifest->registration_ID);
496+
if(it != manifests.end())
497+
{
498+
node->config().manifest = &(it->second);
499+
}
500+
}
501+
}
502+
}
503+
}
504+
483505
void Tree::initialize()
484506
{
485507
wake_up_ = std::make_shared<WakeUpSignal>();

tests/gtest_factory.cpp

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,3 +482,84 @@ TEST(BehaviorTreeFactory, addMetadataToManifest)
482482
const auto& modified_manifest = factory.manifests().at("SaySomething");
483483
EXPECT_EQ(modified_manifest.metadata, makeTestMetadata());
484484
}
485+
486+
// Action node used to reproduce issue #1046 (use-after-free on
487+
// manifest pointer). It calls getInput() for a port name that is
488+
// NOT in the XML, so getInputStamped falls through to the
489+
// manifest lookup.
490+
class ActionIssue1046 : public BT::SyncActionNode
491+
{
492+
public:
493+
ActionIssue1046(const std::string& name, const BT::NodeConfig& config)
494+
: BT::SyncActionNode(name, config)
495+
{}
496+
497+
BT::NodeStatus tick() override
498+
{
499+
// "value" is declared in providedPorts() but NOT set in the
500+
// XML, so getInput() will look it up in the manifest.
501+
// If the manifest pointer is dangling, this is a
502+
// use-after-free.
503+
int value = 0;
504+
auto res = getInput("value", value);
505+
// We expect this to fail (no default, not in XML), but it
506+
// must not crash.
507+
(void)res;
508+
return BT::NodeStatus::SUCCESS;
509+
}
510+
511+
static BT::PortsList providedPorts()
512+
{
513+
return { BT::InputPort<int>("value", "a test port") };
514+
}
515+
};
516+
517+
// Test for issue #1046: heap use-after-free when
518+
// BehaviorTreeFactory is destroyed before the tree is ticked.
519+
TEST(BehaviorTreeFactory, FactoryDestroyedBeforeTick)
520+
{
521+
// clang-format off
522+
// The XML deliberately does NOT set the "value" port so
523+
// that getInput falls through to the manifest to read
524+
// the port info. This triggers the dangling-pointer bug.
525+
static const char* xml_text_issue_1046 = R"(
526+
<root BTCPP_format="4" >
527+
<BehaviorTree ID="Main">
528+
<ActionIssue1046/>
529+
</BehaviorTree>
530+
</root> )";
531+
// clang-format on
532+
533+
BT::Tree tree;
534+
{
535+
// Factory created in an inner scope
536+
BT::BehaviorTreeFactory factory;
537+
factory.registerNodeType<ActionIssue1046>("ActionIssue1046");
538+
factory.registerBehaviorTreeFromText(xml_text_issue_1046);
539+
tree = factory.createTree("Main");
540+
// factory is destroyed here
541+
}
542+
543+
// Verify that every node's manifest pointer points into the
544+
// tree's own copy, not into freed factory memory.
545+
for(const auto& subtree : tree.subtrees)
546+
{
547+
for(const auto& node : subtree->nodes)
548+
{
549+
const auto* manifest_ptr =
550+
static_cast<const BT::TreeNode*>(node.get())->config().manifest;
551+
if(manifest_ptr)
552+
{
553+
auto it = tree.manifests.find(manifest_ptr->registration_ID);
554+
ASSERT_NE(it, tree.manifests.end());
555+
EXPECT_EQ(manifest_ptr, &(it->second));
556+
}
557+
}
558+
}
559+
560+
// Ticking after factory destruction should not crash.
561+
// Before the fix, NodeConfig::manifest pointed to a manifest
562+
// owned by the now-destroyed factory, causing use-after-free
563+
// when getInput() looked up the port in the manifest.
564+
EXPECT_EQ(BT::NodeStatus::SUCCESS, tree.tickWhileRunning());
565+
}

0 commit comments

Comments
 (0)