Skip to content

Commit 18a5391

Browse files
Merge pull request #1022 from BehaviorTree/thread_sanitizer
Thread and Address sanitizers (#1019 + fixes)
2 parents 6f16449 + 831cadd commit 18a5391

File tree

10 files changed

+168
-41
lines changed

10 files changed

+168
-41
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: cmake Ubuntu Sanitizers
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
pull_request:
8+
types: [opened, synchronize, reopened]
9+
10+
env:
11+
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
12+
BUILD_TYPE: Debug
13+
14+
jobs:
15+
build:
16+
# The CMake configure and build commands are platform agnostic and should work equally
17+
# well on Windows or Mac. You can convert this to a matrix build if you need
18+
# cross-platform coverage.
19+
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
20+
runs-on: ${{ matrix.os }}
21+
strategy:
22+
fail-fast: false
23+
matrix:
24+
os: [ubuntu-22.04]
25+
sanitizer: [asan_ubsan, tsan]
26+
27+
steps:
28+
- uses: actions/checkout@v2
29+
30+
- name: Install Conan
31+
id: conan
32+
uses: turtlebrowser/get-conan@main
33+
34+
- name: Create default profile
35+
run: conan profile detect
36+
37+
- name: Install conan dependencies
38+
run: conan install conanfile.py -s build_type=${{env.BUILD_TYPE}} --build=missing
39+
40+
- name: Normalize build type
41+
shell: bash
42+
# The build type is Capitalized, e.g. Release, but the preset is all lowercase, e.g. release.
43+
# There is no built in way to do string manipulations on GHA as far as I know.`
44+
run: echo "BUILD_TYPE_LOWERCASE=$(echo "${BUILD_TYPE}" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
45+
46+
- name: Configure CMake
47+
shell: bash
48+
run: |
49+
if [[ "${{ matrix.sanitizer }}" == "asan_ubsan" ]]; then
50+
cmake --preset conan-${{ env.BUILD_TYPE_LOWERCASE }} \
51+
-DBTCPP_ENABLE_ASAN:BOOL=ON -DBTCPP_ENABLE_UBSAN:BOOL=ON
52+
else
53+
cmake --preset conan-${{ env.BUILD_TYPE_LOWERCASE }} \
54+
-DBTCPP_ENABLE_TSAN:BOOL=ON
55+
fi
56+
57+
- name: Build
58+
shell: bash
59+
run: cmake --build --preset conan-${{ env.BUILD_TYPE_LOWERCASE }}
60+
61+
- name: run test (Linux + Address and Undefined Behavior Sanitizers)
62+
env:
63+
GTEST_COLOR: "On"
64+
ASAN_OPTIONS: "color=always"
65+
UBSAN_OPTIONS: "halt_on_error=1:print_stacktrace=1:color=always"
66+
TSAN_OPTIONS: "color=always"
67+
# There is a known issue with TSAN on recent kernel versions. Without the vm.mmap_rnd_bits=28
68+
# workaround all binaries with TSan enabled crash with "FATAL: ThreadSanitizer: unexpected memory mapping"
69+
run: sudo sysctl vm.mmap_rnd_bits=28 && ctest --test-dir build/${{env.BUILD_TYPE}} --output-on-failure

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ option(BTCPP_EXAMPLES "Build tutorials and examples" ON)
1313
option(BUILD_TESTING "Build the unit tests" ON)
1414
option(BTCPP_GROOT_INTERFACE "Add Groot2 connection. Requires ZeroMQ" ON)
1515
option(BTCPP_SQLITE_LOGGING "Add SQLite logging." ON)
16+
option(BTCPP_ENABLE_ASAN "Enable Address Sanitizer" OFF)
17+
option(BTCPP_ENABLE_UBSAN "Enable Undefined Behavior Sanitizer" OFF)
18+
option(BTCPP_ENABLE_TSAN "Enable Thread Sanitizer" OFF)
1619

1720
option(USE_V3_COMPATIBLE_NAMES "Use some alias to compile more easily old 3.x code" OFF)
1821
option(ENABLE_FUZZING "Enable fuzzing builds" OFF)
@@ -54,6 +57,8 @@ endif()
5457
set(CMAKE_CONFIG_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake")
5558
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CONFIG_PATH}")
5659

60+
include(sanitizers)
61+
5762
set(BTCPP_LIBRARY ${PROJECT_NAME})
5863

5964
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)

