Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

sqlite3 crashes if a callback closes the connection during sqlite3_step #151030

Copy link
Copy link
@KowalskiThomas

Description

@KowalskiThomas
Issue body actions

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:

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

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.