Skip to content

Null pointer dereference in cursor.fetchone after re-entrant text_factory closes connection #143662

@jackfromeast

Description

@jackfromeast

A user-provided text_factory runs while _pysqlite_fetch_one_row converts TEXT columns. If it calls Connection.close, connection->db is set to NULL, but pysqlite_cursor_iternext still calls sqlite3_changes(self->connection->db) once iteration advances. The SQLite helper dereferences the cleared pointer and segfaults.

Proof of Concept:

import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("create table t(x)")
cur = conn.cursor()
cur.execute("insert into t values ('a') returning x")

def text_factory(val, _conn=conn):
    if not getattr(text_factory, "armed", False):
        text_factory.armed = True
        _conn.close()
    return "x"

conn.text_factory = text_factory
cur.fetchone()

Vulnerable Code Snippet:

Click to expand
/* Buggy Re-entrant Path */
static PyObject *
pysqlite_cursor_fetchone_impl(pysqlite_Cursor *self)
{
    return pysqlite_cursor_iternext((PyObject *)self);
}

static PyObject *
pysqlite_cursor_iternext(PyObject *op)
{
    pysqlite_Cursor *self = _pysqlite_Cursor_CAST(op);
    /* ... */
    PyObject *row = _pysqlite_fetch_one_row(self);
    /* ... */
    if (rc == SQLITE_DONE) {
        if (self->statement->is_dml) {
            sqlite3 *db = self->connection->db;  /* crashing pointer derived */
            self->rowcount = (long)sqlite3_changes(db);  /* Crash site */
        }
        /* ... */
    }
    return row;
}

static PyObject *
_pysqlite_fetch_one_row(pysqlite_Cursor* self)
{
    /* ... */
    converted = PyObject_CallFunction(self->connection->text_factory,
                                      "y#", text, nbytes);  /* Reentrant call site */
    /* ... */
}

/* Clobbering Path */
static int
connection_close(pysqlite_Connection *self)
{
    sqlite3 *db = self->db;
    self->db = NULL;  /* state mutate site */
    (void)sqlite3_close_v2(db);
    /* ... */
    return rc;
}

Sanitizer Output:

Click to expand
AddressSanitizer:DEADLYSIGNAL
=================================================================
==431550==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000078 (pc 0x7fbfd133c1b4 bp 0x7ffed0387610 sp 0x7ffed0387608 T0)
==431550==The signal is caused by a READ memory access.
==431550==Hint: address points to the zero page.
    #0 0x7fbfd133c1b4 in sqlite3_changes64 (/lib/x86_64-linux-gnu/libsqlite3.so.0+0xa41b4) (BuildId: ac2bec9c45eec6feab0928b3b1373b467aa51339)
    #1 0x7fbfd133c1cc in sqlite3_changes (/lib/x86_64-linux-gnu/libsqlite3.so.0+0xa41cc) (BuildId: ac2bec9c45eec6feab0928b3b1373b467aa51339)
    #2 0x7fbfd3bdbb1c in pysqlite_cursor_iternext Modules/_sqlite/cursor.c:1121
    #3 0x7fbfd3bdbb1c in pysqlite_cursor_fetchone_impl Modules/_sqlite/cursor.c:1154
    #4 0x7fbfd3bdbb1c in pysqlite_cursor_fetchone Modules/_sqlite/clinic/cursor.c.h:169
    #5 0x6390ee26e3e7 in _PyObject_VectorcallTstate Include/internal/pycore_call.h:169
    #6 0x6390ee26e3e7 in PyObject_Vectorcall Objects/call.c:327
    #7 0x6390ee1225a2 in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1620
    #8 0x6390ee5ecad6 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:121
    #9 0x6390ee5ecad6 in _PyEval_Vector Python/ceval.c:2001
    #10 0x6390ee5ecad6 in PyEval_EvalCode Python/ceval.c:884
    #11 0x6390ee73216e in run_eval_code_obj Python/pythonrun.c:1365
    #12 0x6390ee73216e in run_mod Python/pythonrun.c:1459
    #13 0x6390ee736e17 in pyrun_file Python/pythonrun.c:1293
    #14 0x6390ee736e17 in _PyRun_SimpleFileObject Python/pythonrun.c:521
    #15 0x6390ee73793c in _PyRun_AnyFileObject Python/pythonrun.c:81
    #16 0x6390ee7aae3c in pymain_run_file_obj Modules/main.c:410
    #17 0x6390ee7aae3c in pymain_run_file Modules/main.c:429
    #18 0x6390ee7aae3c in pymain_run_python Modules/main.c:691
    #19 0x6390ee7ac71e in Py_RunMain Modules/main.c:772
    #20 0x6390ee7ac71e in pymain_main Modules/main.c:802
    #21 0x6390ee7ac71e in Py_BytesMain Modules/main.c:826
    #22 0x7fbfd442a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #23 0x7fbfd442a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #24 0x6390ee146634 in _start (/home/jackfromeast/Desktop/entropy/targets/grammar-afl++-latest/targets/cpython/python+0x206634) (BuildId: 4d105290d0ad566a4d6f4f7b2f05fbc9e317b533)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libsqlite3.so.0+0xa41b4) (BuildId: ac2bec9c45eec6feab0928b3b1373b467aa51339) in sqlite3_changes64
==431550==ABORTING

Affected Versions:

Details
Python Version Status Exit Code
Python 3.9.24+ (heads/3.9:111bbc15b26, Oct 28 2025, 16:51:20) OK 0
Python 3.10.19+ (heads/3.10:014261980b1, Oct 28 2025, 16:52:08) [Clang 18.1.3 (1ubuntu1)] OK 0
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

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.15.0a1+ (heads/main:f5394c257ce, Oct 28 2025, 19:29:54) [GCC 13.3.0]

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