Repository navigation
_sqlite: NULL dereference when connection is closed from within a callback #145040
Copy link
Copy link
Open
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-sqlite3type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errortype-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
Description
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==ABORTINGSuggested 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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
extension-modulesC modules in the Modules dirC modules in the Modules dirtopic-sqlite3type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errortype-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
erlend-aasland commentedon Feb 20, 2026
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.
raminfp commentedon Feb 20, 2026
@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 withinsqlite3_step(), and the check gates every subsequent use ofself->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?
erlend-aasland commentedon Feb 20, 2026
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.
erlend-aasland commentedon Feb 20, 2026
Is it possible to trigger this API misuse in the step callback in
pysqlite_cursor_iternext?raminfp commentedon Feb 20, 2026
@erlend-aasland
SEGV in pysqlite_cursor_iternext via progress handler
Output :
picnixz commentedon Feb 20, 2026
Didn't I already fix this one actually?
picnixz commentedon Feb 20, 2026
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!
raminfp commentedon Feb 21, 2026
Garbage-collecting pysqlite_cursor_iternext via progress handler
Output :
CC: @picnixz @erlend-aasland
[-]NULL dereference in _sqlite when connection is closed inside aggregate step callback[/-][+]NULL dereference in _sqlite when connection is closed from within a callback[/+]picnixz commentedon Feb 21, 2026
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
raminfp commentedon Feb 21, 2026
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!
[-]NULL dereference in _sqlite when connection is closed from within a callback[/-][+]`_sqlite`: NULL dereference when connection is closed from within a callback[/+]raminfp commentedon Feb 26, 2026
CC: @erlend-aasland
sqlite3crashes if a callback closes the connection duringsqlite3_step#151030 as a duplicate of this issue