Skip to content

Commit 4ecc9c6

Browse files
TimelordUKclaude
andcommitted
fix: add mutex to ConnectionHandles to prevent race condition crash (#378)
Add internal mutex protection to ConnectionHandles class to prevent double-free crash when ReleaseWorker (statement cleanup) and CloseWorker (connection close) race to free the same statement handle. - Add _handlesMutex to protect statement handle map operations - Protect clear(), checkin(), checkout(), size(), exists() methods - Add enhanced logging with thread IDs for debugging - Add stress test to verify fix (examples/stress-test-race-condition.js) Fixes #378 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent e895c92 commit 4ecc9c6

File tree

5 files changed

+446
-25
lines changed

5 files changed

+446
-25
lines changed

cpp/include/odbc/connection_handles.h

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <functional>
66
#include <map>
77
#include <memory>
8+
#include <mutex>
89
#include <set>
910
#include <vector>
1011

@@ -15,10 +16,11 @@
1516
namespace mssql {
1617
/**
1718
* @brief Manages ODBC statement handles for a connection
18-
*
19-
* This class is NOT thread-safe by itself. Thread safety is provided
20-
* by the containing OdbcConnection class through external synchronization.
21-
*
19+
*
20+
* This class is thread-safe. All public methods are protected by an internal
21+
* mutex to prevent race conditions between concurrent operations like
22+
* statement release (checkin) and connection close (clear).
23+
*
2224
* The class provides a checkout/checkin pattern for statement handles
2325
* where each statement is identified by a unique ID.
2426
*/
@@ -59,9 +61,13 @@ class ConnectionHandles {
5961
private:
6062
shared_ptr<IOdbcStatementHandle> store(const long statement_id,
6163
shared_ptr<IOdbcStatementHandle> handle);
62-
shared_ptr<IOdbcStatementHandle> find(const long statement_id);
64+
shared_ptr<IOdbcStatementHandle> find_unlocked(const long statement_id);
6365
std::map<long, std::shared_ptr<SafeHandle<IOdbcStatementHandle>>> _statementHandles;
6466
std::shared_ptr<IOdbcEnvironmentHandle> rawEnvHandle_; // Raw handle - not wrapped in SafeHandle
6567
std::shared_ptr<SafeHandle<IOdbcConnectionHandle>> connectionHandle_;
68+
69+
// Mutex for thread-safe access to statement handles
70+
// This prevents race conditions between checkin() and clear()
71+
mutable std::mutex _handlesMutex;
6672
};
6773
} // namespace mssql

cpp/src/odbc/connection_handles.cpp

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,15 @@ ConnectionHandles::~ConnectionHandles() {
4545
}
4646

4747
void ConnectionHandles::clear() {
48-
SQL_LOG_DEBUG_STREAM("ConnectionHandles::clear - size = " << _statementHandles.size());
48+
std::lock_guard<std::mutex> lock(_handlesMutex);
49+
50+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::clear - ENTER - thread="
51+
<< std::this_thread::get_id() << " size=" << _statementHandles.size());
4952

5053
// Direct iteration - no need for intermediate vector
5154
for (const auto& [id, safeHandle] : _statementHandles) {
52-
SQL_LOG_DEBUG_STREAM("destruct OdbcStatementCache - free statement " << id);
55+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::clear - processing statement " << id
56+
<< " thread=" << std::this_thread::get_id());
5357
if (safeHandle) {
5458
auto handle = safeHandle->get();
5559
if (handle) {
@@ -73,40 +77,55 @@ void ConnectionHandles::clear() {
7377
// Also try to explicitly close the cursor/statement
7478
SQLCloseCursor(handle->get_handle());
7579
SQLFreeStmt(handle->get_handle(), SQL_CLOSE);
80+
} else {
81+
SQL_LOG_WARNING_STREAM("ConnectionHandles::clear - statement " << id
82+
<< " handle already invalid/freed");
7683
}
7784
// During connection cleanup, we need to force-free handles
7885
// Reset references first to avoid warning about freeing referenced handles
86+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::clear - freeing statement " << id
87+
<< " thread=" << std::this_thread::get_id());
7988
safeHandle->resetReferences();
8089
safeHandle->free();
90+
} else {
91+
SQL_LOG_WARNING_STREAM("ConnectionHandles::clear - statement " << id
92+
<< " safeHandle is null");
8193
}
8294
}
8395

8496
_statementHandles.clear();
97+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::clear - EXIT - thread=" << std::this_thread::get_id());
8598
}
8699

