Skip to content

Commit 9b63b50

Browse files
fix use-after-free of the timer node mutex on destruction (#1180)
* fix use-after-free of the timer node mutex on destruction * add regression test for tree destruction with pending timers Without the member reorder in sleep_node.h, delay_node.h and timeout_node.h, this test aborts on libc++ with "mutex lock failed: Invalid argument": the canceled timer handler locks the node mutex after its destructor has already run.
1 parent 94b8e14 commit 9b63b50

4 files changed

Lines changed: 48 additions & 3 deletions

File tree

‎include/behaviortree_cpp/actions/sleep_node.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ class SleepNode : public StatefulActionNode
4040
}
4141

4242
private:
43-
TimerQueue<> timer_;
4443
uint64_t timer_id_ = 0;
4544

4645
std::atomic_bool timer_waiting_ = false;
4746
std::mutex delay_mutex_;
47+
// Keep last: ~TimerQueue() joins the worker thread, so this must be destroyed
48+
// before delay_mutex_ and timer_waiting_, which the timer handler touches.
49+
TimerQueue<> timer_;
4850
};
4951

5052
} // namespace BT

‎include/behaviortree_cpp/decorators/delay_node.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ class DelayNode : public DecoratorNode
6565
void halt() override;
6666

6767
private:
68-
TimerQueue<> timer_;
6968
uint64_t timer_id_;
7069

7170
virtual BT::NodeStatus tick() override;
@@ -76,6 +75,9 @@ class DelayNode : public DecoratorNode
7675
unsigned msec_;
7776
bool read_parameter_from_ports_;
7877
std::mutex delay_mutex_;
78+
// Keep last: ~TimerQueue() joins the worker thread, so this must be destroyed
79+
// before delay_mutex_, which the timer handler locks.
80+
TimerQueue<> timer_;
7981
};
8082

8183
} // namespace BT

‎include/behaviortree_cpp/decorators/timeout_node.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,16 @@ class TimeoutNode : public DecoratorNode
7777

7878
void halt() override;
7979

80-
TimerQueue<> timer_;
8180
std::atomic_bool child_halted_ = false;
8281
uint64_t timer_id_;
8382

8483
unsigned msec_;
8584
bool read_parameter_from_ports_;
8685
std::atomic_bool timeout_started_ = false;
8786
std::mutex timeout_mutex_;
87+
// Keep last: ~TimerQueue() joins the worker thread, so this must be destroyed
88+
// before timeout_mutex_, which the timer handler locks.
89+
TimerQueue<> timer_;
8890
};
8991

9092
} // namespace BT

‎tests/gtest_decorator.cpp‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,45 @@ TEST(Decorator, Inverter_InSequence)
434434
ASSERT_EQ(status, NodeStatus::SUCCESS);
435435
}
436436

437+
// Regression test: Sleep, Delay and Timeout own a TimerQueue whose worker
438+
// thread locks a mutex belonging to the node. The TimerQueue member must be
439+
// destroyed (joining that thread) before the mutex, otherwise destroying a
440+
// tree with a pending timer lets the handler lock an already-destroyed mutex.
441+
// On libc++ this aborts with "mutex lock failed: Invalid argument".
442+
TEST(Decorator, DestroyTreeWhileTimerIsPending)
443+
{
444+
BT::BehaviorTreeFactory factory;
445+
446+
const std::string xml_text = R"(
447+
<root BTCPP_format="4">
448+
<BehaviorTree ID="PendingTimers">
449+
<Parallel success_count="6" failure_count="1">
450+
<Sleep msec="500"/>
451+
<Sleep msec="500"/>
452+
<Sleep msec="500"/>
453+
<Sleep msec="500"/>
454+
<Delay delay_msec="500">
455+
<AlwaysSuccess/>
456+
</Delay>
457+
<Timeout msec="500">
458+
<Sleep msec="500"/>
459+
</Timeout>
460+
</Parallel>
461+
</BehaviorTree>
462+
</root>)";
463+
464+
factory.registerBehaviorTreeFromText(xml_text);
465+
466+
for(int i = 0; i < 1000; i++)
467+
{
468+
auto tree = factory.createTree("PendingTimers");
469+
ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
470+
// The tree is destroyed here while all the timers are still pending:
471+
// each canceled handler runs on its TimerQueue worker thread, racing
472+
// with the destruction of the node members.
473+
}
474+
}
475+
437476
// Test KeepRunningUntilFailure decorator
438477
TEST(Decorator, KeepRunningUntilFailure)
439478
{

0 commit comments

Comments
 (0)