cmake/sanitizers.cmake

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
if(BTCPP_ENABLE_ASAN OR BTCPP_ENABLE_UBSAN OR BTCPP_ENABLE_TSAN)
2+
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug|RelWithDebInfo")
3+
message(FATAL_ERROR "Sanitizers require debug symbols. Please set CMAKE_BUILD_TYPE to Debug or RelWithDebInfo.")
4+
endif()
5+
add_compile_options(-fno-omit-frame-pointer)
6+
endif()
7+
8+
# Address Sanitizer and Undefined Behavior Sanitizer can be run at the same time.
9+
# Thread Sanitizer requires its own build.
10+
if(BTCPP_ENABLE_TSAN AND (BTCPP_ENABLE_ASAN OR BTCPP_ENABLE_UBSAN))
11+
message(FATAL_ERROR "TSan is not compatible with ASan or UBSan. ASan and UBSan can run together, but TSan requires its own separate build.")
12+
endif()
13+
14+
if(BTCPP_ENABLE_ASAN)
15+
message(STATUS "Address Sanitizer enabled")
16+
add_compile_options(-fsanitize=address)
17+
add_link_options(-fsanitize=address)
18+
endif()
19+
20+
if(BTCPP_ENABLE_UBSAN)
21+
message(STATUS "Undefined Behavior Sanitizer enabled")
22+
add_compile_options(-fsanitize=undefined)
23+
add_link_options(-fsanitize=undefined)
24+
endif()
25+
26+
if(BTCPP_ENABLE_TSAN)
27+
message(STATUS "Thread Sanitizer enabled")
28+
add_compile_options(-fsanitize=thread)
29+
add_link_options(-fsanitize=thread)
30+
add_compile_definitions(USE_SANITIZE_THREAD)
31+
endif()

include/behaviortree_cpp/blackboard.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class Blackboard
141141
const Blackboard* rootBlackboard() const;
142142

143143
private:
144-
mutable std::mutex mutex_;
144+
mutable std::mutex storage_mutex_;
145145
mutable std::recursive_mutex entry_mutex_;
146146
std::unordered_map<std::string, std::shared_ptr<Entry>> storage_;
147147
std::weak_ptr<Blackboard> parent_bb_;
@@ -186,7 +186,7 @@ inline T Blackboard::get(const std::string& key) const
186186

187187
inline void Blackboard::unset(const std::string& key)
188188
{
189-
std::unique_lock lock(mutex_);
189+
std::unique_lock storage_lock(storage_mutex_);
190190

191191
// check local storage
192192
auto it = storage_.find(key);
@@ -207,15 +207,15 @@ inline void Blackboard::set(const std::string& key, const T& value)
207207
rootBlackboard()->set(key.substr(1, key.size() - 1), value);
208208
return;
209209
}
210-
std::unique_lock lock(mutex_);
210+
std::unique_lock storage_lock(storage_mutex_);
211211

212212
// check local storage
213213
auto it = storage_.find(key);
214214
if(it == storage_.end())
215215
{
216216
// create a new entry
217217
Any new_value(value);
218-
lock.unlock();
218+
storage_lock.unlock();
219219
std::shared_ptr<Blackboard::Entry> entry;
220220
// if a new generic port is created with a string, it's type should be AnyTypeAllowed
221221
if constexpr(std::is_same_v<std::string, T>)
@@ -228,7 +228,7 @@ inline void Blackboard::set(const std::string& key, const T& value)
228228
GetAnyFromStringFunctor<T>());
229229
entry = createEntryImpl(key, new_port);
230230
}
231-
lock.lock();
231+
storage_lock.lock();
232232

