Skip to content

Null pointer dereference in _sqlite cursor cache via re-entrant parameter iterator #143198

@jackfromeast

Description

@jackfromeast

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

Metadata

Metadata

Assignees

Labels

extension-modulesC modules in the Modules dirtopic-sqlite3type-crashA hard crash of the interpreter, possibly with a core dump

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions