Skip to content

Commit 447b5fd

Browse files
committed
Fix segfault when closing cursor/connection after Statement.free(). Fix #65.
When a Statement was freed (via context manager or explicit free()) before its Cursor was closed, Cursor._clear() would attempt to close an already- invalidated IResultSet, causing a segfault or DatabaseError. Now checks whether the Statement's interface is still valid before closing the result set. If the statement was already freed, the result set reference is safely discarded instead.
1 parent ea14774 commit 447b5fd

3 files changed

Lines changed: 34 additions & 2 deletions

File tree

src/firebird/driver/core.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3965,7 +3965,13 @@ def _execute(self, operation: str | Statement,
39653965
in_meta.release()
39663966
def _clear(self) -> None:
39673967
if self._result is not None:
3968-
self._result.close()
3968+
if self._stmt is not None and self._stmt._istmt is not None:
3969+
self._result.close()
3970+
else:
3971+
# Statement was already freed; the result set is invalidated
3972+
# at the Firebird API level, so we must not call close() on it.
3973+
# Also prevent __del__ from calling release() on the invalid interface.
3974+
self._result._refcnt = 0
39693975
self._result = None
39703976
self._name = None
39713977
self._last_fetch_status = None

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def pytest_configure(config):
122122
client_lib = Path(client_lib)
123123
if not client_lib.is_file():
124124
pytest.exit(f"Client library '{client_lib}' not found!")
125-
driver_config.fb_client_library.value = client_lib
125+
driver_config.fb_client_library.value = str(client_lib)
126126
#
127127
if host := config.getoption('host'):
128128
_vars_['host'] = host

tests/test_issues.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,29 @@ def test_issue_53(db_connection):
3939
numeric_val_exponent = numeric_val.as_tuple()[2]
4040
db_connection.commit()
4141
assert numeric_val_exponent == -2
42+
43+
def test_issue_65_prepare_ctx_mgr(db_connection):
44+
"""Freeing a Statement via context manager must not crash when cursor/connection closes."""
45+
with db_connection.cursor() as cur:
46+
with cur.prepare('select count(*) from country where 1 < ?') as stmt:
47+
row = cur.execute(stmt, (2,)).fetchone()
48+
assert row is not None
49+
50+
def test_issue_65_free_then_cursor_close(db_connection):
51+
"""Explicit stmt.free() followed by cursor.close() must not crash."""
52+
cur = db_connection.cursor()
53+
stmt = cur.prepare('select count(*) from country where 1 < ?')
54+
row = cur.execute(stmt, (2,)).fetchone()
55+
assert row is not None
56+
stmt.free()
57+
cur.close()
58+
59+
def test_issue_65_free_then_conn_close(dsn):
60+
"""stmt.free() followed by connection close must not crash."""
61+
from firebird.driver import connect
62+
with connect(dsn) as conn:
63+
cur = conn.cursor()
64+
stmt = cur.prepare('select count(*) from country where 1 < ?')
65+
row = cur.execute(stmt, (2,)).fetchone()
66+
assert row is not None
67+
stmt.free()

0 commit comments

Comments
 (0)