233233
entry->value = new_value;
234234
entry->sequence_id++;
@@ -239,6 +239,8 @@ inline void Blackboard::set(const std::string& key, const T& value)
239239
// this is not the first time we set this entry, we need to check
240240
// if the type is the same or not.
241241
Entry& entry = *it->second;
242+
storage_lock.unlock();
243+
242244
std::scoped_lock scoped_lock(entry.entry_mutex);
243245

244246
Any& previous_any = entry.value;

include/behaviortree_cpp/utils/timer_queue.h

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ class Semaphore
2323

2424
void notify()
2525
{
26-
{
27-
std::lock_guard<std::mutex> lock(m_mtx);
28-
m_count++;
29-
}
26+
m_count.fetch_add(1);
3027
m_cv.notify_one();
3128
}
3229

@@ -38,8 +35,15 @@ class Semaphore
3835
{
3936
return false;
4037
}
41-
m_count--;
38+
// Only decrement if there is a real count. If we woke because of manualUnlock,
39+
// m_count may be zero and we must not decrement it.
40+
if(m_count > 0)
41+
{
42+
m_count.fetch_sub(1);
43+
}
44+
// Clear the manual unlock flag
4245
m_unlock = false;
46+
4347
return true;
4448
}
4549

@@ -52,7 +56,7 @@ class Semaphore
5256
private:
5357
std::mutex m_mtx;
5458
std::condition_variable m_cv;
55-
unsigned m_count = 0;
59+
std::atomic_uint m_count = 0;
5660
std::atomic_bool m_unlock = false;
5761
};
5862
} // namespace details
@@ -74,15 +78,18 @@ class TimerQueue
7478
public:
7579
TimerQueue()
7680
{
77-
m_th = std::thread([this] { run(); });
81+
m_finish.store(false);
82+
m_thread = std::thread([this]() { run(); });
7883
}
7984

8085
~TimerQueue()
8186
{
82-
m_finish = true;
87+
m_finish.store(true);
8388
cancelAll();
84-
m_checkWork.manualUnlock();
85-
m_th.join();
89+
if(m_thread.joinable())
90+
{
91+
m_thread.join();
92+
}
8693
}
8794

8895
//! Adds a new timer
@@ -174,7 +181,7 @@ class TimerQueue
174181

175182
void run()
176183
{
177-
while(!m_finish)
184+
while(!m_finish.load())
178185
{
179186
auto end = calcWaitTime();
180187
if(end.first)
@@ -239,8 +246,8 @@ class TimerQueue
239246
}
240247

241248
details::Semaphore m_checkWork;
242-
std::thread m_th;
243-
bool m_finish = false;
249+
std::thread m_thread;
250+
std::atomic_bool m_finish = false;
244251
uint64_t m_idcounter = 0;
245252

246253
struct WorkItem

src/blackboard.cpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ Blackboard::getEntry(const std::string& key) const
5252
return rootBlackboard()->getEntry(key.substr(1, key.size() - 1));
5353
}
5454

55-
std::unique_lock<std::mutex> lock(mutex_);
56-
auto it = storage_.find(key);
57-
if(it != storage_.end())
5855
{
59-
return it->second;
56+
std::unique_lock<std::mutex> storage_lock(storage_mutex_);
57+
auto it = storage_.find(key);
58+
if(it != storage_.end())
59+
{
60+
return it->second;
61+
}
6062
}
6163
// not found. Try autoremapping
6264
if(auto parent = parent_bb_.lock())
@@ -130,7 +132,7 @@ std::vector<StringView> Blackboard::getKeys() const
130132

131133
void Blackboard::clear()
132134
{
133-
std::unique_lock<std::mutex> lock(mutex_);
135+
std::unique_lock<std::mutex> storage_lock(storage_mutex_);
134136
storage_.clear();
135137
}
136138

@@ -157,8 +159,10 @@ void Blackboard::createEntry(const std::string& key, const TypeInfo& info)
157159

