From 447b5fd64c3c718f912e70b6eddd205b408fd79e Mon Sep 17 00:00:00 2001 From: "F.D.Castel" Date: Wed, 15 Apr 2026 23:11:14 -0300 Subject: [PATCH] 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. --- src/firebird/driver/core.py | 8 +++++++- tests/conftest.py | 2 +- tests/test_issues.py | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/firebird/driver/core.py b/src/firebird/driver/core.py index a5154ee..0615987 100644 --- a/src/firebird/driver/core.py +++ b/src/firebird/driver/core.py @@ -3965,7 +3965,13 @@ def _execute(self, operation: str | Statement, in_meta.release() def _clear(self) -> None: if self._result is not None: - self._result.close() + if self._stmt is not None and self._stmt._istmt is not None: + self._result.close() + else: + # Statement was already freed; the result set is invalidated + # at the Firebird API level, so we must not call close() on it. + # Also prevent __del__ from calling release() on the invalid interface. + self._result._refcnt = 0 self._result = None self._name = None self._last_fetch_status = None diff --git a/tests/conftest.py b/tests/conftest.py index 45776d9..14d72e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -122,7 +122,7 @@ def pytest_configure(config): client_lib = Path(client_lib) if not client_lib.is_file(): pytest.exit(f"Client library '{client_lib}' not found!") - driver_config.fb_client_library.value = client_lib + driver_config.fb_client_library.value = str(client_lib) # if host := config.getoption('host'): _vars_['host'] = host diff --git a/tests/test_issues.py b/tests/test_issues.py index 8f27d58..cdfc3f2 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -39,3 +39,29 @@ def test_issue_53(db_connection): numeric_val_exponent = numeric_val.as_tuple()[2] db_connection.commit() assert numeric_val_exponent == -2 + +def test_issue_65_prepare_ctx_mgr(db_connection): + """Freeing a Statement via context manager must not crash when cursor/connection closes.""" + with db_connection.cursor() as cur: + with cur.prepare('select count(*) from country where 1 < ?') as stmt: + row = cur.execute(stmt, (2,)).fetchone() + assert row is not None + +def test_issue_65_free_then_cursor_close(db_connection): + """Explicit stmt.free() followed by cursor.close() must not crash.""" + cur = db_connection.cursor() + stmt = cur.prepare('select count(*) from country where 1 < ?') + row = cur.execute(stmt, (2,)).fetchone() + assert row is not None + stmt.free() + cur.close() + +def test_issue_65_free_then_conn_close(dsn): + """stmt.free() followed by connection close must not crash.""" + from firebird.driver import connect + with connect(dsn) as conn: + cur = conn.cursor() + stmt = cur.prepare('select count(*) from country where 1 < ?') + row = cur.execute(stmt, (2,)).fetchone() + assert row is not None + stmt.free()