Skip to content

Commit 11e99cb

Browse files
authored
Fix DbgCtl use-after-free shutdown crash via leaky singleton (#12777)
The DbgCtl registry was experiencing use-after-free crashes during program shutdown, particularly visible in CI regression tests. The root cause was the undefined destruction order of static objects in C++. Problem: - DbgCtl objects can be static or thread-local with lifetimes spanning program execution - When one compilation unit's DbgCtl destructs and triggers registry cleanup, other compilation units may still have DbgCtl objects with pointers into that registry - Thread exit order is also unpredictable, causing similar issues when thread-local DbgCtl objects destruct This implements the "leaky singleton" pattern where the registry is: - Created on first use - Never destroyed (destructor is now = default) - Memory (~20KB) reclaimed by OS at process exit Fixes: #12776
1 parent dd3455c commit 11e99cb

File tree

6 files changed

+24
-79
lines changed

6 files changed

+24
-79
lines changed

ci/asan_leak_suppression/regression.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ leak:RegressionTest_HostDBProcessor
1919
leak:RegressionTest_DNS
2020
leak:RegressionTest_UDPNet_echo
2121
leak:HttpConfig::startup
22+
# DbgCtl leaky singleton pattern - intentional, see issue #12776.
23+
leak:DbgCtl::_new_reference
24+
leak:_new_reference
2225
# libswoc
2326
leak:MemArena::make_block

include/tsutil/DbgCtl.h

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,10 @@ class DbgCtl
6767
//
6868
DbgCtl() : _ptr{&_No_tag_dummy()} {}
6969

70-
~DbgCtl()
71-
{
72-
if (_ptr != &_No_tag_dummy()) {
73-
_rm_reference();
74-
}
75-
}
70+
// Default destructor: the registry uses a "leaky singleton" pattern to avoid
71+
// use-after-free crashes during shutdown due to undefined static destruction
72+
// order. See issue #12776.
73+
~DbgCtl() = default;
7674

7775
// No copying. Only moving from a tagged to a tagless instance allowed.
7876
//
@@ -153,8 +151,6 @@ class DbgCtl
153151

154152
static const _TagData *_new_reference(char const *tag);
155153

156-
static void _rm_reference();
157-
158154
class _RegistryAccessor;
159155

160156
static std::atomic<int> _config_mode;

plugins/esi/test/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ macro(ADD_ESI_TEST NAME)
2222
add_executable(${NAME} print_funcs.cc ${ARGN})
2323
target_link_libraries(${NAME} PRIVATE esitest)
2424
add_catch2_test(NAME ${NAME}_esi COMMAND $<TARGET_FILE:${NAME}>)
25+
# Use ASan leak suppression for intentional DbgCtl leaks (issue #12776)
26+
set_tests_properties(
27+
${NAME}_esi PROPERTIES ENVIRONMENT
28+
"LSAN_OPTIONS=suppressions=${CMAKE_CURRENT_SOURCE_DIR}/esi_test_leak_suppression.txt"
29+
)
2530
endmacro()
2631

2732
add_esi_test(test_docnode docnode_test.cc)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ESI unit test leak suppressions
2+
# DbgCtl leaky singleton pattern - intentional, see issue #12776
3+
leak:DbgCtl::_new_reference
4+
leak:_new_reference
5+

plugins/esi/test/print_funcs.cc

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,43 +63,29 @@ class DbgCtl::_RegistryAccessor
6363
{
6464
public:
6565
// No mutex protection, assuming unit test is single threaded.
66-
//
6766
static std::map<char const *, bool> &
6867
registry()
6968
{
7069
static std::map<char const *, bool> r;
7170
return r;
7271
}
73-
static inline int ref_count{0};
7472
};
7573

7674
std::atomic<int> DbgCtl::_config_mode{1};
7775

7876
DbgCtl::_TagData const *
7977
DbgCtl::_new_reference(char const *tag)
8078
{
81-
++_RegistryAccessor::ref_count;
82-
8379
auto it{_RegistryAccessor::registry().find(tag)};
8480
if (it == _RegistryAccessor::registry().end()) {
85-
char *s = new char[std::strlen(tag) + 1];
81+
char *s = new char[std::strlen(tag) + 1]; // Never deleted - leaky singleton pattern.
8682
std::strcpy(s, tag);
8783
auto r{_RegistryAccessor::registry().emplace(s, true)}; // Tag is always enabled.
8884
it = r.first;
8985
}
9086
return &(*it);
9187
}
9288

93-
void
94-
DbgCtl::_rm_reference()
95-
{
96-
if (!--_RegistryAccessor::ref_count) {
97-
for (auto &elem : _RegistryAccessor::registry()) {
98-
delete[] elem.first;
99-
}
100-
}
101-
}
102-
10389
bool
10490
DbgCtl::_override_global_on()
10591
{

src/tsutil/DbgCtl.cc

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ DbgCtl::operator=(DbgCtl &&src)
5454
return *this;
5555
}
5656

57-
// The resistry of fast debug controllers has a ugly implementation to handle the whole-program initialization
58-
// and destruction order problem with C++.
59-
//
6057
class DbgCtl::_RegistryAccessor
6158
{
6259
private:
@@ -79,15 +76,8 @@ class DbgCtl::_RegistryAccessor
7976
private:
8077
Registry() = default;
8178

82-
// Mutex must be locked before this is called.
83-
//
84-
~Registry()
85-
{
86-
for (auto &elem : map) {
87-
delete[] elem.first;
88-
}
89-
_mtx.unlock();
90-
}
79+
// Destructor never called - this is a leaky singleton. See issue #12776.
80+
~Registry() = default;
9181

9282
std::mutex _mtx;
9383

@@ -115,29 +105,15 @@ class DbgCtl::_RegistryAccessor
115105
}
116106
}
117107

