Skip to content

_sqlite: NULL dereference when connection is closed from within a callback #145040

Open

Description

@raminfp

Bug report

Bug description:

A segmentation fault occurs in the _sqlite module when a connection is closed inside an aggregate step() callback. After sqlite3_step() returns, _pysqlite_query_execute() calls sqlite3_last_insert_rowid(self->connection->db) without verifying that self->connection->db is still valid.

If the connection was closed during the callback, self->connection->db becomes NULL, leading to a NULL pointer dereference inside sqlite3_last_insert_rowid().

Affected Code

Modules/_sqlite/cursor.c:974

lastrowid = sqlite3_last_insert_rowid(self->connection->db);

Reproducer

import sqlite3

conn = sqlite3.connect(":memory:", autocommit=True)
conn.execute("CREATE TABLE t(x INTEGER)")

for i in range(50):
    conn.execute("INSERT INTO t VALUES (?)", (i,))

class MyAgg:
    def __init__(self):
        self.total = 0

    def step(self, value):
        self.total += value
        conn.close()

    def finalize(self):
        return self.total


conn.create_aggregate("agg_sum", 1, MyAgg)
conn.execute("SELECT agg_sum(x) FROM t").fetchone()

Result

AddressSanitizer:DEADLYSIGNAL
=================================================================
==19418==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000038 (pc 0x742d9562d034 bp 0x7fffdf6c6360 sp 0x7fffdf6c62f8 T0)
==19418==The signal is caused by a READ memory access.
==19418==Hint: address points to the zero page.
    #0 0x742d9562d034 in sqlite3_last_insert_rowid (/lib/x86_64-linux-gnu/libsqlite3.so.0+0xaa034) (BuildId: f5959b839b5d4de9879dc3630ecde4d76d4be713)
    #1 0x742d96bd55c5 in _pysqlite_query_execute Modules/_sqlite/cursor.c:974
    #2 0x742d96bcdbc5 in pysqlite_connection_execute_impl Modules/_sqlite/connection.c:1848
    #3 0x742d96bcdd60 in pysqlite_connection_execute Modules/_sqlite/clinic/connection.c.h:967
    #4 0x5f5ad4a35cd6 in method_vectorcall_FASTCALL Objects/descrobject.c:402
    #5 0x5f5ad4a15ccc in _PyObject_VectorcallTstate Include/internal/pycore_call.h:136
    #6 0x5f5ad4a15dbf in PyObject_Vectorcall Objects/call.c:327
    #7 0x5f5ad4c948b7 in _Py_VectorCallInstrumentation_StackRefSteal Python/ceval.c:769
    #8 0x5f5ad4ca487b in _PyEval_EvalFrameDefault Python/generated_cases.c.h:1817
    #9 0x5f5ad4cdbab0 in _PyEval_EvalFrame Include/internal/pycore_ceval.h:118
    #10 0x5f5ad4cdbe16 in _PyEval_Vector Python/ceval.c:2132
    #11 0x5f5ad4cdc0cc in PyEval_EvalCode Python/ceval.c:680
    #12 0x5f5ad4ddf771 in run_eval_code_obj Python/pythonrun.c:1366
    #13 0x5f5ad4ddfab7 in run_mod Python/pythonrun.c:1469
    #14 0x5f5ad4de09ec in pyrun_file Python/pythonrun.c:1294
    #15 0x5f5ad4de3822 in _PyRun_SimpleFileObject Python/pythonrun.c:518
    #16 0x5f5ad4de3ace in _PyRun_AnyFileObject Python/pythonrun.c:81
    #17 0x5f5ad4e38d76 in pymain_run_file_obj Modules/main.c:410
    #18 0x5f5ad4e38fe3 in pymain_run_file Modules/main.c:429
    #19 0x5f5ad4e3a7e1 in pymain_run_python Modules/main.c:691
    #20 0x5f5ad4e3ae77 in Py_RunMain Modules/main.c:772
    #21 0x5f5ad4e3b063 in pymain_main Modules/main.c:802
    #22 0x5f5ad4e3b3e8 in Py_BytesMain Modules/main.c:826
    #23 0x5f5ad48a0675 in main Programs/python.c:15
    #24 0x742d9782a3b7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #25 0x742d9782a47a in __libc_start_main_impl ../csu/libc-start.c:360
    #26 0x5f5ad48a05a4 in _start (/home/raminfp/Projects/cpython/python+0x2ee5a4) (BuildId: fde840bca26b2112d86f1a7490e122abaa6caef7)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libsqlite3.so.0+0xaa034) (BuildId: f5959b839b5d4de9879dc3630ecde4d76d4be713) in sqlite3_last_insert_rowid
