wireplumber/modules/module-portal-permissionstore.c
Charles 627b003a05 m-permissions-portal: Avoid race condition during shutdown
Attempts to workaround a race condition between daemon thread and
GDBus worker thread during shutdown.

Ubuntu bug: https://bugs.launchpad.net/bugs/2127049

I've not been able to get a symbolic backtrace yet or reproduce it
myself, but the behaviour points to a threading bug. Hypothesis,

Main thread (1, daemon thread) shuts down, unregistering its plugins.
One of the plugins, module-permissions-portal, is triggered to
shutdown.
It tries to clear its GDBus connection handle without disconnecting
its signal handlers.
GDBus thread (2) is in the middle of writing a message on the same
connection handle.
Once finished, it also tries to clear its handle.
The main thread has already taken the signal lock and the signal
handler table ends up in an invalid state, triggering the assert.

I believe this could happen since
wp_portal_permissionstore_plugin_disable is not disconnecting its
signal handlers before trying to clear its DBus object.

See https://bugzilla.gnome.org/show_bug.cgi?id=730296 for more
discussion about this assert in the Glib signal handling code.
2025-10-10 13:04:01 +03:00

295 lines
8.6 KiB
C

/* WirePlumber
*
* Copyright © 2021 Collabora Ltd.
* @author Julian Bouzas <julian.bouzas@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include "dbus-connection-state.h"
WP_DEFINE_LOCAL_LOG_TOPIC ("m-portal-permissionstore")
#define DBUS_INTERFACE_NAME "org.freedesktop.impl.portal.PermissionStore"
#define DBUS_OBJECT_PATH "/org/freedesktop/impl/portal/PermissionStore"
enum
{
ACTION_GET_DBUS,
ACTION_LOOKUP,
ACTION_SET,
SIGNAL_CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0 };
struct _WpPortalPermissionStorePlugin
{
WpPlugin parent;
WpPlugin *dbus;
guint signal_id;
};
G_DECLARE_FINAL_TYPE (WpPortalPermissionStorePlugin,
wp_portal_permissionstore_plugin, WP, PORTAL_PERMISSIONSTORE_PLUGIN,
WpPlugin)
G_DEFINE_TYPE (WpPortalPermissionStorePlugin, wp_portal_permissionstore_plugin,
WP_TYPE_PLUGIN)
static gpointer
wp_portal_permissionstore_plugin_get_dbus (WpPortalPermissionStorePlugin *self)
{
return self->dbus ? g_object_ref (self->dbus) : NULL;
}
static GVariant *
wp_portal_permissionstore_plugin_lookup (WpPortalPermissionStorePlugin *self,
const gchar *table, const gchar *id)
{
g_autoptr (GDBusConnection) conn = NULL;
g_autoptr (GError) error = NULL;
g_autoptr (GVariant) res = NULL;
GVariant *permissions = NULL, *data = NULL;
g_object_get (self->dbus, "connection", &conn, NULL);
g_return_val_if_fail (conn, NULL);
/* Lookup */
res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Lookup",
g_variant_new ("(ss)", table, id), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL,
&error);
if (error) {
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
g_dbus_error_strip_remote_error (error);
/* NotFound is neither unexpected nor important, so log it as INFO */
if (!g_strcmp0 (remote_error, "org.freedesktop.portal.Error.NotFound")) {
wp_info_object (self, "Lookup: %s (%s)", error->message, remote_error);
return NULL;
}
wp_warning_object (self, "Lookup: %s (%s)", error->message, remote_error);
return NULL;
}
/* Get the permissions */
g_variant_get (res, "(@a{sas}@v)", &permissions, &data);
return permissions ? g_variant_ref (permissions) : NULL;
}
static void
wp_portal_permissionstore_plugin_set (WpPortalPermissionStorePlugin *self,
const gchar *table, gboolean create, const gchar *id, GVariant *permissions)
{
g_autoptr (GDBusConnection) conn = NULL;
g_autoptr (GError) error = NULL;
g_autoptr (GVariant) res = NULL;
GVariant *data = NULL;
g_object_get (self->dbus, "connection", &conn, NULL);
g_return_if_fail (conn);
/* Set */
res = g_dbus_connection_call_sync (conn, DBUS_INTERFACE_NAME,
DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, "Set",
g_variant_new ("(sbs@a{sas}@v)", table, id, permissions, data), NULL,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
if (error) {
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
g_dbus_error_strip_remote_error (error);
wp_warning_object (self, "Set: %s (%s)", error->message, remote_error);
}
}
static void
wp_portal_permissionstore_plugin_changed (GDBusConnection *connection,
const gchar *sender_name, const gchar *object_path,
const gchar *interface_name, const gchar *signal_name,
GVariant *parameters, gpointer user_data)
{
WpPortalPermissionStorePlugin *self =
WP_PORTAL_PERMISSIONSTORE_PLUGIN (user_data);
const char *table = NULL, *id = NULL;
gboolean deleted = FALSE;
GVariant *permissions = NULL, *data = NULL;
g_return_if_fail (parameters);
g_variant_get (parameters, "(ssb@v@a{sas})", &table, &id, &deleted, &data,
&permissions);
g_signal_emit (self, signals[SIGNAL_CHANGED], 0, table, id, deleted,
permissions);
}
static void
clear_signal (WpPortalPermissionStorePlugin *self)
{
g_autoptr (GDBusConnection) conn = NULL;
g_object_get (self->dbus, "connection", &conn, NULL);
if (conn && self->signal_id > 0) {
g_dbus_connection_signal_unsubscribe (conn, self->signal_id);
self->signal_id = 0;
}
}
static void
on_dbus_state_changed (GObject * dbus, GParamSpec * spec,
WpPortalPermissionStorePlugin *self)
{
WpDBusConnectionState state = -1;
g_object_get (dbus, "state", &state, NULL);
switch (state) {
case WP_DBUS_CONNECTION_STATE_CONNECTED: {
g_autoptr (GDBusConnection) conn = NULL;
g_object_get (dbus, "connection", &conn, NULL);
g_return_if_fail (conn);
self->signal_id = g_dbus_connection_signal_subscribe (conn,
DBUS_INTERFACE_NAME, DBUS_INTERFACE_NAME, "Changed", NULL, NULL,
G_DBUS_SIGNAL_FLAGS_NONE, wp_portal_permissionstore_plugin_changed,
self, NULL);
break;
}
case WP_DBUS_CONNECTION_STATE_CONNECTING:
case WP_DBUS_CONNECTION_STATE_CLOSED:
clear_signal (self);
break;
default:
break;
}
}
static void
wp_portal_permissionstore_plugin_init (WpPortalPermissionStorePlugin * self)
{
}
static void
wp_portal_permissionstore_plugin_enable (WpPlugin * plugin,
WpTransition * transition)
{
WpPortalPermissionStorePlugin *self =
WP_PORTAL_PERMISSIONSTORE_PLUGIN (plugin);
g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
self->dbus = wp_plugin_find (core, "dbus-connection");
if (!self->dbus) {
wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY,
WP_LIBRARY_ERROR_INVARIANT,
"dbus-connection module must be loaded before portal-permissionstore"));
return;
}
g_signal_connect_object (self->dbus, "notify::state",
G_CALLBACK (on_dbus_state_changed), self, 0);
on_dbus_state_changed (G_OBJECT (self->dbus), NULL, self);
wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0);
}
static void
wp_portal_permissionstore_plugin_disable (WpPlugin * plugin)
{
WpPortalPermissionStorePlugin *self =
WP_PORTAL_PERMISSIONSTORE_PLUGIN (plugin);
clear_signal (self);
if (self->dbus)
g_signal_handlers_disconnect_by_data (self->dbus, self);
g_clear_object (&self->dbus);
wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED);
}
static void
wp_portal_permissionstore_plugin_class_init (
WpPortalPermissionStorePluginClass * klass)
{
WpPluginClass *plugin_class = (WpPluginClass *) klass;
plugin_class->enable = wp_portal_permissionstore_plugin_enable;
plugin_class->disable = wp_portal_permissionstore_plugin_disable;
/**
* WpPortalPermissionStorePlugin::get-dbus:
*
* Returns: (transfer full): the dbus object
*/
signals[ACTION_GET_DBUS] = g_signal_new_class_handler (
"get-dbus", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
(GCallback) wp_portal_permissionstore_plugin_get_dbus,
NULL, NULL, NULL,
G_TYPE_OBJECT, 0);
/**
* WpPortalPermissionStorePlugin::lookup:
*
* @brief
* @em table: the table name
* @em id: the Id name
*
* Returns: (transfer full): the GVariant with permissions
*/
signals[ACTION_LOOKUP] = g_signal_new_class_handler (
"lookup", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
(GCallback) wp_portal_permissionstore_plugin_lookup,
NULL, NULL, NULL, G_TYPE_VARIANT,
2, G_TYPE_STRING, G_TYPE_STRING);
/**
* WpPortalPermissionStorePlugin::set:
*
* @brief
* @em table: the table name
* @em create: whether to create the table if it does not exist
* @em id: the Id name
* @em permissions: the permissions
*
* Sets the permissions in the permission store
*/
signals[ACTION_SET] = g_signal_new_class_handler (
"set", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
(GCallback) wp_portal_permissionstore_plugin_set,
NULL, NULL, NULL, G_TYPE_NONE,
4, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_VARIANT);
/**
* WpPortalPermissionStorePlugin::changed:
*
* @brief
* @em table: the table name
* @em id: the Id name
* @em deleted: whether the permission was deleted or not
* @em permissions: the GVariant with permissions
*
* Signaled when the permissions changed
*/
signals[SIGNAL_CHANGED] = g_signal_new (
"changed", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST, 0,
NULL, NULL, NULL, G_TYPE_NONE, 4,
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_VARIANT);
}
WP_PLUGIN_EXPORT GObject *
wireplumber__module_init (WpCore * core, WpSpaJson * args, GError ** error)
{
return G_OBJECT (g_object_new (
wp_portal_permissionstore_plugin_get_type(),
"name", "portal-permissionstore",
"core", core,
NULL));
}