87-
shared_ptr<IOdbcStatementHandle> ConnectionHandles::find(const long statement_id) {
100+
shared_ptr<IOdbcStatementHandle> ConnectionHandles::find_unlocked(const long statement_id) {
101+
// NOTE: This is an internal method - caller must hold _handlesMutex
88102
const auto itr = _statementHandles.find(statement_id);
89103
if (itr != _statementHandles.end()) {
90104
auto ref = itr->second->get();
91105
if (ref) {
92106
return ref;
93107
}
94-
SQL_LOG_WARNING_STREAM("Found invalid statement handle for id: " << statement_id);
108+
SQL_LOG_WARNING_STREAM("find_unlocked - statement " << statement_id
109+
<< " found but handle is invalid");
95110
}
96111
return nullptr;
97112
}
98113

99114
size_t ConnectionHandles::size() const {
115+
std::lock_guard<std::mutex> lock(_handlesMutex);
100116
return _statementHandles.size();
101117
}
102118

103119
bool ConnectionHandles::exists(long statement_id) const {
120+
std::lock_guard<std::mutex> lock(_handlesMutex);
104121
return _statementHandles.find(statement_id) != _statementHandles.end();
105122
}
106123

107124
shared_ptr<IOdbcStatementHandle> ConnectionHandles::store(const long statement_id,
108125
shared_ptr<IOdbcStatementHandle> handle) {
109-
SQL_LOG_DEBUG_STREAM("ConnectionHandles::store - statementId = " << statement_id);
126+
// NOTE: This is an internal method - caller must hold _handlesMutex
127+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::store - statementId=" << statement_id
128+
<< " thread=" << std::this_thread::get_id());
110129

111130
auto safeHandle = std::make_shared<SafeHandle<IOdbcStatementHandle>>(
112131
"Statement_" + std::to_string(statement_id), handle);
@@ -115,23 +134,33 @@ shared_ptr<IOdbcStatementHandle> ConnectionHandles::store(const long statement_i
115134
const auto [it, inserted] = _statementHandles.try_emplace(statement_id, safeHandle);
116135

117136
if (!inserted) {
118-
SQL_LOG_ERROR_STREAM(
119-
"ConnectionHandles::store - statementId already exists = " << statement_id);
137+
SQL_LOG_ERROR_STREAM("ConnectionHandles::store - statementId already exists = " << statement_id
138+
<< " thread=" << std::this_thread::get_id());
120139
return nullptr;
121140
}
122141

142+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::store - statementId=" << statement_id
143+
<< " stored successfully, total handles=" << _statementHandles.size());
123144
return handle;
124145
}
125146

126147
shared_ptr<IOdbcStatementHandle> ConnectionHandles::checkout(long statement_id) {
127148
if (statement_id < 0) {
128-
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - invalid statementId = " << statement_id);
149+
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - invalid statementId=" << statement_id
150+
<< " thread=" << std::this_thread::get_id());
129151
return nullptr;
130152
}
131153

132-
auto statement = find(statement_id);
154+
std::lock_guard<std::mutex> lock(_handlesMutex);
155+
156+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkout - ENTER statementId=" << statement_id
157+
<< " thread=" << std::this_thread::get_id()
158+
<< " current_handles=" << _statementHandles.size());
159+
160+
auto statement = find_unlocked(statement_id);
133161
if (statement) {
134-
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkout ok on statementId = " << statement_id);
162+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkout - found existing statementId=" << statement_id
163+
<< " thread=" << std::this_thread::get_id());
135164
return statement;
136165
}
137166

@@ -143,38 +172,54 @@ shared_ptr<IOdbcStatementHandle> ConnectionHandles::checkout(long statement_id)
143172
// Get connection handle for allocation
144173
auto connRef = connectionHandle_->get();
145174
if (!connRef) {
146-
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - connection handle invalid");
175+
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - connection handle invalid, statementId="
176+
<< statement_id << " thread=" << std::this_thread::get_id());
147177
return nullptr;
148178
}
149179

150180
if (!safeHandle->alloc(connRef->get_handle())) {
151-
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - failed to allocate statement handle");
181+
SQL_LOG_ERROR_STREAM("ConnectionHandles::checkout - failed to allocate statement handle for statementId="
182+
<< statement_id << " thread=" << std::this_thread::get_id());
152183
return nullptr;
153184
}
154185

155186
// Store the SafeHandle wrapper
156187
_statementHandles[statement_id] = safeHandle;
157188

158-
SQL_LOG_DEBUG_STREAM(
159-
"ConnectionHandles::checkout - created new handle for statementId = " << statement_id);
189+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkout - created new handle for statementId="
190+
<< statement_id << " thread=" << std::this_thread::get_id()
191+
<< " total_handles=" << _statementHandles.size());
160192

161193
// Return the handle itself, not the SafeHandle
162194
return safeHandle->get();
163195
}
164196

165197
void ConnectionHandles::checkin(long statementId) {
166-
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkin - statementId = " << statementId);
198+
std::lock_guard<std::mutex> lock(_handlesMutex);
199+
200+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkin - ENTER statementId=" << statementId
201+
<< " thread=" << std::this_thread::get_id()
202+
<< " current_handles=" << _statementHandles.size());
167203

168204
const auto itr = _statementHandles.find(statementId);
169205
if (itr == _statementHandles.end()) {
170-
SQL_LOG_ERROR_STREAM(
171-
"ConnectionHandles::checkin - no handle found for statementId = " << statementId);
206+
// This can happen legitimately if clear() already freed this statement
207+
// during connection close - not necessarily an error
208+
SQL_LOG_WARNING_STREAM("ConnectionHandles::checkin - statementId=" << statementId
209+
<< " not found (may have been freed by clear()) thread="
210+
<< std::this_thread::get_id());
172211
return;
173212
}
174213

175214
// Free the handle through SafeHandle
215+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkin - freeing statementId=" << statementId
216+
<< " thread=" << std::this_thread::get_id());
176217
itr->second->free();
177218
_statementHandles.erase(statementId);
219+
220+
SQL_LOG_DEBUG_STREAM("ConnectionHandles::checkin - EXIT statementId=" << statementId
221+
<< " thread=" << std::this_thread::get_id()
222+
<< " remaining_handles=" << _statementHandles.size());
178223
}
179224

180225
// Return the interface pointer

0 commit comments

Comments
 (0)