-
Notifications
You must be signed in to change notification settings - Fork 35
FIX: Segmentation Fault in libmsodbcsql-18.5 during SQLFreeHandle() #415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Fixes a use-after-free / double-free crash seen with msodbcsql 18.5 when a connection (DBC) is freed while statement (STMT) handles are still alive, by explicitly tracking child statement handles and preventing later cleanup from calling into ODBC on already-freed handles.
Changes:
- Track statement handles created by a
Connectionvia aweak_ptrlist and mark them “implicitly freed” duringdisconnect(). - Add an
_implicitly_freedflag +markImplicitlyFreed()toSqlHandle, and skipSQLFreeHandlewhen set. - Add a new regression test suite for connection invalidation / GC cleanup scenarios and expose the new method via pybind11.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_016_connection_invalidation_segfault.py | Adds regression tests targeting the historical segfault scenario during GC cleanup after connection invalidation. |
| mssql_python/pybind/ddbc_bindings.h | Extends SqlHandle API/state to support “implicitly freed” tracking. |
| mssql_python/pybind/ddbc_bindings.cpp | Implements the implicit-free guard in SqlHandle::free() and exports the new method to Python. |
| mssql_python/pybind/connection/connection.h | Adds per-connection tracking for child statement handles. |
| mssql_python/pybind/connection/connection.cpp | Marks tracked STMT handles as implicitly freed on disconnect() and records new STMT handles on allocation. |
Comments suppressed due to low confidence (1)
mssql_python/pybind/connection/connection.cpp:114
checkError(ret)can throw onSQLDisconnect_ptr, which means_dbcHandle.reset()will never run. At that point the child statement handles were already marked implicitly freed and the tracking vector cleared, leaving the connection handle alive while STMT handles are now flagged to skip freeing (resource leak / inconsistent state). Restructuredisconnect()so handle-marking +_dbcHandle.reset()are exception-safe (e.g., always reset in a scope guard/finally; only mark children when you're definitely freeing the DBC handle).
SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get());
checkError(ret);
// triggers SQLFreeHandle via destructor, if last owner
_dbcHandle.reset();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Track child statement handles to mark them as implicitly freed when connection closes | ||
| // Uses weak_ptr to avoid circular references and allow normal cleanup | ||
| std::vector<std::weak_ptr<SqlHandle>> _childStatementHandles; |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
connection.h now uses std::vector<std::weak_ptr<SqlHandle>> but doesn’t include <vector>. Please add the direct include to avoid relying on transitive includes from ddbc_bindings.h (which can break compilation if headers change).
| .def("free", &SqlHandle::free, "Free the handle") | ||
| .def("markImplicitlyFreed", &SqlHandle::markImplicitlyFreed, | ||
| "Mark handle as implicitly freed by parent handle"); |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new Python-exposed method name markImplicitlyFreed is camelCase, but other ddbc_bindings.Connection methods are exported in snake_case (e.g., set_autocommit, alloc_statement_handle). Consider exporting this as mark_implicitly_freed (optionally keeping markImplicitlyFreed as a temporary alias) to keep the binding API consistent.
| conn.close() | ||
|
|
||
| # Force garbage collection to trigger cursor cleanup | ||
| # This is where the segfault would occur without the fix | ||
| cursors = None |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests call conn.close(), but Connection.close() actively closes all tracked cursors and frees their statement handles before closing the DBC handle. That means this likely doesn’t exercise the implicit-free path the fix targets (DBC freed while STMT handles are still alive). Consider adding a regression test that keeps a SqlHandle STMT alive across a low-level ddbc_bindings.Connection.close() (e.g., via alloc_statement_handle) and then triggers GC/free on the still-referenced STMT handle.
|
|
||
| # Force garbage collection to trigger cursor cleanup | ||
| # This is where the segfault would occur without the fix | ||
| cursors = None |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable cursors is not used.
| cursors = None | |
| del cursors |
| connections = None | ||
| all_cursors = None |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable connections is not used.
| connections = None | |
| all_cursors = None | |
| del connections | |
| del all_cursors |
|
|
||
| # Clear references and force GC | ||
| connections = None | ||
| all_cursors = None |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable all_cursors is not used.
Summary
This pull request implements a critical fix for a long-standing use-after-free (segmentation fault) bug that occurred when a database connection was closed while statement handles were still alive. The fix ensures that child statement handles are properly tracked and marked as "implicitly freed" when the parent connection is closed, preventing double-free and use-after-free errors. Additionally, comprehensive regression tests are added to verify the fix under various scenarios.
Critical bug fix for handle management:
The
Connectionclass now tracks all child statement handles in a_childStatementHandlesvector usingweak_ptrto avoid circular references and memory leaks. When the connection is closed, all child statement handles are marked as "implicitly freed" before the parent handle is released. This prevents theSqlHandledestructor from attempting to free handles that have already been freed by the ODBC driver. [1] [2] [3]The
SqlHandleclass now includes an_implicitly_freedflag and amarkImplicitlyFreed()method. Thefree()method checks this flag and skips ODBC cleanup if the handle was already implicitly freed, preventing use-after-free errors. [1] [2] [3]The Python bindings are updated to expose the new
markImplicitlyFreedmethod onSqlHandle, ensuring that the state can be managed from both C++ and Python layers.Testing and verification:
test_016_connection_invalidation_segfault.py, is added with extensive tests that reproduce the original segfault scenarios, including multiple cursors, uncommitted transactions, prepared statements, and concurrent connection invalidation. These tests confirm that the fix prevents crashes and ensures clean resource management.Other minor changes:
This change is critical for stability and correctness when using connection pooling, SQLAlchemy, or any code that may close connections before all cursors are explicitly closed.