Crash report
What happened?
If a Python callback is invoked by SQLite during query preparation or execution -- an authorizer (set_authorizer), progress handler (set_progress_handler), trace callback (set_trace_callback), or user-defined function (create_function) -- and that callback calls con.close while SQLite is still on the C stack, the process hard-crashes.
The problem is that connection_close sets self->db to null (then calls sqlite3_close_v2 -- which defers the actual close until all statements are finalised).
|
connection_close(pysqlite_Connection *self) |
|
{ |
|
if (self->db == NULL) { |
|
return 0; |
|
} |
|
|
|
int rc = 0; |
|
if (self->autocommit == AUTOCOMMIT_DISABLED && |
|
!sqlite3_get_autocommit(self->db)) |
|
{ |
|
if (connection_exec_stmt(self, "ROLLBACK") < 0) { |
|
rc = -1; |
|
} |
|
} |
|
|
|
sqlite3 *db = self->db; |
|
self->db = NULL; |
|
|
|
Py_BEGIN_ALLOW_THREADS |
|
/* The v2 close call always returns SQLITE_OK if given a valid database |
|
* pointer (which we do), so we can safely ignore the return value */ |
|
(void)sqlite3_close_v2(db); |
|
Py_END_ALLOW_THREADS |
|
|
|
free_callback_contexts(self); |
There are two different crash causes from what I can tell.
The first is a null pointer dereference on db.
After the callback returns, self->connection->db is null but the cursor code reads it unconditionally in several places:
|
set_error_from_db(state, self->connection->db); |
|
self->rowcount += (long)sqlite3_changes(self->connection->db); |
|
lastrowid = sqlite3_last_insert_rowid(self->connection->db); |
|
self->rowcount = (long)sqlite3_changes(self->connection->db); |
|
rc = set_error_from_db(self->connection->state, self->connection->db); |
All call sites either call set_error_from_db directly or pass db to a SQLite API. set_error_from_db also lacks a null guard:
|
set_error_from_db(pysqlite_state *state, sqlite3 *db) |
|
{ |
|
int errorcode = sqlite3_errcode(db); |
|
PyObject *exc_class = get_exception_class(state, errorcode); |
|
if (exc_class == NULL) { |
|
// No new exception need be raised. |
|
return SQLITE_OK; |
|
} |
|
|
|
/* Create and set the exception. */ |
|
int extended_errcode = sqlite3_extended_errcode(db); |
|
// sqlite3_errmsg() always returns an UTF-8 encoded message |
|
const char *errmsg = sqlite3_errmsg(db); |
|
raise_exception(exc_class, extended_errcode, errmsg); |
|
return errorcode; |
|
} |
The second crash path is a use-after-free of callback_context (authorizer / progress / trace)
sqlite3_prepare_v2 can call the authorizer more than once per statement (e.g. once for SQLITE_SELECT, then once per SQLITE_READ). If the authorizer closes the connection (through connection_close), free_callback_contexts is called, which calls decref_callback_context on the in-flight context. Combined with the matching decref at the end of the C wrapper, the context's refcount reaches zero and the struct is freed. The next authorizer invocation then calls incref_callback_context on a freed callback_context.
The same problem applies to progress_callback and trace_callback when those fire more than once per step.
Additionally, because pysqlite_connection_close_impl calls Py_CLEAR(self->statement_cache) before calling into connection_close, and because get_statement_from_cache holds only a borrowed reference to the cache object, closing the connection from within a callback frees that object while the call into it is still ongoing.
Reproducer
This is a reproducer for just one example, the set_authorizer one.
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t (v INTEGER)")
def auth(action, arg1, arg2, db, trigger):
con.close()
return sqlite3.SQLITE_OK
con.set_authorizer(auth)
con.execute("SELECT v FROM t")
On CPython main, this results in
Assertion failed: (ctx->refcount > 0), function incref_callback_context, file connection.c, line 1131.
zsh: abort ./python.exe repro.py
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
No response
Linked PRs
Crash report
What happened?
If a Python callback is invoked by SQLite during query preparation or execution -- an authorizer (
set_authorizer), progress handler (set_progress_handler), trace callback (set_trace_callback), or user-defined function (create_function) -- and that callback callscon.closewhile SQLite is still on the C stack, the process hard-crashes.The problem is that
connection_closesetsself->dbto null (then callssqlite3_close_v2-- which defers the actual close until all statements are finalised).cpython/Modules/_sqlite/connection.c
Lines 441 to 465 in 884ac3e
There are two different crash causes from what I can tell.
The first is a null pointer dereference on
db.After the callback returns,
self->connection->dbis null but the cursor code reads it unconditionally in several places:cpython/Modules/_sqlite/cursor.c
Line 918 in 884ac3e
cpython/Modules/_sqlite/cursor.c
Line 961 in 884ac3e
cpython/Modules/_sqlite/cursor.c
Line 974 in 884ac3e
cpython/Modules/_sqlite/cursor.c
Line 1161 in 884ac3e
cpython/Modules/_sqlite/cursor.c
Line 1170 in 884ac3e
All call sites either call
set_error_from_dbdirectly or pass db to a SQLite API.set_error_from_dbalso lacks a null guard:cpython/Modules/_sqlite/util.c
Lines 139 to 154 in 884ac3e
The second crash path is a use-after-free of
callback_context(authorizer / progress / trace)sqlite3_prepare_v2can call the authorizer more than once per statement (e.g. once forSQLITE_SELECT, then once perSQLITE_READ). If the authorizer closes the connection (throughconnection_close),free_callback_contextsis called, which callsdecref_callback_contexton the in-flight context. Combined with the matching decref at the end of the C wrapper, the context's refcount reaches zero and the struct is freed. The next authorizer invocation then callsincref_callback_contexton a freedcallback_context.The same problem applies to
progress_callbackandtrace_callbackwhen those fire more than once per step.Additionally, because
pysqlite_connection_close_implcallsPy_CLEAR(self->statement_cache)before calling intoconnection_close, and becauseget_statement_from_cacheholds only a borrowed reference to the cache object, closing the connection from within a callback frees that object while the call into it is still ongoing.Reproducer
This is a reproducer for just one example, the
set_authorizerone.On CPython
main, this results inCPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
No response
Linked PRs