==19418==ABORTING

Suggested Fix

Add a NULL check before calling sqlite3_last_insert_rowid():

if (self->connection->db != NULL) {
    lastrowid = sqlite3_last_insert_rowid(self->connection->db);
}

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

  1. erlend-aasland commented on Feb 20, 2026

    @erlend-aasland
    Contributor

    This probably affects the other callbacks (see what's required for window functions). I do find it very peculiar that real code would ever close the connection from any of those callbacks. This is a programming error (or API misuse). However, we should never crash hard.

  2. raminfp commented on Feb 20, 2026

    @raminfp
    ContributorAuthor

    @erlend-aasland

    You're right, this affects all callbacks invoked during sqlite3_step(). I've already found and reported the sqlite3_changes() variant via create_function + executemany in #145046.

    The fix in my PR (the NULL check right after stmt_step()) should cover all callback types, aggregate step/finalize, user-defined functions, window function callbacks, and progress handlers, since they all execute within sqlite3_step(), and the check gates every subsequent use of self->connection->db.

    I'll add additional tests for the other callback scenarios (create_function, window functions) to make the coverage comprehensive. Should I also close #145046 as a duplicate since the root cause and fix are the same?

  3. erlend-aasland commented on Feb 20, 2026

    @erlend-aasland
    Contributor

    Yes, close the duplicate with a ref. to this issue. Moreover, add (as you suggested) test cases that ensures all paths to the C callbacks are handled.

  4. erlend-aasland commented on Feb 20, 2026

    @erlend-aasland
    Contributor

    Is it possible to trigger this API misuse in the step callback in pysqlite_cursor_iternext?

  5. raminfp commented on Feb 20, 2026

    @raminfp
    ContributorAuthor

    @erlend-aasland

    SEGV in pysqlite_cursor_iternext via progress handler

    import sqlite3
    
    conn = sqlite3.connect(":memory:", autocommit=True)
    conn.execute("CREATE TABLE t(x)")
    conn.executemany("INSERT INTO t VALUES(?)", [(i,) for i in range(1000)])
    
    rows = 0
    def evil_progress():
        global rows
        if rows >= 5:
            conn.close()
            return 1
        return 0
    
    cursor = conn.execute("SELECT * FROM t")
    conn.set_progress_handler(evil_progress, 1)
    for row in cursor:
        rows += 1

    Output :

    python: ./Modules/_sqlite/cursor.c:83: cursor_sqlite3_internal_error: Assertion `!PyErr_Occurred()' failed.
    [1]    25320 IOT instruction (core dumped)  ./python segv_iternext.py
  6. picnixz commented on Feb 20, 2026

    @picnixz
    Member

    Didn't I already fix this one actually?

  7. picnixz commented on Feb 20, 2026

    @picnixz
    Member

    Ok I actually forgot about it in #143245. I knew that there could have been other cases but I wasn't able to trigger them. So thanks for finding them!

  8. raminfp commented on Feb 21, 2026

    @raminfp
    ContributorAuthor

    Garbage-collecting pysqlite_cursor_iternext via progress handler

    import sqlite3, faulthandler
    faulthandler.enable()
    
    conn = sqlite3.connect(':memory:', autocommit=True)
    conn.execute('CREATE TABLE t(x)')
    conn.executemany('INSERT INTO t VALUES(?)', [(i,) for i in range(1000)])
    
    rows = 0
    def evil_progress():
        global rows
        if rows >= 5:
            conn.close()
            return 1
        return 0
    
    cursor = conn.execute('SELECT * FROM t')
    conn.set_progress_handler(evil_progress, 1)
    for row in cursor:
        rows += 1

    Output :

    python: ./Include/internal/pycore_call.h:85: _PyVectorcall_FunctionInline: Assertion `callable != NULL' failed.
    Fatal Python error: Aborted
    
    Current thread 0x00007289132e3380 [python] (most recent call first):
      Garbage-collecting
    
    Current thread's C stack trace (most recent call first):
      Binary file "/lib/x86_64-linux-gnu/libasan.so.8", at +0x831e0 [0x728912c831e0]
      Binary file "./python", at _Py_DumpStack+0xb6 [0x5d04183355dd]
      Binary file "./python", at +0x89f0c3 [0x5d041836d0c3]
      Binary file "./python", at +0x8a0417 [0x5d041836e417]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x45250 [0x728912845250]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at pthread_kill+0x11c [0x7289128a3f1c]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at gsignal+0x1e [0x72891284519e]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at abort+0xdf [0x728912828902]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x2881e [0x72891282881e]
      Binary file "/lib/x86_64-linux-gnu/libc.so.6", at +0x3b7c7 [0x72891283b7c7]
      Binary file "./python", at +0x462f10 [0x5d0417f30f10]
      Binary file "./python", at +0x463cb6 [0x5d0417f31cb6]
      Binary file "./python", at PyObject_CallOneArg+0xcb [0x5d0417f31e8d]
      Binary file "./python", at +0x77a72d [0x5d041824872d]
      Binary file "./python", at _PyErr_SetObject+0x166 [0x5d04182499f8]
      Binary file "./python", at _PyErr_SetString+0x35 [0x5d0418249e11]
      Binary file "./python", at PyErr_SetString+0x21 [0x5d0418249e51]
      Binary file "/home/raminfp/Projects/cpython/build/lib.linux-x86_64-3.15/_sqlite3.cpython-315d-x86_64-linux-gnu.so", at +0x26c43 [0x728911bd0c43]
      Binary file "/home/raminfp/Projects/cpython/build/lib.linux-x86_64-3.15/_sqlite3.cpython-315d-x86_64-linux-gnu.so", at +0x26c80 [0x728911bd0c80]
      Binary file "/home/raminfp/Projects/cpython/build/lib.linux-x86_64-3.15/_sqlite3.cpython-315d-x86_64-linux-gnu.so", at +0x2896f [0x728911bd296f]
      Binary file "/home/raminfp/Projects/cpython/build/lib.linux-x86_64-3.15/_sqlite3.cpython-315d-x86_64-linux-gnu.so", at +0x2ab51 [0x728911bd4b51]
      Binary file "./python", at _Py_Dealloc+0x186 [0x5d0417ff61c8]
      Binary file "./python", at +0x4efa72 [0x5d0417fbda72]
      Binary file "./python", at +0x4efa91 [0x5d0417fbda91]
      Binary file "./python", at +0x4f00e9 [0x5d0417fbe0e9]
      Binary file "./python", at +0x50110f [0x5d0417fcf10f]
      Binary file "./python", at PyDict_Clear+0xd [0x5d0417fcf2a6]
      Binary file "./python", at +0x5012d6 [0x5d0417fcf2d6]
      Binary file "./python", at +0x798f4a [0x5d0418266f4a]
      Binary file "./python", at +0x7996b7 [0x5d04182676b7]
      Binary file "./python", at +0x79abec [0x5d0418268bec]
      Binary file "./python", at +0x79c1a2 [0x5d041826a1a2]
      <truncated rest of calls>
    [1]    79145 IOT instruction (core dumped) 

    CC: @picnixz @erlend-aasland

  9. changed the title [-]NULL dereference in _sqlite when connection is closed inside aggregate step callback[/-] [+]NULL dereference in _sqlite when connection is closed from within a callback[/+] on Feb 21, 2026
  10. picnixz commented on Feb 21, 2026

    @picnixz
    Member

    FTR, I also have #143210 that tries to fix some issues during the binding step. I don't think it will help here but when you try to find more UAF/SEGV issues, try to also assume that the PR is merged (that is, checkout from there). It helps isolating the exact issues as well

  11. raminfp commented on Feb 21, 2026

    @raminfp
    ContributorAuthor

    Thanks for the pointer! I've reviewed #143210, our PRs are complementary: yours guards against mutations during the binding step (before stmt_step()), while mine guards against callbacks that close the connection during sqlite3_step() (aggregate, UDF, progress handler, collation, window functions). No overlap, and both are needed. I'll keep your branch in mind when hunting for more issues. Really appreciate you taking the time to connect the dots between these two efforts! ‍

  12. added
    type-crashA hard crash of the interpreter, possibly with a core dump
    on Feb 21, 2026
  13. changed the title [-]NULL dereference in _sqlite when connection is closed from within a callback[/-] [+]`_sqlite`: NULL dereference when connection is closed from within a callback[/+] on Feb 25, 2026
  14. raminfp commented on Feb 26, 2026

    @raminfp
    ContributorAuthor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirtopic-sqlite3type-bugAn unexpected behavior, bug, or errortype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions