Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 104 additions & 66 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ static const char * const begin_statements[] = {

static int pysqlite_connection_set_isolation_level(pysqlite_Connection* self, PyObject* isolation_level, void *Py_UNUSED(ignored));
static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self);
static void free_callback_context(callback_context *ctx);

static PyObject *
new_statement_cache(pysqlite_Connection *self, int maxsize)
Expand All @@ -76,6 +77,16 @@ new_statement_cache(pysqlite_Connection *self, int maxsize)
return res;
}


#define SET_CALLBACK_CONTEXT(ctx_ptr, ctx) \
do { \
callback_context *tmp = ctx_ptr; \
ctx_ptr = ctx; \
if (tmp) { \
free_callback_context(tmp); \
} \
} while (0)

/*[clinic input]
_sqlite3.Connection.__init__ as pysqlite_connection_init

Expand Down Expand Up @@ -168,9 +179,9 @@ pysqlite_connection_init_impl(pysqlite_Connection *self,
self->thread_ident = PyThread_get_thread_ident();
self->check_same_thread = check_same_thread;

self->function_pinboard_trace_callback = NULL;
self->function_pinboard_progress_handler = NULL;
self->function_pinboard_authorizer_cb = NULL;
SET_CALLBACK_CONTEXT(self->trace_ctx, NULL);
SET_CALLBACK_CONTEXT(self->progress_ctx, NULL);
SET_CALLBACK_CONTEXT(self->authorizer_ctx, NULL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three statements are named free_callback_contexts() below. But here they are problematic.

  • If this is a brand new connection, then PyType_GenericAlloc has set all these to NULL and they don't need to be NULLed again.
  • If someone is calling __init__ for the second time, this might free the Python callable, but SQLite will still have the pointer. Also the old db will leak, and who knows what'll happen to old cursors… Whoa, I don't see how reinitialization could work; should it rather raise an exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had reinitialisation in mind, but as you say, it's hard to imagine how to do that properly anyway. Maybe implicit connection close at the start of __init__ will do the trick. I'll have to think that through. Perhaps we should fix the reinit case before proceeding with this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe implicit connection close at the start of __init__ will do the trick.

I would be surprised. Cursors seem to have hard (and reasonable!) assumptions about their connection's db.
I also don't see the use case – when would a new connection object not be enough?

Copy link
Contributor Author

@erlend-aasland erlend-aasland Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, true. Let's raise an exception instead.

I did some tests, and reinit actually seems to work pretty well using an implicit close in __init__, even with dangling cursors that fetch and execute after reinit. There's no refleaks on our side, and SQLite seems to be able to clean up as well. The only improvement would be to clear the callbacks from SQLite before the implicit close.

I also don't see the use case

Me neither.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no refleaks on our side, and SQLite seems to be able to clean up as well.

I'm surprised. Wouldn't this result in a sqlite3_open_v2 without a corresponding close?

Copy link
Contributor Author

@erlend-aasland erlend-aasland Sep 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I was a little bit vague. I did the tests with an added implicit close in __init__.

Here's the diff I used:

diff --git a/Modules/_sqlite/connection.c b/Modules/_sqlite/connection.c
index 63fd916662..961afe8de0 100644
--- a/Modules/_sqlite/connection.c
+++ b/Modules/_sqlite/connection.c
@@ -56,6 +56,7 @@ static const char * const begin_statements[] = {
 static int pysqlite_connection_set_isolation_level(pysqlite_Connection* self, PyObject* isolation_level, void *Py_UNUSED(ignored));
 static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self);
 static void free_callback_context(callback_context *ctx);
+static void connection_close(pysqlite_Connection *self);
 
 static PyObject *
 new_statement_cache(pysqlite_Connection *self, int maxsize)
@@ -114,6 +115,18 @@ pysqlite_connection_init_impl(pysqlite_Connection *self,
         return -1;
     }
 
+    // Gracefully handle reinitialization
+    if (self->initialized) {
+        (void)sqlite3_set_authorizer(self->db, NULL, NULL);
+        sqlite3_progress_handler(self->db, 0, NULL, NULL);
+#ifdef HAVE_TRACE_V2
+        sqlite3_trace_v2(self->db, SQLITE_TRACE_STMT, NULL, NULL);
+#else
+        sqlite3_trace(self->db, NULL, NULL);
+#endif
+        connection_close(self);
+    }
+
     pysqlite_state *state = pysqlite_get_state_by_type(Py_TYPE(self));
     self->state = state;
 

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Well, this is more complicated than I initially thought; I guess it should be a separate bpo issue.

I keep finding issues:

  • db is not set to NULL if init fails.
  • This segfaults for me:
import sqlite3

conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE foo (bar)')

try:
    conn.__init__('/bad-file/')
except sqlite3.OperationalError:
    pass

conn.execute('INSERT INTO foo (bar) VALUES (1), (2), (3), (4)')

... and even if that's fixed, I'm concerned that there will be other edge cases we can't reasonably think about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there's some issues in connection init. I'd gladly clean it up and harden it before continuing with this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a blocker for this PR; just please open a bpo so the issues won't be lost in a closed PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened issue 45126 for this.


self->Warning = state->Warning;
self->Error = state->Error;
Expand Down Expand Up @@ -214,6 +225,13 @@ pysqlite_do_all_statements(pysqlite_Connection *self)
}
}

#define VISIT_CALLBACK_CONTEXT(ctx) \
do { \
if (ctx) { \
Py_VISIT(ctx->callable); \
} \
} while (0)

static int
connection_traverse(pysqlite_Connection *self, visitproc visit, void *arg)
{
Expand All @@ -223,12 +241,20 @@ connection_traverse(pysqlite_Connection *self, visitproc visit, void *arg)
Py_VISIT(self->cursors);
Py_VISIT(self->row_factory);
Py_VISIT(self->text_factory);
Py_VISIT(self->function_pinboard_trace_callback);
Py_VISIT(self->function_pinboard_progress_handler);
Py_VISIT(self->function_pinboard_authorizer_cb);
VISIT_CALLBACK_CONTEXT(self->trace_ctx);
VISIT_CALLBACK_CONTEXT(self->progress_ctx);
VISIT_CALLBACK_CONTEXT(self->authorizer_ctx);
#undef VISIT_CALLBACK_CONTEXT
return 0;
}

#define CLEAR_CALLBACK_CONTEXT(ctx) \
do { \
if (ctx) { \
Py_CLEAR(ctx->callable); \
} \
} while (0)

static int
connection_clear(pysqlite_Connection *self)
{
Expand All @@ -237,9 +263,10 @@ connection_clear(pysqlite_Connection *self)
Py_CLEAR(self->cursors);
Py_CLEAR(self->row_factory);
Py_CLEAR(self->text_factory);
Py_CLEAR(self->function_pinboard_trace_callback);
Py_CLEAR(self->function_pinboard_progress_handler);
Py_CLEAR(self->function_pinboard_authorizer_cb);
CLEAR_CALLBACK_CONTEXT(self->trace_ctx);
CLEAR_CALLBACK_CONTEXT(self->progress_ctx);
CLEAR_CALLBACK_CONTEXT(self->authorizer_ctx);
#undef CLEAR_CALLBACK_CONTEXT
return 0;
}

Expand All @@ -253,6 +280,14 @@ connection_close(pysqlite_Connection *self)
}
}

static void
free_callback_contexts(pysqlite_Connection *self)
{
SET_CALLBACK_CONTEXT(self->trace_ctx, NULL);
SET_CALLBACK_CONTEXT(self->progress_ctx, NULL);
SET_CALLBACK_CONTEXT(self->authorizer_ctx, NULL);
}

static void
connection_dealloc(pysqlite_Connection *self)
{
Expand All @@ -262,6 +297,7 @@ connection_dealloc(pysqlite_Connection *self)

/* Clean up if user has not called .close() explicitly. */
connection_close(self);
free_callback_contexts(self);

