Skip to content

Commit 05086f0

Browse files
committed
gh-145040: Fix crash in sqlite3 when connection is closed from within a callback
1 parent 1ecb567 commit 05086f0

File tree

3 files changed

+114
-8
lines changed

3 files changed

+114
-8
lines changed

Lib/test/test_sqlite3/test_userfunctions.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,33 @@ def value(self): return 1 << 65
587587
self.assertRaisesRegex(sqlite.DataError, "string or blob too big",
588588
self.cur.execute, self.query % "err_val_ret")
589589

590+
def test_close_conn_in_window_func_value(self):
591+
"""gh-145040: closing connection in window function value() callback."""
592+
con = sqlite.connect(":memory:", autocommit=True)
593+
con.execute("CREATE TABLE t(x INTEGER)")
594+
con.executemany("INSERT INTO t VALUES(?)",
595+
[(i,) for i in range(20)])
596+
597+
class CloseConnWindow:
598+
def step(self, value):
599+
pass
600+
def finalize(self):
601+
return 0
602+
def value(self):
603+
con.close()
604+
return 0
605+
def inverse(self, value):
606+
pass
607+
608+
con.create_window_function("evil_win", 1, CloseConnWindow)
609+
msg = "from within a callback"
610+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
611+
cursor = con.execute(
612+
"SELECT evil_win(x) OVER "
613+
"(ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t"
614+
)
615+
list(cursor)
616+
590617

591618
class AggregateTests(unittest.TestCase):
592619
def setUp(self):
@@ -743,9 +770,78 @@ def finalize(self):
743770
return self.total
744771

745772
con.create_aggregate("agg_close", 1, CloseConnAgg)
746-
with self.assertRaises(sqlite.ProgrammingError):
773+
msg = "from within a callback"
774+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
747775
con.execute("SELECT agg_close(x) FROM t")
748776

777+
def test_close_conn_in_udf_during_executemany(self):
778+
"""gh-145040: closing connection in UDF during executemany."""
779+
con = sqlite.connect(":memory:", autocommit=True)
780+
con.execute("CREATE TABLE t(x)")
781+
782+
def close_conn(x):
783+
con.close()
784+
return x
785+
786+
con.create_function("close_conn", 1, close_conn)
787+
msg = "from within a callback"
788+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
789+
con.executemany("INSERT INTO t VALUES(close_conn(?))",
790+
[(i,) for i in range(10)])
791+
792+
def test_close_conn_in_progress_handler_during_iternext(self):
793+
"""gh-145040: closing connection in progress handler during iteration."""
794+
con = sqlite.connect(":memory:", autocommit=True)
795+
con.execute("CREATE TABLE t(x)")
796+
con.executemany("INSERT INTO t VALUES(?)",
797+
[(i,) for i in range(100)])
798+
799+
count = 0
800+
def close_progress():
801+
nonlocal count
802+
count += 1
803+
if count >= 5:
804+
con.close()
805+
return 1
806+
return 0
807+
808+
cursor = con.execute("SELECT * FROM t")
809+
con.set_progress_handler(close_progress, 1)
810+
msg = "from within a callback"
811+
import test.support
812+
with test.support.catch_unraisable_exception():
813+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
814+
for row in cursor:
815+
pass
816+
del cursor
817+
gc_collect()
818+
819+
def test_close_conn_in_collation_callback(self):
820+
"""gh-145040: closing connection in collation callback."""
821+
con = sqlite.connect(":memory:", autocommit=True)
822+
con.execute("CREATE TABLE t(x TEXT)")
823+
con.executemany("INSERT INTO t VALUES(?)",
824+
[(f"item_{i}",) for i in range(50)])
825+
826+
count = 0
827+
def evil_collation(a, b):
828+
nonlocal count
829+
count += 1
830+
if count == 10:
831+
con.close()
832+
if a < b:
833+
return -1
834+
elif a > b:
835+
return 1
836+
return 0
837+
838+
con.create_collation("evil_coll", evil_collation)
839+
msg = "from within a callback"
840+
with self.assertRaisesRegex(sqlite.ProgrammingError, msg):
841+
con.execute(
842+
"SELECT * FROM t ORDER BY x COLLATE evil_coll"
843+
)
844+
749845

750846
class AuthorizerTests(unittest.TestCase):
751847
@staticmethod
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
Fixed a crash in the :mod:`sqlite3` module when
2-
:meth:`~sqlite3.Connection.close` is called on the connection during an
3-
aggregate callback (e.g., in the ``step`` method). The interpreter now raises
4-
:exc:`~sqlite3.ProgrammingError` instead of crashing with a segmentation
5-
fault.
1+
Fixed a crash in the :mod:`sqlite3` module caused by closing the database
2+
connection from within a callback function invoked during
3+
:func:`sqlite3_step` (e.g., an aggregate ``step``, a user-defined function
4+
via :meth:`~sqlite3.Connection.create_function`, a progress handler, or a
5+
collation callback). Raise :exc:`~sqlite3.ProgrammingError` instead of
6+
crashing.

Modules/_sqlite/cursor.c

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,8 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
908908
rc = stmt_step(self->statement->st);
909909
if (self->connection->db == NULL) {
910910
PyErr_SetString(state->ProgrammingError,
911-
"Cannot operate on a closed database.");
911+
"Cannot close the database connection "
912+
"from within a callback function.");
912913
goto error;
913914
}
914915
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
@@ -972,7 +973,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation
972973
Py_XDECREF(parameters);
973974
}
974975

975-
if (!multiple && self->connection->db) {
976+
if (!multiple) {
976977
sqlite_int64 lastrowid;
977978

978979
Py_BEGIN_ALLOW_THREADS
@@ -1161,6 +1162,14 @@ pysqlite_cursor_iternext(PyObject *op)
11611162
return NULL;
11621163
}
11631164
int rc = stmt_step(stmt);
1165+
if (self->connection->db == NULL) {
1166+
Py_DECREF(row);
1167+
Py_CLEAR(self->statement);
1168+
PyErr_SetString(self->connection->state->ProgrammingError,
1169+
"Cannot close the database connection "
1170+
"from within a callback function.");
1171+
return NULL;
1172+
}
11641173
if (rc == SQLITE_DONE) {
11651174
if (self->statement->is_dml) {
11661175
self->rowcount = (long)sqlite3_changes(self->connection->db);

0 commit comments

Comments
 (0)