118-
// This is not static so it can't be called with the registry mutex is unlocked. It should not be called
119-
// after registry is deleted.
108+
// This is not static so it can't be called with the registry mutex unlocked.
109+
// The registry is never deleted (leaky singleton pattern), so it's always safe to call after creation.
120110
//
121111
Registry &
122112
data()
123113
{
124114
return *_registry_instance;
125115
}
126116

127-
void
128-
delete_registry()
129-
{
130-
auto r = _registry_instance.load();
131-
debug_assert(r != nullptr);
132-
_registry_instance = nullptr;
133-
delete r;
134-
_mtx_is_locked = false;
135-
}
136-
137-
// Reference count of references to Registry.
138-
//
139-
inline static std::atomic<unsigned> registry_reference_count{0};
140-
141117
private:
142118
bool _mtx_is_locked{false};
143119

@@ -152,13 +128,6 @@ DbgCtl::_new_reference(char const *tag)
152128

153129
_TagData *new_tag_data{nullptr};
154130

155-
// DbgCtl instances may be declared as static objects in the destructors of objects not destroyed till program exit.
156-
// So, we must handle the case where the construction of such instances of DbgCtl overlaps with the destruction of
157-
// other instances of DbgCtl. That is why it is important to make sure the reference count is non-zero before
158-
// constructing _RegistryAccessor. The _RegistryAccessor constructor is thereby able to assume that, if it creates
159-
// the Registry, the new Registry will not be destroyed before the mutex in the new Registry is locked.
160-
161-
++_RegistryAccessor::registry_reference_count;
162131
{
163132
_RegistryAccessor ra;
164133

@@ -172,7 +141,7 @@ DbgCtl::_new_reference(char const *tag)
172141

173142
debug_assert(sz > 0);
174143

175-
char *t = new char[sz + 1]; // Deleted by ~Registry().
144+
char *t = new char[sz + 1]; // Never deleted (leaky singleton) - reclaimed by OS at process exit.
176145
std::memcpy(t, tag, sz + 1);
177146
_TagData new_elem{t, false};
178147

@@ -201,30 +170,11 @@ DbgCtl::_new_reference(char const *tag)
201170
return new_tag_data;
202171
}
203172

204-
void
205-
DbgCtl::_rm_reference()
206-
{
207-
_RegistryAccessor ra;
208-
209-
debug_assert(ra.registry_reference_count != 0);
210-
211-
--ra.registry_reference_count;
212-
213-
if (0 == ra.registry_reference_count) {
214-
ra.delete_registry();
215-
}
216-
}
217-
218173
void
219174
DbgCtl::update(const std::function<bool(const char *)> &f)
220175
{
221176
_RegistryAccessor ra;
222-
223-
if (!ra.registry_reference_count) {
224-
return;
225-
}
226-
227-
auto &d{ra.data()};
177+
auto &d{ra.data()};
228178

229179
for (auto &i : d.map) {
230180
i.second = f(i.first);

0 commit comments

Comments
 (0)