Skip to content

Commit c7e0cf1

Browse files
xusheng6claude
andauthored
Guard g_debuggerControllers for concurrent access (#1017)
Replace the raw pointer + manual realloc/placement-new with a heap-allocated ControllerState struct (mutex + vector), accessed via a function-local static pointer. This fixes potential data races when multiple threads call GetController, DeleteController, or ControllerExists concurrently. The ControllerState is intentionally leaked (never freed) so the mutex remains valid even during late cleanup -- e.g., Python's GC calling Destroy() via FFI after static destruction has begun. Fix #1016 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ee2355 commit c7e0cf1

2 files changed

Lines changed: 51 additions & 56 deletions

File tree

core/debuggercontroller.cpp

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,61 +1729,59 @@ void DebuggerController::LaunchOrConnect()
17291729
}
17301730

17311731

1732-
// Can't use a vector here as initialization order is not guaranteed.
1733-
DbgRef<DebuggerController>* DebuggerController::g_debuggerControllers = nullptr;
1734-
size_t DebuggerController::g_controllerCount = 0;
1732+
// Use a function-local static to avoid two problems:
1733+
// 1. Static initialization order fiasco -- if any other translation unit's static initializer
1734+
// calls GetController before this TU is initialized, a global would not yet be constructed.
1735+
// 2. Static destruction order -- during process exit, a namespace-scope std::mutex can be
1736+
// destroyed before cleanup code (e.g., Python GC calling Destroy() via FFI) tries to lock it,
1737+
// causing "mutex lock failed: Invalid argument". Bundling the mutex and vector in the same
1738+
// function-local static ensures they share the same lifetime.
1739+
DebuggerController::ControllerState& DebuggerController::GetControllerState()
1740+
{
1741+
// Intentionally heap-allocated and never freed. A function-local static would still be
1742+
// destroyed during static cleanup, but Python's GC can call Destroy() -> DeleteController()
1743+
// even later than that, hitting a destroyed mutex. Leaking the allocation ensures the mutex
1744+
// and vector remain valid for the entire process lifetime. The OS reclaims the memory at exit.
1745+
static ControllerState* state = new ControllerState();
1746+
return *state;
1747+
}
17351748

17361749

17371750
DbgRef<DebuggerController> DebuggerController::GetController(BinaryViewRef data)
17381751
{
1739-
for (size_t i = 0; i < g_controllerCount; i++)
1752+
auto& state = GetControllerState();
1753+
std::lock_guard<std::mutex> lock(state.mutex);
1754+
for (auto& c : state.controllers)
17401755
{
1741-
DebuggerController* controller = g_debuggerControllers[i];
1742-
if (!controller)
1743-
continue;
1744-
if (controller->m_file == data->GetFile())
1745-
return controller;
1756+
if (c && c->m_file == data->GetFile())
1757+
return c;
17461758
}
17471759

17481760
auto controller = new DebuggerController(data);
1749-
g_debuggerControllers = (DbgRef<DebuggerController>*)realloc(g_debuggerControllers,
1750-
sizeof(DbgRef<DebuggerController>) * (g_controllerCount + 1));
1751-
1752-
// We must call the DbgRef ctor on the newly allocated space explicitly. If we do a
1753-
// g_debuggerControllers[g_controllerCount] = controller;
1754-
// The `=` operator on the next line will cause a call to `DbgRef<T>& operator=(T* obj)` on an uninitialized DbgRef
1755-
// object, leading to a crash when `DbgRef::m_obj` is de-referenced.
1756-
// In fact, this is how std::vector does things inside `push_back`.
1757-
new (&g_debuggerControllers[g_controllerCount]) DbgRef<DebuggerController>(controller);
1758-
g_controllerCount++;
1761+
state.controllers.emplace_back(controller);
17591762
return controller;
17601763
}
17611764

17621765

17631766
void DebuggerController::DeleteController(BinaryViewRef data)
17641767
{
1765-
for (size_t i = 0; i < g_controllerCount; i++)
1768+
auto& state = GetControllerState();
1769+
std::lock_guard<std::mutex> lock(state.mutex);
1770+
for (auto& c : state.controllers)
17661771
{
1767-
DbgRef<DebuggerController> controller = g_debuggerControllers[i];
1768-
if (!controller)
1769-
continue;
1770-
1771-
if (controller->GetFile() == data->GetFile())
1772-
{
1773-
g_debuggerControllers[i] = nullptr;
1774-
}
1772+
if (c && c->GetFile() == data->GetFile())
1773+
c = nullptr;
17751774
}
17761775
}
17771776

