Skip to content

Commit ed92f32

Browse files
facontidavideclaude
andcommitted
add Blackboard::getKeyNames() and deprecate getKeys()
getKeys() returns StringViews into the keys of the storage map, valid only while the storage lock is held, i.e. never for the caller. ExportBlackboardToJSON built a std::string from them while another thread could unset() the entry: heap-use-after-free (Groot2 thread vs UnsetBlackboard on the tick thread). getKeyNames() returns copies made under the lock. getKeys() is unchanged but deprecated, so the change is purely additive (no ABI/API break). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 2bcfe9b commit ed92f32

5 files changed

Lines changed: 62 additions & 16 deletions

File tree

‎include/behaviortree_cpp/blackboard.h‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,14 @@ class Blackboard
123123

124124
void debugMessage() const;
125125

126-
[[nodiscard]] std::vector<StringView> getKeys() const;
126+
/// The names of all the entries stored in this blackboard (copies).
127+
[[nodiscard]] std::vector<std::string> getKeyNames() const;
128+
129+
// The views point into the storage: they dangle as soon as another thread
130+
// removes the entry.
131+
[[deprecated(
132+
"The views may dangle: use getKeyNames")]] [[nodiscard]] std::vector<StringView>
133+
getKeys() const;
127134

128135
[[deprecated("This command is unsafe. Consider using Backup/Restore instead")]] void
129136
clear();

‎src/blackboard.cpp‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,18 @@ void Blackboard::debugMessage() const
229229
}
230230
}
231231

