Repository navigation
Null pointer dereference in _sqlite cursor cache via re-entrant parameter iterator #143198
Copy link
Copy link
Open
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-sqlite3type-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
Description
What happened?
cursor.executemany fetches parameters by calling PyObject_GetIter, so a crafted iterable can close the connection during iteration and clear self->connection->db. The cursor then continues into get_statement_from_cache with a NULL connection handle, leading _Py_TYPE to dereference a null pointer when trying to reuse the cached statement.
Proof of Concept:
import sqlite3
class Params:
def __iter__(self):
conn.close()
return iter([(1,)])
conn = sqlite3.connect(':memory:')
cur = conn.cursor()
cur.executemany('SELECT ?', Params())Affected Versions
| Python Version | Status | Exit Code |
|---|---|---|
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) |
Exception | 1 |
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] |
Exception | 1 |
Python 3.11.14+ (heads/3.11:88f3f5b5f11, Oct 28 2025, 16:53:08) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.12.12+ (heads/3.12:8cb2092bd8c, Oct 28 2025, 16:54:14) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.13.9+ (heads/3.13:9c8eade20c6, Oct 28 2025, 16:55:18) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.14.0+ (heads/3.14:2e216728038, Oct 28 2025, 16:56:16) [Clang 18.1.3 (1ubuntu1)] |
ASAN | 1 |
Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0] |
ASAN | 1 |
Vulnerable Code
PyObject *
_pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* operation, PyObject* second_argument)
{
PyObject* parameters_list = NULL;
PyObject* parameters_iter = NULL;
PyObject* parameters = NULL;
int i;
int rc;
int numcols;
PyObject* column_name;
if (!check_cursor(self)) {
goto error;
}
self->locked = 1;
if (multiple) {
if (PyIter_Check(second_argument)) {
/* iterator */
parameters_iter = Py_NewRef(second_argument);
} else {
/* sequence */
// Bug: Trigger __iter__ method and set self->connection->db to null
parameters_iter = PyObject_GetIter(second_argument);
if (!parameters_iter) {
goto error;
}
}
} else {
parameters_list = PyList_New(0);
if (!parameters_list) {
goto error;
}
if (second_argument == NULL) {
second_argument = PyTuple_New(0);
if (!second_argument) {
goto error;
}
} else {
Py_INCREF(second_argument);
}
if (PyList_Append(parameters_list, second_argument) != 0) {
Py_DECREF(second_argument);
goto error;
}
Py_DECREF(second_argument);
parameters_iter = PyObject_GetIter(parameters_list);
if (!parameters_iter) {
goto error;
}
}
/* reset description */
Py_INCREF(Py_None);
Py_SETREF(self->description, Py_None);
if (self->statement) {
// Reset pending statements on this cursor.
if (stmt_reset(self->statement) != SQLITE_OK) {
goto reset_failure;
}
}
// Crash: Use the null pointer without anycheck
PyObject *stmt = get_statement_from_cache(self, operation);
Py_XSETREF(self->statement, (pysqlite_Statement *)stmt);
if (!self->statement) {
goto error;
}
/* ... */Sanitizer Output
=================================================================
==1701025==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x5d9f40309b11 bp 0x7fff345709e0 sp 0x7fff345709d0 T0)
==1701025==The signal is caused by a READ memory access.
==1701025==Hint: address points to the zero page.
#0 0x5d9f40309b11 in _Py_TYPE Include/object.h:277
#1 0x5d9f40309b11 in _PyVectorcall_FunctionInline Include/internal/pycore_call.h:120
#2 0x5d9f40309b11 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:164
#3 0x5d9f40309b11 in PyObject_Vectorcall Objects/call.c:327
#4 0x76a658353bb4 in get_statement_from_cache Modules/_sqlite/cursor.c:562
#5 0x76a658353bb4 in _pysqlite_query_execute Modules/_sqlite/cursor.c:884
#6 0x5d9f40309bc7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
#7 0x5d9f40309bc7 in PyObject_Vectorcall Objects/call.c:327
#8 0x5d9f401abb82 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
#9 0x5d9f4068a256 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
#10 0x5d9f4068a256 in _PyEval_Vector Python/ceval.c:2005
#11 0x5d9f4068a256 in PyEval_EvalCode Python/ceval.c:888
#12 0x5d9f407cff8e in run_eval_code_obj Python/pythonrun.c:1365
#13 0x5d9f407cff8e in run_mod Python/pythonrun.c:1459
#14 0x5d9f407d4c37 in pyrun_file Python/pythonrun.c:1293
#15 0x5d9f407d4c37 in _PyRun_SimpleFileObject Python/pythonrun.c:521
#16 0x5d9f407d575c in _PyRun_AnyFileObject Python/pythonrun.c:81
#17 0x5d9f40847ecc in pymain_run_file_obj Modules/main.c:410
#18 0x5d9f40847ecc in pymain_run_file Modules/main.c:429
#19 0x5d9f40847ecc in pymain_run_python Modules/main.c:691
#20 0x5d9f408497ae in Py_RunMain Modules/main.c:772
#21 0x5d9f408497ae in pymain_main Modules/main.c:802
#22 0x5d9f408497ae in Py_BytesMain Modules/main.c:826
#23 0x76a658a2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#24 0x76a658a2a28a in __libc_start_main_impl ../csu/libc-start.c:360
#25 0x5d9f401e1694 in _start (/home/jackfromeast/Desktop/entropy/tasks/reproducexx/targets/cpython-main/python+0x20e694) (BuildId: 9b8528ade1939b4eac85ea0eb0dad4843ee8ca4b)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV Include/object.h:277 in _Py_TYPE
==1701025==ABORTING
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-sqlite3type-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
Projects
- StatusShow more project fieldsNo status
Activity
pythongh-143198: fix SIGSEGV in `sqlite3.execute[many]` with re-entra…
pythongh-143198: fix SIGSEGV in `sqlite3.execute[many]` with re-entra…
serhiy-storchaka commentedon Dec 29, 2025
Many such reports use the word "re-entrant". I do not think it means what you think it means. A re-entrant call is
when a function, while still executing its first call, is called again (either recursively or by another thread/process).
jackfromeast commentedon Dec 29, 2025
Hi @serhiy-storchaka, thank you for bringing this up!
I understand "reentrant" has a precise technical definition that the same function being entered twice before the first invocation completes. However, I'm using it in a broader sense here to describe a related pattern: the interpreter frame being entered twice (when an opcode handler that dispatched from a interpreter frame invokes user-overridden/overwritten/provided code that gets interpreted again).
I'm drawing from CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls, which describes a similar issue pattern. That said, I recognize there may be a more precise term for this specific bug pattern in the Python interpreter context.
Would you suggest an alternative term that better captures this nested interpreter invocation pattern? I'm open to adjusting the terminology if "reentrant" feels too broad or potentially misleading here.
_sqliteexecute via re-entrant sequence length #143380 as a duplicate of this issue