Skip to content

Commit 2b9e571

Browse files
facontidavideclaude
andcommitted
Fix Blackboard thread-safety data races
Fix 6 data races in Blackboard, verified with ThreadSanitizer (clang-21): - set() new-entry path: wrote value/sequence_id/stamp without entry_mutex after createEntryImpl — add scoped_lock on entry_mutex - set() existing-entry path: held raw reference after unlocking storage_mutex_, risking use-after-free if concurrent unset() erases the entry — copy shared_ptr before unlocking - cloneInto(): read/wrote entry members without entry_mutex — add scoped_lock on src and dst entry mutexes - ImportBlackboardFromJSON(): wrote entry->value without any lock — add scoped_lock on entry_mutex - debugMessage(): iterated storage_ without storage_mutex_ — add unique_lock - getKeys(): iterated storage_ without storage_mutex_ — add unique_lock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a557f3c commit 2b9e571

File tree

4 files changed

+358
-4
lines changed

4 files changed

+358
-4
lines changed

include/behaviortree_cpp/blackboard.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,10 @@ inline void Blackboard::set(const std::string& key, const T& value)
300300
GetAnyFromStringFunctor<T>());
301301
entry = createEntryImpl(key, new_port);
302302
}
303-
storage_lock.lock();
304303

304+
// Lock entry_mutex before writing to prevent data races with
305+
// concurrent readers (BUG-1/BUG-8 fix).
306+
std::scoped_lock entry_lock(entry->entry_mutex);
305307
entry->value = new_value;
306308
entry->sequence_id++;
307309
entry->stamp = std::chrono::steady_clock::now().time_since_epoch();
@@ -310,8 +312,11 @@ inline void Blackboard::set(const std::string& key, const T& value)
310312
{
311313
// this is not the first time we set this entry, we need to check
312314
// if the type is the same or not.
313-
Entry& entry = *it->second;
315+
// Copy shared_ptr to prevent use-after-free if another thread
316+
// calls unset() while we hold the reference (BUG-2 fix).
317+
auto entry_ptr = it->second;
314318
storage_lock.unlock();
319+
Entry& entry = *entry_ptr;
315320

316321
std::scoped_lock scoped_lock(entry.entry_mutex);
317322

src/blackboard.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ void Blackboard::addSubtreeRemapping(StringView internal, StringView external)
103103

104104
void Blackboard::debugMessage() const
105105
{
106+
// Lock storage_mutex_ to prevent iterator invalidation from
107+
// concurrent modifications (BUG-5 fix).
108+
const std::unique_lock<std::mutex> storage_lock(storage_mutex_);
106109
for(const auto& [key, entry] : storage_)
107110
{
108111
auto port_type = entry->info.type();
@@ -136,6 +139,9 @@ void Blackboard::debugMessage() const
136139

137140
std::vector<StringView> Blackboard::getKeys() const
138141
{
142+
// Lock storage_mutex_ to prevent iterator invalidation and
143+
// dangling StringView from concurrent modifications (BUG-6 fix).
144+
const std::unique_lock<std::mutex> storage_lock(storage_mutex_);
139145
if(storage_.empty())
140146
{
141147
return {};
@@ -200,8 +206,10 @@ void Blackboard::cloneInto(Blackboard& dst) const
200206
auto it = dst_storage.find(src_key);
201207
if(it != dst_storage.end())
202208
{
203-
// overwrite
209+
// overwrite — lock both entry mutexes to prevent data races
210+
// with concurrent readers that hold shared_ptr<Entry> (BUG-3 fix).
204211
auto& dst_entry = it->second;
212+
std::scoped_lock entry_locks(src_entry->entry_mutex, dst_entry->entry_mutex);
205213
dst_entry->string_converter = src_entry->string_converter;
206214
dst_entry->value = src_entry->value;
207215
dst_entry->info = src_entry->info;
@@ -210,7 +218,8 @@ void Blackboard::cloneInto(Blackboard& dst) const
210218
}
211219
else
212220
{
213-
// create new
221+
// create new — lock src entry to read its members safely
222+
std::scoped_lock src_lock(src_entry->entry_mutex);
214223
auto new_entry = std::make_shared<Entry>(src_entry->info);
215224
new_entry->value = src_entry->value;
216225
new_entry->string_converter = src_entry->string_converter;
@@ -317,6 +326,8 @@ void ImportBlackboardFromJSON(const nlohmann::json& json, Blackboard& blackboard
317326
blackboard.createEntry(it.key(), res->second);
318327
entry = blackboard.getEntry(it.key());
319328
}
329+
// Lock entry_mutex before writing to prevent data races (BUG-4 fix).
330+
std::scoped_lock lk(entry->entry_mutex);
320331
entry->value = res->first;
321332
}
322333
}

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ set(BT_TESTS
5555
gtest_simple_string.cpp
5656
gtest_polymorphic_ports.cpp
5757
gtest_plugin_issue953.cpp
58+
gtest_blackboard_thread_safety.cpp
5859
gtest_xml_null_subtree_id.cpp
5960

6061
script_parser_test.cpp

0 commit comments

Comments
 (0)