232+
std::vector<std::string> Blackboard::getKeyNames() const
233+
{
234+
const std::shared_lock storage_lock(storage_mutex_);
235+
std::vector<std::string> out;
236+
out.reserve(storage_.size());
237+
for(const auto& entry_it : storage_)
238+
{
239+
out.push_back(entry_it.first);
240+
}
241+
return out;
242+
}
243+
232244
std::vector<StringView> Blackboard::getKeys() const
233245
{
234246
// Lock storage_mutex_ (shared) to prevent iterator invalidation and
@@ -452,9 +464,8 @@ std::shared_ptr<Blackboard::Entry> Blackboard::createEntryImpl(const std::string
452464
nlohmann::json ExportBlackboardToJSON(const Blackboard& blackboard)
453465
{
454466
nlohmann::json dest;
455-
for(auto entry_name : blackboard.getKeys())
467+
for(const auto& name : blackboard.getKeyNames())
456468
{
457-
const std::string name(entry_name);
458469
if(auto any_ref = blackboard.getAnyLocked(name))
459470
{
460471
if(auto any_ptr = any_ref.get())

‎tests/gtest_blackboard.cpp‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ TEST(BlackboardTest, AnyPtrLockedSurvivesUnset)
320320
ASSERT_TRUE(bool(locked));
321321

322322
blackboard->unset("value");
323-
ASSERT_TRUE(blackboard->getKeys().empty());
323+
ASSERT_TRUE(blackboard->getKeyNames().empty());
324324

325325
// the entry must stay alive as long as we hold the lock
326326
ASSERT_EQ(locked.get()->cast<int>(), 42);
@@ -338,7 +338,7 @@ TEST(BlackboardTest, AnyPtrLockedSurvivesClear)
338338
ASSERT_TRUE(bool(locked));
339339

340340
blackboard->clear();
341-
ASSERT_TRUE(blackboard->getKeys().empty());
341+
ASSERT_TRUE(blackboard->getKeyNames().empty());
342342
ASSERT_EQ(locked.get()->cast<int>(), 42);
343343
}
344344

@@ -353,7 +353,7 @@ TEST(BlackboardTest, AnyPtrLockedSurvivesCloneInto)
353353

354354
// "stale" doesn't exist in src, so cloneInto() removes it from dst
355355
src->cloneInto(*dst);
356-
ASSERT_TRUE(dst->getKeys().empty());
356+
ASSERT_TRUE(dst->getKeyNames().empty());
357357
ASSERT_EQ(locked.get()->cast<int>(), 42);
358358
}
359359

@@ -397,7 +397,7 @@ TEST(BlackboardTest, AnyPtrLockedCrossUnsetDoesNotDeadlock)
397397

398398
ASSERT_EQ(values[0], 1);
399399
ASSERT_EQ(values[1], 2);
400-
ASSERT_TRUE(blackboard->getKeys().empty());
400+
ASSERT_TRUE(blackboard->getKeyNames().empty());
401401
}
402402

403403
TEST(BlackboardTest, AnyPtrLockedDeferredEntryIsDestroyed)
@@ -674,7 +674,7 @@ TEST(BlackboardTest, BlackboardBackup)
674674
for(const auto& sub : tree.subtrees)
675675
{
676676
std::vector<std::string> keys;
677-
for(const auto& str_view : sub->blackboard->getKeys())
677+
for(const auto& str_view : sub->blackboard->getKeyNames())
678678
{
679679
keys.push_back(std::string(str_view));
680680
}
@@ -691,7 +691,7 @@ TEST(BlackboardTest, BlackboardBackup)
691691

692692
for(size_t i = 0; i < tree.subtrees.size(); i++)
693693
{
694-
const auto keys = tree.subtrees[i]->blackboard->getKeys();
694+
const auto keys = tree.subtrees[i]->blackboard->getKeyNames();
695695
ASSERT_EQ(expected_keys[i].size(), keys.size());
696696
for(size_t a = 0; a < keys.size(); a++)
697697
{

‎tests/gtest_blackboard_thread_safety.cpp‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ TEST(BlackboardThreadSafety, DebugMessageWhileModifying_Bug5)
301301
SUCCEED();
302302
}
303303

304-
// BUG-6: getKeys() iterates storage_ without holding storage_mutex_.
304+
// BUG-6: getKeyNames() iterates storage_ without holding storage_mutex_.
305305
// Also returns StringView into map keys which can dangle if entries are erased.
306306
TEST(BlackboardThreadSafety, GetKeysWhileModifying_Bug6)
307307
{
@@ -320,7 +320,7 @@ TEST(BlackboardThreadSafety, GetKeysWhileModifying_Bug6)
320320
auto key_reader = [&]() {
321321
for(int i = 0; i < kIterations; i++)
322322
{
323-
auto keys = bb->getKeys();
323+
auto keys = bb->getKeyNames();
324324
// Just access the keys to detect any race
325325
volatile size_t count = keys.size();
326326
(void)count;
@@ -335,3 +335,31 @@ TEST(BlackboardThreadSafety, GetKeysWhileModifying_Bug6)
335335

336336
SUCCEED();
337337
}
338+
339+
// ExportBlackboardToJSON used the StringViews returned by getKeyNames() after the
340+
// storage lock was released: a concurrent unset() frees the key they point to.
341+
// Meaningful with ASan/TSan. The keys are longer than the small string buffer,
342+
// so that they live on the heap.
343+
TEST(BlackboardThreadSafety, ExportToJsonWhileKeysAreRemoved)
344+
{
345+
auto bb = Blackboard::create();
346+
std::atomic_bool stop = false;
347+
348+
std::thread exporter([&]() {
349+
while(!stop)
350+
{
351+
const auto json = ExportBlackboardToJSON(*bb);
352+
ASSERT_LE(json.size(), 8U);
353+
}
354+
});
355+
356+
const std::string prefix = "a_key_that_is_too_long_for_the_small_string_buffer_";
357+
for(int i = 0; i < 20000; i++)
358+
{
359+
const std::string key = prefix + std::to_string(i % 8);
360+
bb->set(key, i);
361+
bb->unset(key);
362+
}
363+
stop = true;
364+
exporter.join();
365+
}

‎tests/script_parser_test.cpp‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ TEST(ParserTest, Equations)
130130
//-------------------
131131
const auto& variables = environment.vars;
132132
EXPECT_EQ(GetResult("x:= 3; y:=5; x+y").cast<double>(), 8.0);
133-
EXPECT_EQ(variables->getKeys().size(), 2);
133+
EXPECT_EQ(variables->getKeyNames().size(), 2);
134134
EXPECT_EQ(variables->get<double>("x"), 3.0);
135135
EXPECT_EQ(variables->get<double>("y"), 5.0);
136136

@@ -170,7 +170,7 @@ TEST(ParserTest, Equations)
170170
"o "
171171
"worl"
172172
"d");
173-
EXPECT_EQ(variables->getKeys().size(), 5);
173+
EXPECT_EQ(variables->getKeyNames().size(), 5);
174174
EXPECT_EQ(variables->get<std::string>("A"), "hello");
175175
EXPECT_EQ(variables->get<std::string>("B"), " ");
176176
EXPECT_EQ(variables->get<std::string>("C"), "world");
@@ -181,7 +181,7 @@ TEST(ParserTest, Equations)
181181
"C= 'left ' ")
182182
.empty());
183183

184-
EXPECT_EQ(variables->getKeys().size(), 5);
184+
EXPECT_EQ(variables->getKeyNames().size(), 5);
185185
EXPECT_EQ(variables->get<std::string>("A"), " right");
186186
EXPECT_EQ(variables->get<std::string>("B"), " center ");
187187
EXPECT_EQ(variables->get<std::string>("C"), "left ");
@@ -196,9 +196,9 @@ TEST(ParserTest, Equations)
196196
EXPECT_ANY_THROW(GetResult(" 'hello' = 2.0 "));
197197
EXPECT_ANY_THROW(GetResult(" 3.0 = 2.0 "));
198198

199-
size_t prev_size = variables->getKeys().size();
199+
size_t prev_size = variables->getKeyNames().size();
200200
EXPECT_ANY_THROW(GetResult("new_var=69"));
201-
EXPECT_EQ(variables->getKeys().size(), prev_size); // shouldn't increase
201+
EXPECT_EQ(variables->getKeyNames().size(), prev_size); // shouldn't increase
202202

203203
// check comparisons
204204
EXPECT_EQ(GetResult("x < y").cast<int>(), 1);

0 commit comments

Comments
 (0)