tp->tp_free(self);
Py_DECREF(tp);
Expand Down Expand Up @@ -598,6 +634,19 @@ _pysqlite_build_py_params(sqlite3_context *context, int argc,
return NULL;
}

static void
print_or_clear_traceback(callback_context *ctx)
{
assert(ctx != NULL);
assert(ctx->state != NULL);
if (ctx->state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
}

// Checks the Python exception and sets the appropriate SQLite error code.
static void
set_sqlite_error(sqlite3_context *context, const char *msg)
Expand All @@ -613,14 +662,7 @@ set_sqlite_error(sqlite3_context *context, const char *msg)
sqlite3_result_error(context, msg, -1);
}
callback_context *ctx = (callback_context *)sqlite3_user_data(context);
assert(ctx != NULL);
assert(ctx->state != NULL);
if (ctx->state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
print_or_clear_traceback(ctx);
}

static void
Expand Down Expand Up @@ -793,7 +835,7 @@ static void
free_callback_context(callback_context *ctx)
{
assert(ctx != NULL);
Py_DECREF(ctx->callable);
Py_XDECREF(ctx->callable);
PyMem_Free(ctx);
}

Expand Down Expand Up @@ -913,30 +955,20 @@ static int _authorizer_callback(void* user_arg, int action, const char* arg1, co
PyObject *ret;
int rc;

ret = PyObject_CallFunction((PyObject*)user_arg, "issss", action, arg1, arg2, dbname, access_attempt_source);
callback_context *ctx = (callback_context *)user_arg;
assert(ctx != NULL);
ret = PyObject_CallFunction(ctx->callable, "issss", action, arg1, arg2,
dbname, access_attempt_source);

if (ret == NULL) {
pysqlite_state *state = pysqlite_get_state(NULL);
if (state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}

print_or_clear_traceback(ctx);
rc = SQLITE_DENY;
}
else {
if (PyLong_Check(ret)) {
rc = _PyLong_AsInt(ret);
if (rc == -1 && PyErr_Occurred()) {
pysqlite_state *state = pysqlite_get_state(NULL);
if (state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
print_or_clear_traceback(ctx);
rc = SQLITE_DENY;
}
}
Expand All @@ -956,8 +988,10 @@ static int _progress_handler(void* user_arg)

int rc;
PyObject *ret;
ret = _PyObject_CallNoArg((PyObject*)user_arg);

callback_context *ctx = (callback_context *)user_arg;
assert(ctx != NULL);
ret = _PyObject_CallNoArg(ctx->callable);
if (!ret) {
/* abort query if error occurred */
rc = -1;
Expand All @@ -967,13 +1001,7 @@ static int _progress_handler(void* user_arg)
Py_DECREF(ret);
}
if (rc < 0) {
pysqlite_state *state = pysqlite_get_state(NULL);
if (state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
print_or_clear_traceback(ctx);
}

PyGILState_Release(gilstate);
Expand Down Expand Up @@ -1004,21 +1032,18 @@ static void _trace_callback(void* user_arg, const char* statement_string)
PyObject *ret = NULL;
py_statement = PyUnicode_DecodeUTF8(statement_string,
strlen(statement_string), "replace");
callback_context *ctx = (callback_context *)user_arg;
assert(ctx != NULL);
if (py_statement) {
ret = PyObject_CallOneArg((PyObject*)user_arg, py_statement);
ret = PyObject_CallOneArg(ctx->callable, py_statement);
Py_DECREF(py_statement);
}

if (ret) {
Py_DECREF(ret);
} else {
pysqlite_state *state = pysqlite_get_state(NULL);
if (state->enable_callback_tracebacks) {
PyErr_Print();
}
else {
PyErr_Clear();
}
}
else {
print_or_clear_traceback(ctx);
}

PyGILState_Release(gilstate);
Expand Down Expand Up @@ -1047,17 +1072,21 @@ pysqlite_connection_set_authorizer_impl(pysqlite_Connection *self,
int rc;
if (authorizer_cb == Py_None) {
rc = sqlite3_set_authorizer(self->db, NULL, NULL);
Py_XSETREF(self->function_pinboard_authorizer_cb, NULL);
SET_CALLBACK_CONTEXT(self->authorizer_ctx, NULL);
}
else {
Py_INCREF(authorizer_cb);
Py_XSETREF(self->function_pinboard_authorizer_cb, authorizer_cb);
rc = sqlite3_set_authorizer(self->db, _authorizer_callback, authorizer_cb);
callback_context *ctx = create_callback_context(self->state,
authorizer_cb);
if (ctx == NULL) {
return NULL;
}
rc = sqlite3_set_authorizer(self->db, _authorizer_callback, ctx);
SET_CALLBACK_CONTEXT(self->authorizer_ctx, ctx);
}
if (rc != SQLITE_OK) {
PyErr_SetString(self->OperationalError,
"Error setting authorizer callback");
Py_XSETREF(self->function_pinboard_authorizer_cb, NULL);
SET_CALLBACK_CONTEXT(self->authorizer_ctx, NULL);
return NULL;
}
Py_RETURN_NONE;
Expand Down Expand Up @@ -1085,11 +1114,15 @@ pysqlite_connection_set_progress_handler_impl(pysqlite_Connection *self,
if (progress_handler == Py_None) {
/* None clears the progress handler previously set */
sqlite3_progress_handler(self->db, 0, 0, (void*)0);
Py_XSETREF(self->function_pinboard_progress_handler, NULL);
SET_CALLBACK_CONTEXT(self->progress_ctx, NULL);
} else {
sqlite3_progress_handler(self->db, n, _progress_handler, progress_handler);
Py_INCREF(progress_handler);
Py_XSETREF(self->function_pinboard_progress_handler, progress_handler);
callback_context *ctx = create_callback_context(self->state,
progress_handler);
if (ctx == NULL) {
return NULL;
}
sqlite3_progress_handler(self->db, n, _progress_handler, ctx);
SET_CALLBACK_CONTEXT(self->progress_ctx, ctx);
}
Py_RETURN_NONE;
}
Expand Down Expand Up @@ -1126,15 +1159,20 @@ pysqlite_connection_set_trace_callback_impl(pysqlite_Connection *self,
#else
sqlite3_trace(self->db, 0, (void*)0);
#endif
Py_XSETREF(self->function_pinboard_trace_callback, NULL);
} else {
SET_CALLBACK_CONTEXT(self->trace_ctx, NULL);
}
else {
callback_context *ctx = create_callback_context(self->state,
trace_callback);
if (ctx == NULL) {
return NULL;
}
#ifdef HAVE_TRACE_V2
sqlite3_trace_v2(self->db, SQLITE_TRACE_STMT, _trace_callback, trace_callback);
sqlite3_trace_v2(self->db, SQLITE_TRACE_STMT, _trace_callback, ctx);
#else
sqlite3_trace(self->db, _trace_callback, trace_callback);
sqlite3_trace(self->db, _trace_callback, ctx);
#endif
Py_INCREF(trace_callback);
Py_XSETREF(self->function_pinboard_trace_callback, trace_callback);
SET_CALLBACK_CONTEXT(self->trace_ctx, ctx);
}

Py_RETURN_NONE;
Expand Down
8 changes: 4 additions & 4 deletions Modules/_sqlite/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ typedef struct
*/
PyObject* text_factory;

/* remember references to object used in trace_callback/progress_handler/authorizer_cb */
PyObject* function_pinboard_trace_callback;
PyObject* function_pinboard_progress_handler;
PyObject* function_pinboard_authorizer_cb;
// Remember contexts used by the trace, progress, and authoriser callbacks
callback_context *trace_ctx;
callback_context *progress_ctx;
callback_context *authorizer_ctx;

/* Exception objects: borrowed refs. */
PyObject* Warning;
Expand Down