Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion Lib/test/test_sqlite3/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,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
Expand Down Expand Up @@ -728,6 +728,33 @@ def test_database_keyword(self):
self.assertEqual(type(cx), sqlite.Connection)


class CxWrapper:
def __init__(self, cx):
self.cx = cx

def side_effect(self):
self.cx.close()


class ParamsCxCloseInIterMany(CxWrapper):
def __iter__(self):
self.side_effect()
return iter([(1,), (2,), (3,)])


class ParamsCxCloseInNext(CxWrapper):
def __init__(self, cx):
super().__init__(cx)
self.r = iter(range(10))

def __iter__(self):
return self

def __next__(self):
self.side_effect()
return (next(self.r),)


class CursorTests(unittest.TestCase):
def setUp(self):
self.cx = sqlite.connect(":memory:")
Expand Down Expand Up @@ -1030,6 +1057,18 @@ def test_execute_many_not_iterable(self):
with self.assertRaises(TypeError):
self.cu.executemany("insert into test(income) values (?)", 42)

@subTests("params_class", (ParamsCxCloseInIterMany, ParamsCxCloseInNext))
def test_executemany_use_after_close(self, 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.
self.cu.execute("delete from test")
Expand Down Expand Up @@ -1719,6 +1758,17 @@ 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", (ParamsCxCloseInIterMany, ParamsCxCloseInNext))
def test_connection_executemany_use_after_close(self, 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
con.executescript("""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`sqlite3`: fix crashes in :meth:`Connection.executemany <sqlite3.Connection.executemany>`
and :meth:`Cursor.executemany <sqlite3.Cursor.executemany>` when iterating over
the query's parameters closes the current connection. Patch by Bénédikt Tran.
11 changes: 11 additions & 0 deletions Modules/_sqlite/cursor.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -925,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.
Copy link
Member Author

@picnixz picnixz Dec 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: if parameters is NULL and we have an active exception, we break the loop. It should only happen when we are having a custom iterator, that is, multiple = 1. As such, we wouldn't branch into the if (!multiple) and we wouldn't call PyLong_FromLongLong.

However, I'll add tomorrow some assertions because it's not entirely clear from the code alone.

// See: https://github.com/python/cpython/issues/143198.
if (!pysqlite_check_connection(self->connection)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!pysqlite_check_connection(self->connection)) {
if (multiple && !pysqlite_check_connection(self->connection)) {

Strictly speaking, this check is redundant when dealing with mutliple = 0 because parameters_iter is a true list_iterator that is safely constructed and parameters may be an evil one. With multiple = 1, we need to check this at every iteration as parameters_iter may come from user-defined code.

goto error;
}

bind_parameters(state, self->statement, parameters);
if (PyErr_Occurred()) {
Expand Down
Loading