From da5c4a6790dd19f58999c47da8296d08056fe9f0 Mon Sep 17 00:00:00 2001 From: raminfp Date: Fri, 20 Feb 2026 19:11:42 +0330 Subject: [PATCH 1/8] gh-145040: Fix crash in sqlite3 when connection is closed during aggregate callback Fix a segmentation fault in the _sqlite module that occurs when Connection.close() is called inside an aggregate step() callback. After stmt_step() returns, _pysqlite_query_execute() calls sqlite3_last_insert_rowid(self->connection->db) without checking if self->connection->db is still valid. If the connection was closed during the callback, self->connection->db is NULL, causing a NULL pointer dereference. The fix adds a NULL check for self->connection->db after stmt_step() returns, raising ProgrammingError instead of crashing. --- Lib/test/test_sqlite3/test_userfunctions.py | 23 +++++++++++++++++++ ...00.gh-issue-145040.sqlite3-close-crash.rst | 6 +++++ Modules/_sqlite/cursor.c | 7 +++++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 11cf877a011c78..662ef42472f8ca 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -723,6 +723,29 @@ def test_agg_keyword_args(self): 'takes exactly 3 positional arguments'): self.con.create_aggregate("test", 1, aggregate_class=AggrText) + def test_aggr_close_conn_in_step(self): + """Connection.close() in an aggregate step callback must not crash.""" + con = sqlite.connect(":memory:", autocommit=True) + cur = con.cursor() + cur.execute("CREATE TABLE t(x INTEGER)") + for i in range(50): + cur.execute("INSERT INTO t VALUES (?)", (i,)) + + class CloseConnAgg: + def __init__(self): + self.total = 0 + + def step(self, value): + self.total += value + con.close() + + def finalize(self): + return self.total + + con.create_aggregate("agg_close", 1, CloseConnAgg) + with self.assertRaises(sqlite.ProgrammingError): + con.execute("SELECT agg_close(x) FROM t") + class AuthorizerTests(unittest.TestCase): @staticmethod diff --git a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst new file mode 100644 index 00000000000000..9f4510e8a16796 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst @@ -0,0 +1,6 @@ +Fixed a crash in the :mod:`sqlite3` module when +:meth:`~sqlite3.Connection.close` is called on the connection during an +aggregate callback (e.g., in the ``step`` method). The interpreter now raises +:exc:`~sqlite3.ProgrammingError` instead of crashing with a segmentation +fault. + diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 5a61e43617984d..115aef6fb0874a 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -906,6 +906,11 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation } rc = stmt_step(self->statement->st); + if (self->connection->db == NULL) { + PyErr_SetString(state->ProgrammingError, + "Cannot operate on a closed database."); + goto error; + } if (rc != SQLITE_DONE && rc != SQLITE_ROW) { if (PyErr_Occurred()) { /* there was an error that occurred in a user-defined callback */ @@ -967,7 +972,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation Py_XDECREF(parameters); } - if (!multiple) { + if (!multiple && self->connection->db) { sqlite_int64 lastrowid; Py_BEGIN_ALLOW_THREADS From 1ecb567b3b91e432a7c5c6699e422054dcde90b3 Mon Sep 17 00:00:00 2001 From: Ramin Farajpour Cami Date: Fri, 20 Feb 2026 20:54:18 +0330 Subject: [PATCH 2/8] Update Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst Co-authored-by: Benedikt Johannes --- .../2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst index 9f4510e8a16796..affd20f89bdb33 100644 --- a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst +++ b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst @@ -3,4 +3,3 @@ Fixed a crash in the :mod:`sqlite3` module when aggregate callback (e.g., in the ``step`` method). The interpreter now raises :exc:`~sqlite3.ProgrammingError` instead of crashing with a segmentation fault. - From 05086f0c108b1d0be1b08ff7ddf66241cba75b44 Mon Sep 17 00:00:00 2001 From: raminfp Date: Sat, 21 Feb 2026 03:42:05 +0330 Subject: [PATCH 3/8] gh-145040: Fix crash in sqlite3 when connection is closed from within a callback --- Lib/test/test_sqlite3/test_userfunctions.py | 98 ++++++++++++++++++- ...00.gh-issue-145040.sqlite3-close-crash.rst | 11 ++- Modules/_sqlite/cursor.c | 13 ++- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 662ef42472f8ca..3d47edf6ab98f1 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -587,6 +587,33 @@ def value(self): return 1 << 65 self.assertRaisesRegex(sqlite.DataError, "string or blob too big", self.cur.execute, self.query % "err_val_ret") + def test_close_conn_in_window_func_value(self): + """gh-145040: closing connection in window function value() callback.""" + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x INTEGER)") + con.executemany("INSERT INTO t VALUES(?)", + [(i,) for i in range(20)]) + + class CloseConnWindow: + def step(self, value): + pass + def finalize(self): + return 0 + def value(self): + con.close() + return 0 + def inverse(self, value): + pass + + con.create_window_function("evil_win", 1, CloseConnWindow) + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + cursor = con.execute( + "SELECT evil_win(x) OVER " + "(ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t" + ) + list(cursor) + class AggregateTests(unittest.TestCase): def setUp(self): @@ -743,9 +770,78 @@ def finalize(self): return self.total con.create_aggregate("agg_close", 1, CloseConnAgg) - with self.assertRaises(sqlite.ProgrammingError): + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): con.execute("SELECT agg_close(x) FROM t") + def test_close_conn_in_udf_during_executemany(self): + """gh-145040: closing connection in UDF during executemany.""" + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x)") + + def close_conn(x): + con.close() + return x + + con.create_function("close_conn", 1, close_conn) + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + con.executemany("INSERT INTO t VALUES(close_conn(?))", + [(i,) for i in range(10)]) + + def test_close_conn_in_progress_handler_during_iternext(self): + """gh-145040: closing connection in progress handler during iteration.""" + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x)") + con.executemany("INSERT INTO t VALUES(?)", + [(i,) for i in range(100)]) + + count = 0 + def close_progress(): + nonlocal count + count += 1 + if count >= 5: + con.close() + return 1 + return 0 + + cursor = con.execute("SELECT * FROM t") + con.set_progress_handler(close_progress, 1) + msg = "from within a callback" + import test.support + with test.support.catch_unraisable_exception(): + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + for row in cursor: + pass + del cursor + gc_collect() + + def test_close_conn_in_collation_callback(self): + """gh-145040: closing connection in collation callback.""" + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x TEXT)") + con.executemany("INSERT INTO t VALUES(?)", + [(f"item_{i}",) for i in range(50)]) + + count = 0 + def evil_collation(a, b): + nonlocal count + count += 1 + if count == 10: + con.close() + if a < b: + return -1 + elif a > b: + return 1 + return 0 + + con.create_collation("evil_coll", evil_collation) + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + con.execute( + "SELECT * FROM t ORDER BY x COLLATE evil_coll" + ) + class AuthorizerTests(unittest.TestCase): @staticmethod diff --git a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst index affd20f89bdb33..291672919c6b79 100644 --- a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst +++ b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst @@ -1,5 +1,6 @@ -Fixed a crash in the :mod:`sqlite3` module when -:meth:`~sqlite3.Connection.close` is called on the connection during an -aggregate callback (e.g., in the ``step`` method). The interpreter now raises -:exc:`~sqlite3.ProgrammingError` instead of crashing with a segmentation -fault. +Fixed a crash in the :mod:`sqlite3` module caused by closing the database +connection from within a callback function invoked during +:func:`sqlite3_step` (e.g., an aggregate ``step``, a user-defined function +via :meth:`~sqlite3.Connection.create_function`, a progress handler, or a +collation callback). Raise :exc:`~sqlite3.ProgrammingError` instead of +crashing. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 115aef6fb0874a..282a111a868ff0 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -908,7 +908,8 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation rc = stmt_step(self->statement->st); if (self->connection->db == NULL) { PyErr_SetString(state->ProgrammingError, - "Cannot operate on a closed database."); + "Cannot close the database connection " + "from within a callback function."); goto error; } if (rc != SQLITE_DONE && rc != SQLITE_ROW) { @@ -972,7 +973,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation Py_XDECREF(parameters); } - if (!multiple && self->connection->db) { + if (!multiple) { sqlite_int64 lastrowid; Py_BEGIN_ALLOW_THREADS @@ -1161,6 +1162,14 @@ pysqlite_cursor_iternext(PyObject *op) return NULL; } int rc = stmt_step(stmt); + if (self->connection->db == NULL) { + Py_DECREF(row); + Py_CLEAR(self->statement); + PyErr_SetString(self->connection->state->ProgrammingError, + "Cannot close the database connection " + "from within a callback function."); + return NULL; + } if (rc == SQLITE_DONE) { if (self->statement->is_dml) { self->rowcount = (long)sqlite3_changes(self->connection->db); From c7dbc7e54e1102a9b28ac48b982dd5ccf5affeb4 Mon Sep 17 00:00:00 2001 From: raminfp Date: Sat, 21 Feb 2026 03:54:05 +0330 Subject: [PATCH 4/8] Fix NEWS: use literal markup for sqlite3_step() --- .../2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst index 291672919c6b79..08225bf941d813 100644 --- a/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst +++ b/Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst @@ -1,6 +1,6 @@ Fixed a crash in the :mod:`sqlite3` module caused by closing the database connection from within a callback function invoked during -:func:`sqlite3_step` (e.g., an aggregate ``step``, a user-defined function +``sqlite3_step()`` (e.g., an aggregate ``step``, a user-defined function via :meth:`~sqlite3.Connection.create_function`, a progress handler, or a collation callback). Raise :exc:`~sqlite3.ProgrammingError` instead of crashing. From eb69460eed1ad0aeaa5ae100601064d79ba671ec Mon Sep 17 00:00:00 2001 From: raminfp Date: Fri, 27 Feb 2026 08:54:55 +0330 Subject: [PATCH 5/8] gh-145040: Prevent closing sqlite3 connection from within a callback Instead of detecting a closed connection after the damage has been done, prevent Connection.close() from succeeding while a SQLite callback is executing. This aligns with the SQLite C API docs, which state that applications must not close the database connection from within a callback. Add an in_callback counter to the connection object, incremented before stmt_step() and decremented after. If close() is called while the counter is positive, ProgrammingError is raised and the database connection remains open. A counter (rather than a boolean flag) is used to correctly handle nested callbacks. Also convert test docstrings to comments per reviewer feedback, and add a test for the nested callback scenario. --- Lib/test/test_sqlite3/test_userfunctions.py | 33 +++++++++++++++++---- Modules/_sqlite/connection.c | 12 ++++++++ Modules/_sqlite/connection.h | 9 ++++++ Modules/_sqlite/cursor.c | 12 ++++++-- 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index 3d47edf6ab98f1..c72aee4435aeef 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -588,7 +588,7 @@ def value(self): return 1 << 65 self.cur.execute, self.query % "err_val_ret") def test_close_conn_in_window_func_value(self): - """gh-145040: closing connection in window function value() callback.""" + # gh-145040: closing connection in window function value() callback. con = sqlite.connect(":memory:", autocommit=True) con.execute("CREATE TABLE t(x INTEGER)") con.executemany("INSERT INTO t VALUES(?)", @@ -751,7 +751,7 @@ def test_agg_keyword_args(self): self.con.create_aggregate("test", 1, aggregate_class=AggrText) def test_aggr_close_conn_in_step(self): - """Connection.close() in an aggregate step callback must not crash.""" + # Connection.close() in an aggregate step callback must not crash. con = sqlite.connect(":memory:", autocommit=True) cur = con.cursor() cur.execute("CREATE TABLE t(x INTEGER)") @@ -774,8 +774,31 @@ def finalize(self): with self.assertRaisesRegex(sqlite.ProgrammingError, msg): con.execute("SELECT agg_close(x) FROM t") + def test_close_conn_in_nested_callback(self): + # gh-145040: close() must be prevented even in nested callbacks. + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x INTEGER)") + for i in range(5): + con.execute("INSERT INTO t VALUES(?)", (i,)) + + def outer_func(x): + con.close() + return x + + def inner_func(x): + return x * 10 + + con.create_function("outer_func", 1, outer_func) + con.create_function("inner_func", 1, inner_func) + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + con.execute("SELECT outer_func(inner_func(x)) FROM t") + # Connection must still be usable after the failed close attempt. + self.assertEqual(con.execute("SELECT 1").fetchone(), (1,)) + con.close() + def test_close_conn_in_udf_during_executemany(self): - """gh-145040: closing connection in UDF during executemany.""" + # gh-145040: closing connection in UDF during executemany. con = sqlite.connect(":memory:", autocommit=True) con.execute("CREATE TABLE t(x)") @@ -790,7 +813,7 @@ def close_conn(x): [(i,) for i in range(10)]) def test_close_conn_in_progress_handler_during_iternext(self): - """gh-145040: closing connection in progress handler during iteration.""" + # gh-145040: closing connection in progress handler during iteration. con = sqlite.connect(":memory:", autocommit=True) con.execute("CREATE TABLE t(x)") con.executemany("INSERT INTO t VALUES(?)", @@ -817,7 +840,7 @@ def close_progress(): gc_collect() def test_close_conn_in_collation_callback(self): - """gh-145040: closing connection in collation callback.""" + # gh-145040: closing connection in collation callback. con = sqlite.connect(":memory:", autocommit=True) con.execute("CREATE TABLE t(x TEXT)") con.executemany("INSERT INTO t VALUES(?)", diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index af63271b9fd971..f1297ec6dbe23a 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -302,6 +302,8 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, PyObject *database, self->blobs = blobs; self->row_factory = Py_NewRef(Py_None); self->text_factory = Py_NewRef(&PyUnicode_Type); + self->in_callback = 0; + self->close_attempted_in_callback = 0; self->trace_ctx = NULL; self->progress_ctx = NULL; self->authorizer_ctx = NULL; @@ -655,6 +657,16 @@ pysqlite_connection_close_impl(pysqlite_Connection *self) return NULL; } + if (self->in_callback > 0) { + self->close_attempted_in_callback = 1; + PyTypeObject *tp = Py_TYPE(self); + pysqlite_state *state = pysqlite_get_state_by_type(tp); + PyErr_SetString(state->ProgrammingError, + "Cannot close the database connection " + "from within a callback function."); + return NULL; + } + pysqlite_close_all_blobs(self); Py_CLEAR(self->statement_cache); if (connection_close(self) < 0) { diff --git a/Modules/_sqlite/connection.h b/Modules/_sqlite/connection.h index a2241bd540669c..ddc8e4bf2ee93a 100644 --- a/Modules/_sqlite/connection.h +++ b/Modules/_sqlite/connection.h @@ -65,6 +65,15 @@ typedef struct int initialized; + /* set to 1 while a SQLite callback (UDF, aggregate, progress handler, + * etc.) is executing; used to prevent closing the connection from + * within a callback, which is illegal per the SQLite C API docs */ + int in_callback; + + /* set to 1 when close() is attempted during a callback; checked after + * stmt_step() returns to raise the appropriate ProgrammingError */ + int close_attempted_in_callback; + /* thread identification of the thread the connection was created in */ unsigned long thread_ident; diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 282a111a868ff0..319a40f652e97d 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -905,8 +905,12 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation goto error; } + self->connection->in_callback++; rc = stmt_step(self->statement->st); - if (self->connection->db == NULL) { + self->connection->in_callback--; + if (self->connection->close_attempted_in_callback) { + self->connection->close_attempted_in_callback = 0; + PyErr_Clear(); PyErr_SetString(state->ProgrammingError, "Cannot close the database connection " "from within a callback function."); @@ -1161,10 +1165,14 @@ pysqlite_cursor_iternext(PyObject *op) if (row == NULL) { return NULL; } + self->connection->in_callback++; int rc = stmt_step(stmt); - if (self->connection->db == NULL) { + self->connection->in_callback--; + if (self->connection->close_attempted_in_callback) { + self->connection->close_attempted_in_callback = 0; Py_DECREF(row); Py_CLEAR(self->statement); + PyErr_Clear(); PyErr_SetString(self->connection->state->ProgrammingError, "Cannot close the database connection " "from within a callback function."); From 6f07252ce9b1ebb75b22ccca4ad1f5508dc215c8 Mon Sep 17 00:00:00 2001 From: raminfp Date: Fri, 27 Feb 2026 09:07:58 +0330 Subject: [PATCH 6/8] gh-145040: Fix close_attempted_in_callback flag consumed by nested callbacks Only check and consume the close_attempted_in_callback flag when in_callback reaches zero (the outermost level). Previously, a nested stmt_step() inside a callback could consume the flag, causing the outermost caller to miss the error. --- Lib/test/test_sqlite3/test_userfunctions.py | 27 +++++++++++++++++++++ Modules/_sqlite/cursor.c | 8 ++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index c72aee4435aeef..d31497b6956188 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -797,6 +797,33 @@ def inner_func(x): self.assertEqual(con.execute("SELECT 1").fetchone(), (1,)) con.close() + def test_close_conn_in_nested_callback_caught(self): + # gh-145040: close attempt must propagate even if the exception + # is caught inside the callback and a nested execute consumes + # the flag. + con = sqlite.connect(":memory:", autocommit=True) + con.execute("CREATE TABLE t(x INTEGER)") + con.execute("INSERT INTO t VALUES(1)") + + def swallow_close(x): + try: + con.close() + except sqlite.ProgrammingError: + pass + try: + con.execute("SELECT 1") + except sqlite.ProgrammingError: + pass + return x + + con.create_function("swallow_close", 1, swallow_close) + msg = "from within a callback" + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + con.execute("SELECT swallow_close(x) FROM t") + # Connection must still be usable. + self.assertEqual(con.execute("SELECT 1").fetchone(), (1,)) + con.close() + def test_close_conn_in_udf_during_executemany(self): # gh-145040: closing connection in UDF during executemany. con = sqlite.connect(":memory:", autocommit=True) diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 319a40f652e97d..323c95acbcb291 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -908,7 +908,9 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation self->connection->in_callback++; rc = stmt_step(self->statement->st); self->connection->in_callback--; - if (self->connection->close_attempted_in_callback) { + if (self->connection->close_attempted_in_callback + && self->connection->in_callback == 0) + { self->connection->close_attempted_in_callback = 0; PyErr_Clear(); PyErr_SetString(state->ProgrammingError, @@ -1168,7 +1170,9 @@ pysqlite_cursor_iternext(PyObject *op) self->connection->in_callback++; int rc = stmt_step(stmt); self->connection->in_callback--; - if (self->connection->close_attempted_in_callback) { + if (self->connection->close_attempted_in_callback + && self->connection->in_callback == 0) + { self->connection->close_attempted_in_callback = 0; Py_DECREF(row); Py_CLEAR(self->statement); From 36ea50481cccbc7df26ca4f94beab464fe1fa841 Mon Sep 17 00:00:00 2001 From: raminfp Date: Sat, 28 Feb 2026 06:02:50 +0330 Subject: [PATCH 7/8] gh-145040: Use cursor->locked to prevent closing sqlite3 connection in callback Replace the in_callback counter with cursor->locked checks. Restore the cursor weakref list (partially rolling back 74c1f41) so that close() can iterate cursors and detect locked ones. --- Modules/_sqlite/connection.c | 48 ++++++++++++++++------ Modules/_sqlite/connection.h | 13 +++--- Modules/_sqlite/cursor.c | 77 ++++++++++++++++++++++++++++++------ 3 files changed, 106 insertions(+), 32 deletions(-) diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c index f1297ec6dbe23a..95d30b452750b6 100644 --- a/Modules/_sqlite/connection.c +++ b/Modules/_sqlite/connection.c @@ -38,6 +38,7 @@ #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() #include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing() #include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL +#include "pycore_weakref.h" #include @@ -283,10 +284,17 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, PyObject *database, goto error; } - /* Create lists of weak references to blobs */ + /* Create lists of weak references to cursors and blobs */ + PyObject *cursors = PyList_New(0); + if (cursors == NULL) { + Py_DECREF(statement_cache); + goto error; + } + PyObject *blobs = PyList_New(0); if (blobs == NULL) { Py_DECREF(statement_cache); + Py_DECREF(cursors); goto error; } @@ -299,11 +307,11 @@ pysqlite_connection_init_impl(pysqlite_Connection *self, PyObject *database, self->check_same_thread = check_same_thread; self->thread_ident = PyThread_get_thread_ident(); self->statement_cache = statement_cache; + self->cursors = cursors; self->blobs = blobs; + self->close_attempted_in_callback = 0; self->row_factory = Py_NewRef(Py_None); self->text_factory = Py_NewRef(&PyUnicode_Type); - self->in_callback = 0; - self->close_attempted_in_callback = 0; self->trace_ctx = NULL; self->progress_ctx = NULL; self->authorizer_ctx = NULL; @@ -383,6 +391,7 @@ connection_traverse(PyObject *op, visitproc visit, void *arg) pysqlite_Connection *self = _pysqlite_Connection_CAST(op); Py_VISIT(Py_TYPE(self)); Py_VISIT(self->statement_cache); + Py_VISIT(self->cursors); Py_VISIT(self->blobs); Py_VISIT(self->row_factory); Py_VISIT(self->text_factory); @@ -407,6 +416,7 @@ connection_clear(PyObject *op) { pysqlite_Connection *self = _pysqlite_Connection_CAST(op); Py_CLEAR(self->statement_cache); + Py_CLEAR(self->cursors); Py_CLEAR(self->blobs); Py_CLEAR(self->row_factory); Py_CLEAR(self->text_factory); @@ -657,14 +667,30 @@ pysqlite_connection_close_impl(pysqlite_Connection *self) return NULL; } - if (self->in_callback > 0) { - self->close_attempted_in_callback = 1; - PyTypeObject *tp = Py_TYPE(self); - pysqlite_state *state = pysqlite_get_state_by_type(tp); - PyErr_SetString(state->ProgrammingError, - "Cannot close the database connection " - "from within a callback function."); - return NULL; + /* Check if any cursor is locked (actively executing a query); + * closing during a callback is illegal per the SQLite C API docs. */ + assert(PyList_CheckExact(self->cursors)); + Py_ssize_t n = PyList_GET_SIZE(self->cursors); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *weakref = PyList_GET_ITEM(self->cursors, i); + if (_PyWeakref_IsDead(weakref)) { + continue; + } + PyObject *obj; + if (!PyWeakref_GetRef(weakref, &obj)) { + continue; + } + int locked = ((pysqlite_Cursor *)obj)->locked; + Py_DECREF(obj); + if (locked) { + self->close_attempted_in_callback = 1; + PyTypeObject *tp = Py_TYPE(self); + pysqlite_state *state = pysqlite_get_state_by_type(tp); + PyErr_SetString(state->ProgrammingError, + "Cannot close the database connection " + "from within a callback function."); + return NULL; + } } pysqlite_close_all_blobs(self); diff --git a/Modules/_sqlite/connection.h b/Modules/_sqlite/connection.h index ddc8e4bf2ee93a..acbc8ccf9d0f4e 100644 --- a/Modules/_sqlite/connection.h +++ b/Modules/_sqlite/connection.h @@ -65,13 +65,9 @@ typedef struct int initialized; - /* set to 1 while a SQLite callback (UDF, aggregate, progress handler, - * etc.) is executing; used to prevent closing the connection from - * within a callback, which is illegal per the SQLite C API docs */ - int in_callback; - - /* set to 1 when close() is attempted during a callback; checked after - * stmt_step() returns to raise the appropriate ProgrammingError */ + /* set to 1 when close() is attempted while a cursor is locked (actively + * executing); checked after stmt_step() returns to raise the appropriate + * ProgrammingError */ int close_attempted_in_callback; /* thread identification of the thread the connection was created in */ @@ -79,7 +75,8 @@ typedef struct PyObject *statement_cache; - /* Lists of weak references to blobs used within this connection */ + /* Lists of weak references to cursors and blobs used within this connection */ + PyObject *cursors; PyObject *blobs; PyObject* row_factory; diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 323c95acbcb291..d21974987fc055 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -58,6 +58,31 @@ check_cursor_locked(pysqlite_Cursor *cur) return 1; } +/* + * Check if any cursor other than 'exclude' is locked on this connection. + * Used to determine if we're in a nested callback scenario. + */ +static int +any_other_cursor_locked(pysqlite_Connection *conn, pysqlite_Cursor *exclude) +{ + assert(PyList_CheckExact(conn->cursors)); + Py_ssize_t n = PyList_GET_SIZE(conn->cursors); + for (Py_ssize_t i = 0; i < n; i++) { + PyObject *weakref = PyList_GET_ITEM(conn->cursors, i); + PyObject *obj; + if (!PyWeakref_GetRef(weakref, &obj)) { + continue; + } + pysqlite_Cursor *cursor = (pysqlite_Cursor *)obj; + int locked = cursor->locked; + Py_DECREF(obj); + if (locked && cursor != exclude) { + return 1; + } + } + return 0; +} + static pysqlite_state * get_module_state_by_cursor(pysqlite_Cursor *cursor) { @@ -99,6 +124,28 @@ class _sqlite3.Cursor "pysqlite_Cursor *" "clinic_state()->CursorType" [clinic start generated code]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=3c5b8115c5cf30f1]*/ +/* + * Registers a cursor with the connection. + * + * 0 => error; 1 => ok + */ +static int +register_cursor(pysqlite_Connection *connection, PyObject *cursor) +{ + PyObject *weakref = PyWeakref_NewRef(cursor, NULL); + if (weakref == NULL) { + return 0; + } + + if (PyList_Append(connection->cursors, weakref) < 0) { + Py_DECREF(weakref); + return 0; + } + + Py_DECREF(weakref); + return 1; +} + /*[clinic input] _sqlite3.Cursor.__init__ as pysqlite_cursor_init @@ -138,6 +185,10 @@ pysqlite_cursor_init_impl(pysqlite_Cursor *self, return -1; } + if (!register_cursor(connection, (PyObject *)self)) { + return -1; + } + self->initialized = 1; return 0; @@ -905,13 +956,12 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation goto error; } - self->connection->in_callback++; rc = stmt_step(self->statement->st); - self->connection->in_callback--; - if (self->connection->close_attempted_in_callback - && self->connection->in_callback == 0) - { - self->connection->close_attempted_in_callback = 0; + if (self->connection->close_attempted_in_callback) { + /* Only clear the flag if no other cursor is locked (outermost) */ + if (!any_other_cursor_locked(self->connection, self)) { + self->connection->close_attempted_in_callback = 0; + } PyErr_Clear(); PyErr_SetString(state->ProgrammingError, "Cannot close the database connection " @@ -1163,18 +1213,19 @@ pysqlite_cursor_iternext(PyObject *op) self->locked = 1; // GH-80254: Prevent recursive use of cursors. PyObject *row = _pysqlite_fetch_one_row(self); - self->locked = 0; if (row == NULL) { + self->locked = 0; return NULL; } - self->connection->in_callback++; int rc = stmt_step(stmt); - self->connection->in_callback--; - if (self->connection->close_attempted_in_callback - && self->connection->in_callback == 0) - { - self->connection->close_attempted_in_callback = 0; + self->locked = 0; + if (self->connection->close_attempted_in_callback) { + /* Only clear the flag if no other cursor is locked (outermost) */ + if (!any_other_cursor_locked(self->connection, self)) { + self->connection->close_attempted_in_callback = 0; + } Py_DECREF(row); + stmt_reset(self->statement); Py_CLEAR(self->statement); PyErr_Clear(); PyErr_SetString(self->connection->state->ProgrammingError, From c7707beedd4570e12c9d753091030f96f42bca44 Mon Sep 17 00:00:00 2001 From: raminfp Date: Sat, 28 Feb 2026 06:17:34 +0330 Subject: [PATCH 8/8] gh-145040: Close connections in tests to avoid ResourceWarning --- Lib/test/test_sqlite3/test_userfunctions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py index d31497b6956188..ce013446375874 100644 --- a/Lib/test/test_sqlite3/test_userfunctions.py +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -613,6 +613,7 @@ def inverse(self, value): "(ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM t" ) list(cursor) + con.close() class AggregateTests(unittest.TestCase): @@ -773,6 +774,7 @@ def finalize(self): msg = "from within a callback" with self.assertRaisesRegex(sqlite.ProgrammingError, msg): con.execute("SELECT agg_close(x) FROM t") + con.close() def test_close_conn_in_nested_callback(self): # gh-145040: close() must be prevented even in nested callbacks. @@ -838,6 +840,7 @@ def close_conn(x): with self.assertRaisesRegex(sqlite.ProgrammingError, msg): con.executemany("INSERT INTO t VALUES(close_conn(?))", [(i,) for i in range(10)]) + con.close() def test_close_conn_in_progress_handler_during_iternext(self): # gh-145040: closing connection in progress handler during iteration. @@ -865,6 +868,7 @@ def close_progress(): pass del cursor gc_collect() + con.close() def test_close_conn_in_collation_callback(self): # gh-145040: closing connection in collation callback. @@ -891,6 +895,7 @@ def evil_collation(a, b): con.execute( "SELECT * FROM t ORDER BY x COLLATE evil_coll" ) + con.close() class AuthorizerTests(unittest.TestCase):