17781777

17791778
bool DebuggerController::ControllerExists(BinaryViewRef data)
17801779
{
1781-
for (size_t i = 0; i < g_controllerCount; i++)
1780+
auto& state = GetControllerState();
1781+
std::lock_guard<std::mutex> lock(state.mutex);
1782+
for (auto& c : state.controllers)
17821783
{
1783-
DbgRef<DebuggerController> controller = g_debuggerControllers[i];
1784-
if (!controller)
1785-
continue;
1786-
if (controller->GetFile() == data->GetFile())
1784+
if (c && c->GetFile() == data->GetFile())
17871785
return true;
17881786
}
17891787

@@ -1793,13 +1791,12 @@ bool DebuggerController::ControllerExists(BinaryViewRef data)
17931791

17941792
DbgRef<DebuggerController> DebuggerController::GetController(FileMetadataRef file)
17951793
{
1796-
for (size_t i = 0; i < g_controllerCount; i++)
1794+
auto& state = GetControllerState();
1795+
std::lock_guard<std::mutex> lock(state.mutex);
1796+
for (auto& c : state.controllers)
17971797
{
1798-
DebuggerController* controller = g_debuggerControllers[i];
1799-
if (!controller)
1800-
continue;
1801-
if (controller->GetFile() == file)
1802-
return controller;
1798+
if (c && c->GetFile() == file)
1799+
return c;
18031800
}
18041801

18051802
// You cannot create a controller from a file -- you must use a binary view for it
@@ -1809,12 +1806,11 @@ DbgRef<DebuggerController> DebuggerController::GetController(FileMetadataRef fil
18091806

18101807
bool DebuggerController::ControllerExists(FileMetadataRef file)
18111808
{
1812-
for (size_t i = 0; i < g_controllerCount; i++)
1809+
auto& state = GetControllerState();
1810+
std::lock_guard<std::mutex> lock(state.mutex);
1811+
for (auto& c : state.controllers)
18131812
{
1814-
DbgRef<DebuggerController> controller = g_debuggerControllers[i];
1815-
if (!controller)
1816-
continue;
1817-
if (controller->GetFile() == file)
1813+
if (c && c->GetFile() == file)
18181814
return true;
18191815
}
18201816

@@ -1824,16 +1820,12 @@ bool DebuggerController::ControllerExists(FileMetadataRef file)
18241820

18251821
void DebuggerController::DeleteController(FileMetadataRef file)
18261822
{
1827-
for (size_t i = 0; i < g_controllerCount; i++)
1823+
auto& state = GetControllerState();
1824+
std::lock_guard<std::mutex> lock(state.mutex);
1825+
for (auto& c : state.controllers)
18281826
{
1829-
DbgRef<DebuggerController> controller = g_debuggerControllers[i];
1830-
if (!controller)
1831-
continue;
1832-
1833-
if (controller->GetFile() == file)
1834-
{
1835-
g_debuggerControllers[i] = nullptr;
1836-
}
1827+
if (c && c->GetFile() == file)
1828+
c = nullptr;
18371829
}
18381830
}
18391831

core/debuggercontroller.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,12 @@ namespace BinaryNinjaDebugger {
8686
// the binary view -- we will no longer need to track it ourselves
8787
uint64_t m_viewStart;
8888

89-
// inline static std::vector<DbgRef<DebuggerController>> g_debuggerControllers;
90-
static DbgRef<DebuggerController>* g_debuggerControllers;
91-
static size_t g_controllerCount;
89+
struct ControllerState
90+
{
91+
std::mutex mutex;
92+
std::vector<DbgRef<DebuggerController>> controllers;
93+
};
94+
static ControllerState& GetControllerState();
9295

9396
std::atomic<size_t> m_callbackIndex = 0;
9497
std::list<DebuggerEventCallback> m_eventCallbacks;

0 commit comments

Comments
 (0)