Skip to content

Commit 3708289

Browse files
fix data race in SetBlackboardNode copying a blackboard entry (#1236)
1 parent 29a2ccf commit 3708289

2 files changed

Lines changed: 66 additions & 12 deletions

File tree

‎include/behaviortree_cpp/actions/set_blackboard_node.h‎

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
#include "behaviortree_cpp/action_node.h"
1717

18+
#include <mutex>
19+
1820
namespace BT
1921
{
2022
/**
@@ -76,13 +78,18 @@ class SetBlackboardNode : public SyncActionNode
7678
{
7779
throw RuntimeError("Can't find the port referred by [value]");
7880
}
81+
// Entry::value and Entry::info are shared with any thread that writes
82+
// the same entry (Blackboard::set holds entry_mutex while reassigning
83+
// them), so they must be copied under the same mutex.
84+
std::unique_lock src_lock(src_entry->entry_mutex);
85+
out_value = src_entry->value;
86+
const TypeInfo src_info = src_entry->info;
87+
src_lock.unlock();
7988
if(!dst_entry)
8089
{
81-
config().blackboard->createEntry(output_key, src_entry->info);
90+
config().blackboard->createEntry(output_key, src_info);
8291
dst_entry = config().blackboard->getEntry(output_key);
8392
}
84-
85-
out_value = src_entry->value;
8693
}
8794
else
8895
{
@@ -94,17 +101,24 @@ class SetBlackboardNode : public SyncActionNode
94101

95102
// avoid type issues when port is remapped: current implementation of the set might be a little bit problematic for initialized on the fly values
96103
// this still does not attack math issues
97-
if(dst_entry && dst_entry->info.type() != typeid(std::string) && out_value.isString())
104+
if(dst_entry && out_value.isString())
98105
{
99-
try
100-
{
101-
out_value = dst_entry->info.parseString(out_value.cast<std::string>());
102-
}
103-
catch(const std::exception& e)
106+
// same as above: Entry::info may be reassigned under entry_mutex by a
107+
// concurrent Blackboard::set, so take a copy under the same mutex.
108+
std::unique_lock dst_lock(dst_entry->entry_mutex);
109+
const TypeInfo dst_info = dst_entry->info;
110+
dst_lock.unlock();
111+
if(dst_info.type() != typeid(std::string))
104112
{
105-
throw LogicError("Can't convert string [", out_value.cast<std::string>(),
106-
"] to type [", BT::demangle(dst_entry->info.type()),
107-
"]: ", e.what());
113+
try
114+
{
115+
out_value = dst_info.parseString(out_value.cast<std::string>());
116+
}
117+
catch(const std::exception& e)
118+
{
119+
throw LogicError("Can't convert string [", out_value.cast<std::string>(),
120+
"] to type [", BT::demangle(dst_info.type()), "]: ", e.what());
121+
}
108122
}
109123
}
110124

‎tests/gtest_blackboard_thread_safety.cpp‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
#include "behaviortree_cpp/blackboard.h"
10+
#include "behaviortree_cpp/bt_factory.h"
1011

1112
#include <atomic>
1213
#include <condition_variable>
@@ -336,6 +337,45 @@ TEST(BlackboardThreadSafety, GetKeysWhileModifying_Bug6)
336337
SUCCEED();
337338
}
338339

340+
// SetBlackboardNode::tick() copied Entry::value (and read Entry::info) of the
341+
// source entry without locking entry_mutex. A concurrent Blackboard::set() on
342+
// the same entry reassigns the stored Any under entry_mutex, so the unlocked
343+
// copy races with the destruction of the previous string buffer.
344+
// Meaningful with ASan/TSan. The strings are longer than the small string
345+
// buffer, so that the content lives on the heap.
346+
TEST(BlackboardThreadSafety, SetBlackboardNodeWhileWriting)
347+
{
348+
static const char* xml_text = R"(
349+
<root BTCPP_format="4">
350+
<BehaviorTree ID="MainTree">
351+
<SetBlackboard value="{src}" output_key="dst"/>
352+
</BehaviorTree>
353+
</root>)";
354+
355+
BehaviorTreeFactory factory;
356+
auto tree = factory.createTreeFromText(xml_text);
357+
auto bb = tree.subtrees.front()->blackboard;
358+
359+
const std::string prefix = "a_value_that_is_too_long_for_the_small_string_buffer_";
360+
bb->set("src", prefix);
361+
362+
std::atomic_bool stop = false;
363+
std::thread writer([&]() {
364+
int i = 0;
365+
while(!stop)
366+
{
367+
bb->set("src", prefix + std::to_string(i++ % 8));
368+
}
369+
});
370+
371+
for(int i = 0; i < 20000; i++)
372+
{
373+
tree.tickWhileRunning();
374+
}
375+
stop = true;
376+
writer.join();
377+
}
378+
339379
// ExportBlackboardToJSON used the StringViews returned by getKeyNames() after the
340380
// storage lock was released: a concurrent unset() frees the key they point to.
341381
// Meaningful with ASan/TSan. The keys are longer than the small string buffer,

0 commit comments

Comments
 (0)