From 9e1ffe25cf91e502b6b8c3d000a6b7972254eb71 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 6 Apr 2026 15:55:53 -0500 Subject: [PATCH] Detach thread context during long-running operations The underlying Assuan calls are not thread safe and we can't perform multiple operations on the same Assuan context simultaneously. Additionally, the possibility of code calling into this extension from multiple threads necessiatates data race protection mechanisms. After introducing the necessary protections, I'm moderately confident that this package is now compatible with "free-threaded" Python interpreters as well, so we might as well declare so. --- pyproject.toml | 1 + src/python/client.c | 45 ++++++++++++++++++++++-- src/python/init.c | 4 +++ src/python/server.c | 84 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fefefa8..ed46010 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ "Operating System :: POSIX :: Linux", "Programming Language :: C", "Programming Language :: Python :: 3", + "Programming Language :: Python :: Free Threading :: 2 - Beta", "Topic :: System :: Archiving :: Packaging", ] diff --git a/src/python/client.c b/src/python/client.c index 7f97f97..ca4e18f 100644 --- a/src/python/client.c +++ b/src/python/client.c @@ -24,6 +24,7 @@ typedef struct PyObject_HEAD gchar * name; assuan_context_t ctx; + GRWLock lock; } ClientObject; static PyObject * @@ -39,6 +40,7 @@ client_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (self) { self->ctx = NULL; self->name = NULL; + g_rw_lock_init(&self->lock); } return (PyObject *)self; } @@ -57,16 +59,21 @@ client_init(ClientObject *self, PyObject *args, PyObject *kwds) return -1; } + g_rw_lock_writer_lock(&self->lock); + if (NULL != self->name) { g_free(self->name); } self->name = g_strdup(name); if (NULL == self->name) { + g_rw_lock_writer_unlock(&self->lock); PyErr_NoMemory(); return -1; } + g_rw_lock_writer_unlock(&self->lock); + return 0; } static void @@ -74,6 +81,7 @@ client_dealloc(ClientObject *self) { Py_XDECREF(client_disconnect(self, NULL)); + g_rw_lock_clear(&self->lock); if (NULL != self->name) { g_free(self->name); } @@ -84,8 +92,14 @@ client_dealloc(ClientObject *self) static PyObject * client_repr(ClientObject *self) { - return PyUnicode_FromFormat( + g_rw_lock_reader_lock(&self->lock); + + PyObject *res = PyUnicode_FromFormat( "", self->name); + + g_rw_lock_reader_unlock(&self->lock); + + return res; } static PyObject * @@ -93,7 +107,16 @@ execute_transaction(ClientObject *self, const char * cmd) { gpg_error_t rc; + Py_BEGIN_ALLOW_THREADS + + g_rw_lock_writer_lock(&self->lock); + rc = assuan_transact(self->ctx, cmd, NULL, NULL, NULL, NULL, NULL, NULL); + + g_rw_lock_writer_unlock(&self->lock); + + Py_END_ALLOW_THREADS + if (rc) { PyErr_Format(PyExc_RuntimeError, "Transaction failed: %s", gpg_strerror(rc)); return NULL; @@ -198,6 +221,8 @@ client_connect(ClientObject *self, PyObject *args) gpg_error_t rc; + g_rw_lock_writer_lock(&self->lock); + gchar *cwd = g_path_is_absolute(self->name) ? NULL : g_get_current_dir(); gchar *sockpath = g_strconcat( cwd ? cwd : "", @@ -208,12 +233,14 @@ client_connect(ClientObject *self, PyObject *args) NULL); g_free(cwd); if (NULL == sockpath) { + g_rw_lock_writer_unlock(&self->lock); return PyErr_NoMemory(); } assuan_release(self->ctx); rc = assuan_new(&self->ctx); if (rc) { + g_rw_lock_writer_unlock(&self->lock); PyErr_Format(PyExc_RuntimeError, "Failed to initialize Assuan context: %s", gpg_strerror(rc)); g_free(sockpath); return NULL; @@ -222,10 +249,14 @@ client_connect(ClientObject *self, PyObject *args) rc = assuan_socket_connect(self->ctx, sockpath, ASSUAN_INVALID_PID, 0); g_free(sockpath); if (rc) { + assuan_release(self->ctx); + g_rw_lock_writer_unlock(&self->lock); PyErr_Format(PyExc_RuntimeError, "Failed to connect to server: %s", gpg_strerror(rc)); return NULL; } + g_rw_lock_writer_unlock(&self->lock); + Py_RETURN_NONE; } @@ -234,9 +265,13 @@ client_disconnect(ClientObject *self, PyObject *args) { (void)args; + g_rw_lock_writer_lock(&self->lock); + assuan_release(self->ctx); self->ctx = NULL; + g_rw_lock_writer_unlock(&self->lock); + Py_RETURN_NONE; } @@ -349,7 +384,13 @@ client_get_name(ClientObject *self, void *closure) { (void)closure; - return PyUnicode_FromString(self->name); + g_rw_lock_reader_lock(&self->lock); + + PyObject *res = PyUnicode_FromString(self->name); + + g_rw_lock_reader_unlock(&self->lock); + + return res; } static struct PyMethodDef client_methods[] = { diff --git a/src/python/init.c b/src/python/init.c index 936e0c9..2c05148 100644 --- a/src/python/init.c +++ b/src/python/init.c @@ -71,5 +71,9 @@ PyInit_createrepo_agent(void) PyModule_AddIntConstant(m, "EXIT_USAGE", CRA_EXIT_USAGE); PyModule_AddIntConstant(m, "EXIT_IN_USE", CRA_EXIT_IN_USE); +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + return m; } diff --git a/src/python/server.c b/src/python/server.c index c3c48db..1c31eea 100644 --- a/src/python/server.c +++ b/src/python/server.c @@ -30,6 +30,7 @@ typedef struct gchar * name; volatile sig_atomic_t sentinel; GThread * thread; + GRWLock lock; } ServerObject; static PyObject * @@ -37,7 +38,13 @@ server_shutdown_thread(ServerObject *self, PyObject *args); static void * server_thread(ServerObject *self) { - command_handler(self->fd, self->name, &self->sentinel); + g_rw_lock_reader_lock(&self->lock); + + if (0 == self->sentinel && ASSUAN_INVALID_FD != self->fd) { + command_handler(self->fd, self->name, &self->sentinel); + } + + g_rw_lock_reader_unlock(&self->lock); return NULL; } @@ -54,6 +61,7 @@ server_new(PyTypeObject *type, PyObject *args, PyObject *kwds) self->name = NULL; self->sentinel = 0; self->thread = NULL; + g_rw_lock_init(&self->lock); } return (PyObject *)self; } @@ -72,16 +80,31 @@ server_init(ServerObject *self, PyObject *args, PyObject *kwds) return -1; } + g_rw_lock_reader_lock(&self->lock); + + if (ASSUAN_INVALID_FD != self->fd) { + PyErr_SetString(PyExc_RuntimeError, "Server is already active"); + g_rw_lock_reader_unlock(&self->lock); + return -1; + } + + g_rw_lock_reader_unlock(&self->lock); + + g_rw_lock_writer_lock(&self->lock); + if (NULL != self->name) { g_free(self->name); } self->name = g_strdup(name); if (NULL == self->name) { + g_rw_lock_writer_unlock(&self->lock); PyErr_NoMemory(); return -1; } + g_rw_lock_writer_unlock(&self->lock); + return 0; } @@ -90,6 +113,7 @@ server_dealloc(ServerObject *self) { Py_XDECREF(server_shutdown_thread(self, NULL)); + g_rw_lock_clear(&self->lock); if (NULL != self->name) { g_free(self->name); } @@ -100,8 +124,14 @@ server_dealloc(ServerObject *self) static PyObject * server_repr(ServerObject *self) { - return PyUnicode_FromFormat( + g_rw_lock_reader_lock(&self->lock); + + PyObject *res = PyUnicode_FromFormat( "", self->name); + + g_rw_lock_reader_unlock(&self->lock); + + return res; } static PyObject * @@ -109,7 +139,30 @@ server_shutdown_thread(ServerObject *self, PyObject *args) { (void)args; + g_rw_lock_reader_lock(&self->lock); + + if (ASSUAN_INVALID_FD == self->fd && NULL == self->thread) { + g_rw_lock_reader_unlock(&self->lock); + Py_RETURN_NONE; + } + self->sentinel = 1; + + if (ASSUAN_INVALID_FD != self->fd) { + shutdown(self->fd, SHUT_RD); + } + + Py_BEGIN_ALLOW_THREADS + + if (NULL != self->thread) { + g_thread_ref(self->thread); + g_thread_join(self->thread); + } + + g_rw_lock_reader_unlock(&self->lock); + + g_rw_lock_writer_lock(&self->lock); + if (ASSUAN_INVALID_FD != self->fd) { shutdown(self->fd, SHUT_RD); self->fd = ASSUAN_INVALID_FD; @@ -122,6 +175,10 @@ server_shutdown_thread(ServerObject *self, PyObject *args) self->sentinel = 0; + g_rw_lock_writer_unlock(&self->lock); + + Py_END_ALLOW_THREADS + Py_RETURN_NONE; } @@ -130,8 +187,11 @@ server_start_thread(ServerObject *self, PyObject *args) { (void)args; + g_rw_lock_reader_lock(&self->lock); + if (ASSUAN_INVALID_FD != self->fd) { PyErr_SetString(PyExc_RuntimeError, "Server is already active"); + g_rw_lock_reader_unlock(&self->lock); return NULL; } @@ -143,11 +203,16 @@ server_start_thread(ServerObject *self, PyObject *args) g_str_has_suffix(self->name, "/") ? "" : "/", CRA_SOCK_NAME, NULL); + g_rw_lock_reader_unlock(&self->lock); g_free(cwd); if (NULL == sockpath) { return PyErr_NoMemory(); } + Py_BEGIN_ALLOW_THREADS + + g_rw_lock_writer_lock(&self->lock); + self->fd = create_server_socket(sockpath); if (self->fd == ASSUAN_INVALID_FD && errno == EADDRINUSE) { gpg_error_t res = try_server(sockpath); @@ -159,7 +224,11 @@ server_start_thread(ServerObject *self, PyObject *args) errno = EADDRINUSE; } } + + Py_END_ALLOW_THREADS + if (ASSUAN_INVALID_FD == self->fd) { + g_rw_lock_writer_unlock(&self->lock); PyErr_SetFromErrnoWithFilename(PyExc_OSError, sockpath); g_free(sockpath); return NULL; @@ -171,10 +240,13 @@ server_start_thread(ServerObject *self, PyObject *args) if (!self->thread) { assuan_sock_close(self->fd); self->fd = ASSUAN_INVALID_FD; + g_rw_lock_writer_unlock(&self->lock); PyErr_SetString(PyExc_RuntimeError, "Failed to start thread"); return NULL; } + g_rw_lock_writer_unlock(&self->lock); + Py_RETURN_NONE; } @@ -199,7 +271,13 @@ server_get_name(ServerObject *self, void *closure) { (void)closure; - return PyUnicode_FromString(self->name); + g_rw_lock_reader_lock(&self->lock); + + PyObject *res = PyUnicode_FromString(self->name); + + g_rw_lock_reader_unlock(&self->lock); + + return res; } static struct PyMethodDef server_methods[] = {