https://github.com/python/cpython/commit/656af533dcb938e1069b83a61cccb5d3266e4694
commit: 656af533dcb938e1069b83a61cccb5d3266e4694
branch: main
author: Jelle Zijlstra <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-10-24T13:46:15-07:00
summary:
gh-159739: Keep sqlite3 cursor metadata and factories valid during queries
(#160403)
A cursor exposed its description tuple before filling in all the columns, so an
allocation failure or a reader on a free-threaded build could encounter an
unset tuple entry. An allocation failure could also clear lastrowid and crash
the next execute. Build new metadata before replacing the old values and
synchronize reads and writes. Keep a strong reference to each row or text
factory while it is called so replacing the factory during a fetch cannot
destroy an active callable.
files:
A Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.tRq417.rst
M Lib/test/test_sqlite3/test_factory.py
M Modules/_sqlite/connection.c
M Modules/_sqlite/cursor.c
M Modules/_sqlite/row.c
M Modules/_sqlite/util.h
diff --git a/Lib/test/test_sqlite3/test_factory.py
b/Lib/test/test_sqlite3/test_factory.py
index 2dd42921d31ddd5..8b92f3f0a419ee9 100644
--- a/Lib/test/test_sqlite3/test_factory.py
+++ b/Lib/test/test_sqlite3/test_factory.py
@@ -21,6 +21,7 @@
# 3. This notice may not be removed or altered from any source distribution.
import unittest
+import weakref
import sqlite3 as sqlite
from collections.abc import Sequence
@@ -156,6 +157,17 @@ def test_delete_connection_text_factory(self):
with self.assertRaises(AttributeError):
del self.con.text_factory
+ def test_replace_active_row_factory(self):
+ class Factory:
+ def row(self, cursor, values):
+ cursor.row_factory = None
+ return factory_ref() is not None
+
+ cursor = self.con.cursor()
+ cursor.row_factory = Factory().row
+ factory_ref = weakref.ref(cursor.row_factory)
+ self.assertIs(cursor.execute("select 1").fetchone(), True)
+
def test_delete_cursor_row_factory(self):
# gh-149738: deleting row_factory should raise an exception
cur = self.con.cursor()
diff --git
a/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.tRq417.rst
b/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.tRq417.rst
new file mode 100644
index 000000000000000..1e174fbfc25959a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-22-16-30-00.gh-issue-159739.tRq417.rst
@@ -0,0 +1,3 @@
+Fix crashes in :mod:`sqlite3` when a row or text factory is replaced during a
+query, when cursor metadata is read during execution in a free-threaded build,
+or when allocating cursor metadata fails.
diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index ede996b0598ee77..f8bd3a8df35b668 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -305,8 +305,9 @@ pysqlite_connection_init_impl(pysqlite_Connection *self,
PyObject *database,
self->thread_ident = PyThread_get_thread_ident();
self->statement_cache = statement_cache;
self->blobs = blobs;
- self->row_factory = Py_NewRef(Py_None);
- self->text_factory = Py_NewRef(&PyUnicode_Type);
+ pysqlite_set_ref((PyObject *)self, &self->row_factory, Py_NewRef(Py_None));
+ pysqlite_set_ref((PyObject *)self, &self->text_factory,
+ Py_NewRef(&PyUnicode_Type));
self->trace_ctx = NULL;
self->progress_ctx = NULL;
self->authorizer_ctx = NULL;
@@ -411,8 +412,8 @@ connection_clear(PyObject *op)
pysqlite_Connection *self = _pysqlite_Connection_CAST(op);
Py_CLEAR(self->statement_cache);
Py_CLEAR(self->blobs);
- Py_CLEAR(self->row_factory);
- Py_CLEAR(self->text_factory);
+ pysqlite_set_ref(op, &self->row_factory, NULL);
+ pysqlite_set_ref(op, &self->text_factory, NULL);
clear_callback_context(self->trace_ctx);
clear_callback_context(self->progress_ctx);
clear_callback_context(self->authorizer_ctx);
@@ -554,9 +555,14 @@ pysqlite_connection_cursor_impl(pysqlite_Connection *self,
PyObject *factory)
return NULL;
}
- if (cursor && self->row_factory != Py_None) {
- Py_INCREF(self->row_factory);
- Py_XSETREF(((pysqlite_Cursor *)cursor)->row_factory,
self->row_factory);
+ PyObject *row_factory = pysqlite_get_ref((PyObject *)self,
+ &self->row_factory, Py_None);
+ if (row_factory != Py_None) {
+ pysqlite_Cursor *cur = (pysqlite_Cursor *)cursor;
+ pysqlite_set_ref(cursor, &cur->row_factory, row_factory);
+ }
+ else {
+ Py_DECREF(row_factory);
}
return cursor;
@@ -566,7 +572,7 @@ static PyObject *
connection_get_row_factory(PyObject *op, void *closure)
{
pysqlite_Connection *self = (pysqlite_Connection *)op;
- return Py_NewRef(self->row_factory);
+ return pysqlite_get_ref(op, &self->row_factory, Py_None);
}
static int
@@ -578,7 +584,7 @@ connection_set_row_factory(PyObject *op, PyObject *value,
void *closure)
"cannot delete row_factory attribute");
return -1;
}
- Py_XSETREF(self->row_factory, Py_NewRef(value));
+ pysqlite_set_ref(op, &self->row_factory, Py_NewRef(value));
return 0;
}
@@ -586,7 +592,8 @@ static PyObject *
connection_get_text_factory(PyObject *op, void *closure)
{
pysqlite_Connection *self = (pysqlite_Connection *)op;
- return Py_NewRef(self->text_factory);
+ return pysqlite_get_ref(op, &self->text_factory,
+ (PyObject *)&PyUnicode_Type);
}
static int
@@ -598,7 +605,7 @@ connection_set_text_factory(PyObject *op, PyObject *value,
void *closure)
"cannot delete text_factory attribute");
return -1;
}
- Py_XSETREF(self->text_factory, Py_NewRef(value));
+ pysqlite_set_ref(op, &self->text_factory, Py_NewRef(value));
return 0;
}
diff --git a/Modules/_sqlite/cursor.c b/Modules/_sqlite/cursor.c
index f1a3087a91f981d..ad8c434c1b2fb8f 100644
--- a/Modules/_sqlite/cursor.c
+++ b/Modules/_sqlite/cursor.c
@@ -121,18 +121,15 @@ pysqlite_cursor_init_impl(pysqlite_Cursor *self,
Py_CLEAR(self->statement);
Py_CLEAR(self->row_cast_map);
- Py_INCREF(Py_None);
- Py_XSETREF(self->description, Py_None);
+ pysqlite_set_ref((PyObject *)self, &self->description, Py_NewRef(Py_None));
- Py_INCREF(Py_None);
- Py_XSETREF(self->lastrowid, Py_None);
+ pysqlite_set_ref((PyObject *)self, &self->lastrowid, Py_NewRef(Py_None));
self->arraysize = 1;
self->closed = 0;
self->rowcount = -1L;
- Py_INCREF(Py_None);
- Py_XSETREF(self->row_factory, Py_None);
+ pysqlite_set_ref((PyObject *)self, &self->row_factory, Py_NewRef(Py_None));
if (!pysqlite_check_thread(self->connection)) {
return -1;
@@ -176,10 +173,10 @@ cursor_clear(PyObject *op)
{
pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op);
Py_CLEAR(self->connection);
- Py_CLEAR(self->description);
+ pysqlite_set_ref(op, &self->description, NULL);
Py_CLEAR(self->row_cast_map);
- Py_CLEAR(self->lastrowid);
- Py_CLEAR(self->row_factory);
+ pysqlite_set_ref(op, &self->lastrowid, NULL);
+ pysqlite_set_ref(op, &self->row_factory, NULL);
if (self->statement) {
/* Reset the statement if the user has not closed the cursor */
int rc = stmt_reset(self->statement);
@@ -415,12 +412,17 @@ _pysqlite_fetch_one_row(pysqlite_Cursor* self)
}
nbytes = sqlite3_column_bytes(self->statement->st, i);
- if (self->connection->text_factory ==
(PyObject*)&PyUnicode_Type) {
+ PyObject *factory = pysqlite_get_ref(
+ (PyObject *)self->connection,
+ &self->connection->text_factory,
+ (PyObject *)&PyUnicode_Type);
+ if (factory == (PyObject*)&PyUnicode_Type) {
converted = PyUnicode_FromStringAndSize(text, nbytes);
if (!converted &&
PyErr_ExceptionMatches(PyExc_UnicodeDecodeError)) {
PyErr_Clear();
colname = sqlite3_column_name(self->statement->st, i);
if (colname == NULL) {
+ Py_DECREF(factory);
PyErr_NoMemory();
goto error;
}
@@ -436,13 +438,14 @@ _pysqlite_fetch_one_row(pysqlite_Cursor* self)
Py_DECREF(error_msg);
}
}
- } else if (self->connection->text_factory ==
(PyObject*)&PyBytes_Type) {
+ } else if (factory == (PyObject*)&PyBytes_Type) {
converted = PyBytes_FromStringAndSize(text, nbytes);
- } else if (self->connection->text_factory ==
(PyObject*)&PyByteArray_Type) {
+ } else if (factory == (PyObject*)&PyByteArray_Type) {
converted = PyByteArray_FromStringAndSize(text, nbytes);
} else {
- converted =
PyObject_CallFunction(self->connection->text_factory, "y#", text, nbytes);
+ converted = PyObject_CallFunction(factory, "y#", text,
nbytes);
}
+ Py_DECREF(factory);
} else {
/* coltype == SQLITE_BLOB */
const void *blob = sqlite3_column_blob(self->statement->st, i);
@@ -797,6 +800,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int
multiple, PyObject* operation
PyObject* parameters_list = NULL;
PyObject* parameters_iter = NULL;
PyObject* parameters = NULL;
+ PyObject* description = NULL;
int i;
int rc;
int numcols;
@@ -846,8 +850,7 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int
multiple, PyObject* operation
}
/* reset description */
- Py_INCREF(Py_None);
- Py_SETREF(self->description, Py_None);
+ pysqlite_set_ref((PyObject *)self, &self->description, Py_NewRef(Py_None));
if (self->statement) {
// Reset pending statements on this cursor.
@@ -931,8 +934,8 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int
multiple, PyObject* operation
numcols = sqlite3_column_count(self->statement->st);
Py_END_ALLOW_THREADS
if (self->description == Py_None && numcols > 0) {
- Py_SETREF(self->description, PyTuple_New(numcols));
- if (!self->description) {
+ description = PyTuple_New(numcols);
+ if (description == NULL) {
goto error;
}
for (i = 0; i < numcols; i++) {
@@ -953,8 +956,10 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int
multiple, PyObject* operation
if (descriptor == NULL) {
goto error;
}
- PyTuple_SET_ITEM(self->description, i, descriptor);
+ PyTuple_SET_ITEM(description, i, descriptor);
}
+ pysqlite_set_ref((PyObject *)self, &self->description,
description);
+ description = NULL;
}
if (rc == SQLITE_DONE) {
@@ -975,11 +980,15 @@ _pysqlite_query_execute(pysqlite_Cursor* self, int
multiple, PyObject* operation
lastrowid = sqlite3_last_insert_rowid(self->connection->db);
Py_END_ALLOW_THREADS
- Py_SETREF(self->lastrowid, PyLong_FromLongLong(lastrowid));
- // Fall through on error.
+ PyObject *lastrowid_obj = PyLong_FromLongLong(lastrowid);
+ if (lastrowid_obj == NULL) {
+ goto error;
+ }
+ pysqlite_set_ref((PyObject *)self, &self->lastrowid, lastrowid_obj);
}
error:
+ Py_XDECREF(description);
Py_XDECREF(parameters);
Py_XDECREF(parameters_iter);
Py_XDECREF(parameters_list);
@@ -1177,12 +1186,13 @@ pysqlite_cursor_iternext(PyObject *op)
}
return NULL;
}
- if (!Py_IsNone(self->row_factory)) {
- PyObject *factory = self->row_factory;
+ PyObject *factory = pysqlite_get_ref(op, &self->row_factory, Py_None);
+ if (!Py_IsNone(factory)) {
PyObject *args[] = { op, row, };
PyObject *new_row = PyObject_Vectorcall(factory, args, 2, NULL);
Py_SETREF(row, new_row);
}
+ Py_DECREF(factory);
return row;
reset_failure:
@@ -1411,7 +1421,7 @@ static PyObject *
cursor_get_row_factory(PyObject *op, void *Py_UNUSED(closure))
{
pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op);
- return Py_NewRef(self->row_factory);
+ return pysqlite_get_ref(op, &self->row_factory, Py_None);
}
static int
@@ -1423,7 +1433,7 @@ cursor_set_row_factory(PyObject *op, PyObject *value,
void *Py_UNUSED(closure))
"cannot delete row_factory attribute");
return -1;
}
- Py_XSETREF(self->row_factory, Py_NewRef(value));
+ pysqlite_set_ref(op, &self->row_factory, Py_NewRef(value));
return 0;
}
diff --git a/Modules/_sqlite/row.c b/Modules/_sqlite/row.c
index 8646f591ef5169f..7e39db9d6bcd4ee 100644
--- a/Modules/_sqlite/row.c
+++ b/Modules/_sqlite/row.c
@@ -27,6 +27,7 @@
#include "row.h"
#include "cursor.h"
+#include "util.h"
#define clinic_state() (pysqlite_get_state_by_type(type))
#include "clinic/row.c.h"
@@ -93,7 +94,8 @@ pysqlite_row_new_impl(PyTypeObject *type, pysqlite_Cursor
*cursor,
return NULL;
self->data = Py_NewRef(data);
- self->description = Py_NewRef(cursor->description);
+ self->description = pysqlite_get_ref((PyObject *)cursor,
+ &cursor->description, Py_None);
return (PyObject *) self;
}
diff --git a/Modules/_sqlite/util.h b/Modules/_sqlite/util.h
index c00369496f99687..d53aedd66b615fb 100644
--- a/Modules/_sqlite/util.h
+++ b/Modules/_sqlite/util.h
@@ -24,6 +24,7 @@
#ifndef PYSQLITE_UTIL_H
#define PYSQLITE_UTIL_H
#include "Python.h"
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_STORE_PTR_RELEASE()
#include "pythread.h"
#include "sqlite3.h"
#include "connection.h"
@@ -36,4 +37,28 @@ void set_error_from_code(pysqlite_state *state, int code);
sqlite_int64 _pysqlite_long_as_int64(PyObject * value);
+/* Take a strong reference while the owning object's lock is held. */
+static inline PyObject *
+pysqlite_get_ref(PyObject *owner, PyObject *const *field, PyObject *fallback)
+{
+ PyObject *value;
+ Py_BEGIN_CRITICAL_SECTION(owner);
+ value = Py_NewRef(*field != NULL ? *field : fallback);
+ Py_END_CRITICAL_SECTION();
+ return value;
+}
+
+/* Steal value. Use the same store as PyMember_SetOne() since a readonly
+ * member descriptor may load the field without taking the object's lock. */
+static inline void
+pysqlite_set_ref(PyObject *owner, PyObject **field, PyObject *value)
+{
+ PyObject *old;
+ Py_BEGIN_CRITICAL_SECTION(owner);
+ old = *field;
+ FT_ATOMIC_STORE_PTR_RELEASE(*field, value);
+ Py_END_CRITICAL_SECTION();
+ Py_XDECREF(old);
+}
+
#endif
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]