158160
void Blackboard::cloneInto(Blackboard& dst) const
159161
{
160-
std::unique_lock lk1(mutex_);
161-
std::unique_lock lk2(dst.mutex_);
162+
// Lock both mutexes without risking lock-order inversion.
163+
std::unique_lock<std::mutex> lk1(storage_mutex_, std::defer_lock);
164+
std::unique_lock<std::mutex> lk2(dst.storage_mutex_, std::defer_lock);
165+
std::lock(lk1, lk2);
162166

163167
// keys that are not updated must be removed.
164168
std::unordered_set<std::string> keys_to_remove;
@@ -212,7 +216,7 @@ Blackboard::Ptr Blackboard::parent()
212216
std::shared_ptr<Blackboard::Entry> Blackboard::createEntryImpl(const std::string& key,
213217
const TypeInfo& info)
214218
{
215-
std::unique_lock<std::mutex> lock(mutex_);
219+
std::unique_lock<std::mutex> storage_lock(storage_mutex_);
216220
// This function might be called recursively, when we do remapping, because we move
217221
// to the top scope to find already existing entries
218222

src/tree_node.cpp

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,19 +106,13 @@ NodeStatus TreeNode::executeTick()
106106
if(!substituted)
107107
{
108108
using namespace std::chrono;
109-
110-
auto t1 = steady_clock::now();
111-
// trick to prevent the compile from reordering the order of execution. See #861
112-
// This makes sure that the code is executed at the end of this scope
113-
std::shared_ptr<void> execute_later(nullptr, [&](...) {
114-
auto t2 = steady_clock::now();
115-
if(monitor_tick)
116-
{
117-
monitor_tick(*this, new_status, duration_cast<microseconds>(t2 - t1));
118-
}
119-
});
120-
109+
const auto t1 = steady_clock::now();
121110
new_status = tick();
111+
const auto t2 = steady_clock::now();
112+
if(monitor_tick)
113+
{
114+
monitor_tick(*this, new_status, duration_cast<microseconds>(t2 - t1));
115+
}
122116
}
123117
}
124118

tests/CMakeLists.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,28 @@ if(ament_cmake_FOUND)
4343

4444
else()
4545

46+
enable_testing()
47+
4648
find_package(GTest REQUIRED)
49+
include(GoogleTest)
4750

48-
enable_testing()
4951
add_executable(behaviortree_cpp_test ${BT_TESTS})
50-
add_test(NAME btcpp_test COMMAND behaviortree_cpp_test)
5152

5253
target_link_libraries(behaviortree_cpp_test
5354
GTest::gtest
5455
GTest::gtest_main)
5556

57+
# gtest_discover_tests queries the test executable for available tests and registers them on ctest individually
58+
# On Windows it needs a little help to find the shared libraries
59+
if(WIN32)
60+
gtest_discover_tests(behaviortree_cpp_test
61+
DISCOVERY_MODE PRE_TEST
62+
DISCOVERY_ENVIRONMENT "PATH=$<TARGET_FILE_DIR:behaviortree_cpp_test>;$ENV{PATH}"
63+
)
64+
else()
65+
gtest_discover_tests(behaviortree_cpp_test)
66+
endif()
67+
5668
endif()
5769

5870
target_include_directories(behaviortree_cpp_test PRIVATE include)

tests/gtest_blackboard.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ TEST(BlackboardTest, CheckTypeSafety)
294294
ASSERT_TRUE(is);
295295
}
296296

297+
#ifndef USE_SANITIZE_THREAD
298+
297299
TEST(BlackboardTest, AnyPtrLocked)
298300
{
299301
auto blackboard = Blackboard::create();
@@ -346,6 +348,7 @@ TEST(BlackboardTest, AnyPtrLocked)
346348
ASSERT_NE(cycles, value);
347349
}
348350
}
351+
#endif
349352

350353
TEST(BlackboardTest, SetStringView)
351354
{

tests/gtest_decorator.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ struct RetryTest : testing::Test
6868

6969
struct TimeoutAndRetry : testing::Test
7070
{
71-
BT::TimeoutNode timeout_root;
7271
BT::RetryNode retry;
72+
BT::TimeoutNode timeout_root;
7373
BT::SyncActionTest action;
7474

7575
TimeoutAndRetry() : timeout_root("deadline", 9), retry("retry", 1000), action("action")

0 commit comments

Comments
 (0)