From 037b42198288ac11f420a5d4210cdbe103e4c6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 11:19:29 +0100 Subject: [PATCH 1/4] gh-143198: fix SIGSEGV in `sqlite3.execute[many]` with re-entrant parameter iterator --- Lib/test/test_sqlite3/test_dbapi.py | 59 ++++++++++++++++++- ...-12-27-10-36-18.gh-issue-143198.DdIHyC.rst | 4 ++ Modules/_sqlite/cursor.c | 6 ++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index 20e39f61e4dedb..c5d21468dae50c 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -22,6 +22,7 @@ import contextlib import functools +import operator import os import sqlite3 as sqlite import subprocess @@ -32,7 +33,7 @@ import warnings from test.support import ( - SHORT_TIMEOUT, check_disallow_instantiation, requires_subprocess + SHORT_TIMEOUT, check_disallow_instantiation, requires_subprocess, subTests ) from test.support import gc_collect from test.support import threading_helper, import_helper @@ -728,6 +729,27 @@ def test_database_keyword(self): self.assertEqual(type(cx), sqlite.Connection) +class CxWrapper: + def __init__(self, cx): + self.cx = cx + + +class EvilParamsWithIter(CxWrapper): + + def __iter__(self): + self.cx.close() + return iter([(1,)]) + + +class EvilParamsWithNext(CxWrapper): + def __iter__(self): + return self + + def __next__(self): + self.cx.close() + return (1,) + + class CursorTests(unittest.TestCase): def setUp(self): self.cx = sqlite.connect(":memory:") @@ -742,6 +764,16 @@ def tearDown(self): self.cu.close() self.cx.close() + def do_test_connection_use_after_close(self, method_name, params_class): + # Prevent SIGSEGV with iterable of parameters closing the connection. + # Regression test for https://github.com/python/cpython/issues/143198. + cx = sqlite.connect(":memory:") + self.addCleanup(cx.close) + cu = cx.cursor() + params = params_class(cx) + method = operator.methodcaller(method_name, "SELECT ?", params) + self.assertRaises(sqlite.ProgrammingError, method, cu) + def test_execute_no_args(self): self.cu.execute("delete from test") @@ -813,6 +845,10 @@ def test_execute_non_iterable(self): self.cu.execute("insert into test(id) values (?)", 42) self.assertEqual(str(cm.exception), 'parameters are of unsupported type') + @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + def test_execute_use_after_close(self, params_class): + self.do_test_connection_use_after_close("execute", params_class) + def test_execute_wrong_no_of_args1(self): # too many parameters with self.assertRaises(sqlite.ProgrammingError): @@ -1030,6 +1066,10 @@ def test_execute_many_not_iterable(self): with self.assertRaises(TypeError): self.cu.executemany("insert into test(income) values (?)", 42) + @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + def test_executemany_use_after_close(self, params_class): + self.do_test_connection_use_after_close("executemany", params_class) + def test_fetch_iter(self): # Optional DB-API extension. self.cu.execute("delete from test") @@ -1645,6 +1685,15 @@ def tearDown(self): self.cur.close() self.con.close() + def do_test_connection_use_after_close(self, method_name, params_class): + # Prevent SIGSEGV with iterable of parameters closing the connection. + # Regression test for https://github.com/python/cpython/issues/143198. + cx = sqlite.connect(":memory:") + self.addCleanup(cx.close) + params = params_class(cx) + method = operator.methodcaller(method_name, "SELECT ?", params) + self.assertRaises(sqlite.ProgrammingError, method, cx) + def test_script_string_sql(self): cur = self.cur cur.executescript(""" @@ -1711,6 +1760,10 @@ def test_connection_execute(self): result = self.con.execute("select 5").fetchone()[0] self.assertEqual(result, 5, "Basic test of Connection.execute") + @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + def test_connection_execute_use_after_close(self, params_class): + self.do_test_connection_use_after_close("execute", params_class) + def test_connection_executemany(self): con = self.con con.execute("create table test(foo)") @@ -1719,6 +1772,10 @@ def test_connection_executemany(self): self.assertEqual(result[0][0], 3, "Basic test of Connection.executemany") self.assertEqual(result[1][0], 4, "Basic test of Connection.executemany") + @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + def test_connection_executemany_use_after_close(self, params_class): + self.do_test_connection_use_after_close("executemany", params_class) + def test_connection_executescript(self): con = self.con con.executescript(""" diff --git a/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst new file mode 100644 index 00000000000000..2c8f241eb35603 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst @@ -0,0 +1,4 @@ +:mod:`sqlite3`: fix crashes in :func:`~sqlite3.execute`, :func:`~sqlite3.executemany`, +:meth:`Cursor.execute `, and :meth:`Cursor.executemany +` when iterating over the query's parameters closes +the current sqlite3 connection. Patch by Bénédikt Tran. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 4611c9e5e3e437..2e7c6b32cd68a7 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -870,6 +870,12 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation } } + // PyObject_GetIter() may have a side-effect on the connection's state. + // See: https://github.com/python/cpython/issues/143198. + if (!pysqlite_check_connection(self->connection)) { + goto error; + } + /* reset description */ Py_INCREF(Py_None); Py_SETREF(self->description, Py_None); From cc0f483a7b28cb790619f75141f2cb09c0d2e115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 11:28:32 +0100 Subject: [PATCH 2/4] Update Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst --- .../2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst index 2c8f241eb35603..cc2f2d66885ab0 100644 --- a/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst +++ b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst @@ -1,4 +1,5 @@ -:mod:`sqlite3`: fix crashes in :func:`~sqlite3.execute`, :func:`~sqlite3.executemany`, -:meth:`Cursor.execute `, and :meth:`Cursor.executemany -` when iterating over the query's parameters closes -the current sqlite3 connection. Patch by Bénédikt Tran. +:mod:`sqlite3`: fix crashes in :meth:`Connection.execute ` +and :meth:`Connection.executemany ` when iterating over +the query's parameters closes the current connection. A similar issue has been fixed +for the corresponding methods on :class:`~sqlite3.Cursor` objects. +Patch by Bénédikt Tran. From b0c8b16cfd230080bafdad4fc5fa2414998a9379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 17:19:03 +0100 Subject: [PATCH 3/4] correctly fix `sqlite3`'s `executemany` --- Lib/test/test_sqlite3/test_dbapi.py | 68 +++++++++---------- ...-12-27-10-36-18.gh-issue-143198.DdIHyC.rst | 8 +-- Modules/_sqlite/cursor.c | 5 ++ 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index c5d21468dae50c..d52e103fed5cad 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -733,21 +733,27 @@ class CxWrapper: def __init__(self, cx): self.cx = cx + def side_effect(self): + self.cx.close() -class EvilParamsWithIter(CxWrapper): +class ParamsCxCloseInIterMany(CxWrapper): def __iter__(self): - self.cx.close() - return iter([(1,)]) + self.side_effect() + return iter([(1,), (2,), (3,)]) -class EvilParamsWithNext(CxWrapper): +class ParamsCxCloseInNext(CxWrapper): + def __init__(self, cx): + super().__init__(cx) + self.r = iter(range(10)) + def __iter__(self): return self def __next__(self): - self.cx.close() - return (1,) + self.side_effect() + return (next(self.r),) class CursorTests(unittest.TestCase): @@ -764,16 +770,6 @@ def tearDown(self): self.cu.close() self.cx.close() - def do_test_connection_use_after_close(self, method_name, params_class): - # Prevent SIGSEGV with iterable of parameters closing the connection. - # Regression test for https://github.com/python/cpython/issues/143198. - cx = sqlite.connect(":memory:") - self.addCleanup(cx.close) - cu = cx.cursor() - params = params_class(cx) - method = operator.methodcaller(method_name, "SELECT ?", params) - self.assertRaises(sqlite.ProgrammingError, method, cu) - def test_execute_no_args(self): self.cu.execute("delete from test") @@ -845,10 +841,6 @@ def test_execute_non_iterable(self): self.cu.execute("insert into test(id) values (?)", 42) self.assertEqual(str(cm.exception), 'parameters are of unsupported type') - @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) - def test_execute_use_after_close(self, params_class): - self.do_test_connection_use_after_close("execute", params_class) - def test_execute_wrong_no_of_args1(self): # too many parameters with self.assertRaises(sqlite.ProgrammingError): @@ -1066,9 +1058,17 @@ def test_execute_many_not_iterable(self): with self.assertRaises(TypeError): self.cu.executemany("insert into test(income) values (?)", 42) - @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + @subTests("params_class", (ParamsCxCloseInIterMany, ParamsCxCloseInNext)) def test_executemany_use_after_close(self, params_class): - self.do_test_connection_use_after_close("executemany", params_class) + # Prevent SIGSEGV with iterable of parameters closing the connection. + # Regression test for https://github.com/python/cpython/issues/143198. + cx = sqlite.connect(":memory:") + cx.execute("create table tmp(a number)") + self.addCleanup(cx.close) + cu = cx.cursor() + msg = r"Cannot operate on a closed database\." + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + cu.executemany("insert into tmp(a) values (?)", params_class(cx)) def test_fetch_iter(self): # Optional DB-API extension. @@ -1685,15 +1685,6 @@ def tearDown(self): self.cur.close() self.con.close() - def do_test_connection_use_after_close(self, method_name, params_class): - # Prevent SIGSEGV with iterable of parameters closing the connection. - # Regression test for https://github.com/python/cpython/issues/143198. - cx = sqlite.connect(":memory:") - self.addCleanup(cx.close) - params = params_class(cx) - method = operator.methodcaller(method_name, "SELECT ?", params) - self.assertRaises(sqlite.ProgrammingError, method, cx) - def test_script_string_sql(self): cur = self.cur cur.executescript(""" @@ -1760,10 +1751,6 @@ def test_connection_execute(self): result = self.con.execute("select 5").fetchone()[0] self.assertEqual(result, 5, "Basic test of Connection.execute") - @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) - def test_connection_execute_use_after_close(self, params_class): - self.do_test_connection_use_after_close("execute", params_class) - def test_connection_executemany(self): con = self.con con.execute("create table test(foo)") @@ -1772,9 +1759,16 @@ def test_connection_executemany(self): self.assertEqual(result[0][0], 3, "Basic test of Connection.executemany") self.assertEqual(result[1][0], 4, "Basic test of Connection.executemany") - @subTests("params_class", (EvilParamsWithIter, EvilParamsWithNext)) + @subTests("params_class", (ParamsCxCloseInIterMany, ParamsCxCloseInNext)) def test_connection_executemany_use_after_close(self, params_class): - self.do_test_connection_use_after_close("executemany", params_class) + # Prevent SIGSEGV with iterable of parameters closing the connection. + # Regression test for https://github.com/python/cpython/issues/143198. + cx = sqlite.connect(":memory:") + cx.execute("create table tmp(a number)") + self.addCleanup(cx.close) + msg = r"Cannot operate on a closed database\." + with self.assertRaisesRegex(sqlite.ProgrammingError, msg): + cx.executemany("insert into tmp(a) values (?)", params_class(cx)) def test_connection_executescript(self): con = self.con diff --git a/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst index cc2f2d66885ab0..dd433dbd22104c 100644 --- a/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst +++ b/Misc/NEWS.d/next/Library/2025-12-27-10-36-18.gh-issue-143198.DdIHyC.rst @@ -1,5 +1,3 @@ -:mod:`sqlite3`: fix crashes in :meth:`Connection.execute ` -and :meth:`Connection.executemany ` when iterating over -the query's parameters closes the current connection. A similar issue has been fixed -for the corresponding methods on :class:`~sqlite3.Cursor` objects. -Patch by Bénédikt Tran. +:mod:`sqlite3`: fix crashes in :meth:`Connection.executemany ` +and :meth:`Cursor.executemany ` when iterating over +the query's parameters closes the current connection. Patch by Bénédikt Tran. diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c index 2e7c6b32cd68a7..91968d2a3d2569 100644 --- a/Modules/_sqlite/cursor.c +++ b/Modules/_sqlite/cursor.c @@ -931,6 +931,11 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation if (!parameters) { break; } + // PyIter_Next() may have a side-effect on the connection's state. + // See: https://github.com/python/cpython/issues/143198. + if (!pysqlite_check_connection(self->connection)) { + goto error; + } bind_parameters(state, self->statement, parameters); if (PyErr_Occurred()) { From ae2a5defb98ec75d2e4fbd3724fcc2ad469e2773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 27 Dec 2025 17:39:52 +0100 Subject: [PATCH 4/4] fix lint --- Lib/test/test_sqlite3/test_dbapi.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_sqlite3/test_dbapi.py b/Lib/test/test_sqlite3/test_dbapi.py index d52e103fed5cad..323a776c1e2cdf 100644 --- a/Lib/test/test_sqlite3/test_dbapi.py +++ b/Lib/test/test_sqlite3/test_dbapi.py @@ -22,7 +22,6 @@ import contextlib import functools -import operator import os import sqlite3 as sqlite import subprocess