summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilip Withnall <philip@tecnocode.co.uk>2024-05-07 14:30:22 +0000
committerPhilip Withnall <philip@tecnocode.co.uk>2024-05-07 14:30:22 +0000
commitd22c4574cd45b1395f4b76937c4b97c66f24742b (patch)
tree374cf2dbb15585480f8704e7f30dffe416fd75bc
parent26c590015ab1678e1ff5eeb4ab77810512bbe712 (diff)
parent7b15b1db4088aac26095d6e211c5b10375160d21 (diff)
downloadglib-d22c4574cd45b1395f4b76937c4b97c66f24742b.tar.gz
Merge branch '2.82-bus-name-owners' into 'main'
gdbusconnection: Don't deliver signals if the sender doesn't match Closes #3268 See merge request GNOME/glib!4038
-rw-r--r--gio/gdbusconnection.c701
-rw-r--r--gio/gdbusprivate.h5
-rw-r--r--gio/tests/gdbus-proxy.c6
-rw-r--r--gio/tests/gdbus-subscribe.c1342
-rw-r--r--gio/tests/meson.build4
5 files changed, 1925 insertions, 133 deletions
diff --git a/gio/gdbusconnection.c b/gio/gdbusconnection.c
index 9dd54d683..ee994cecc 100644
--- a/gio/gdbusconnection.c
+++ b/gio/gdbusconnection.c
@@ -287,6 +287,153 @@ call_destroy_notify (GMainContext *context,
/* ---------------------------------------------------------------------------------------------------- */
+typedef struct
+{
+ /* All fields are immutable after construction. */
+ gatomicrefcount ref_count;
+ GDBusSignalCallback callback;
+ gpointer user_data;
+ GDestroyNotify user_data_free_func;
+ guint id;
+ GMainContext *context;
+} SignalSubscriber;
+
+static SignalSubscriber *
+signal_subscriber_ref (SignalSubscriber *subscriber)
+{
+ g_atomic_ref_count_inc (&subscriber->ref_count);
+ return subscriber;
+}
+
+static void
+signal_subscriber_unref (SignalSubscriber *subscriber)
+{
+ if (g_atomic_ref_count_dec (&subscriber->ref_count))
+ {
+ /* Destroy the user data. It doesn’t matter which thread
+ * signal_subscriber_unref() is called in (or whether it’s called with a
+ * lock held), as call_destroy_notify() always defers to the next
+ * #GMainContext iteration. */
+ call_destroy_notify (subscriber->context,
+ subscriber->user_data_free_func,
+ subscriber->user_data);
+
+ g_main_context_unref (subscriber->context);
+ g_free (subscriber);
+ }
+}
+
+typedef struct
+{
+ /*
+ * 1 reference while waiting for GetNameOwner() to finish
+ * 1 reference for each SignalData that points to this one as its
+ * shared_name_watcher
+ */
+ grefcount ref_count;
+
+ gchar *owner;
+ guint32 get_name_owner_serial;
+} WatchedName;
+
+static WatchedName *
+watched_name_new (void)
+{
+ WatchedName *watched_name = g_new0 (WatchedName, 1);
+
+ g_ref_count_init (&watched_name->ref_count);
+ watched_name->owner = NULL;
+ return g_steal_pointer (&watched_name);
+}
+
+typedef struct SignalData SignalData;
+
+struct SignalData
+{
+ gchar *rule;
+ gchar *sender;
+ gchar *interface_name;
+ gchar *member;
+ gchar *object_path;
+ gchar *arg0;
+ GDBusSignalFlags flags;
+ GPtrArray *subscribers; /* (owned) (element-type SignalSubscriber) */
+
+ /*
+ * If the sender is a well-known name, this is an unowned SignalData
+ * representing the NameOwnerChanged signal that tracks its owner.
+ * NULL if sender is NULL.
+ * NULL if sender is its own owner (a unique name or DBUS_SERVICE_DBUS).
+ *
+ * Invariants: if not NULL, then
+ * shared_name_watcher->sender == DBUS_SERVICE_DBUS
+ * shared_name_watcher->interface_name == DBUS_INTERFACE_DBUS
+ * shared_name_watcher->member == "NameOwnerChanged"
+ * shared_name_watcher->object_path == DBUS_PATH_DBUS
+ * shared_name_watcher->arg0 == sender
+ * shared_name_watcher->flags == NONE
+ * shared_name_watcher->watched_name == NULL
+ */
+ SignalData *shared_name_watcher;
+
+ /*
+ * Non-NULL if this SignalData is another SignalData's shared_name_watcher.
+ * One reference for each SignalData that has this one as its
+ * shared_name_watcher.
+ * Otherwise NULL.
+ */
+ WatchedName *watched_name;
+};
+
+static SignalData *
+signal_data_new_take (gchar *rule,
+ gchar *sender,
+ gchar *interface_name,
+ gchar *member,
+ gchar *object_path,
+ gchar *arg0,
+ GDBusSignalFlags flags)
+{
+ SignalData *signal_data = g_new0 (SignalData, 1);
+
+ signal_data->rule = rule;
+ signal_data->sender = sender;
+ signal_data->interface_name = interface_name;
+ signal_data->member = member;
+ signal_data->object_path = object_path;
+ signal_data->arg0 = arg0;
+ signal_data->flags = flags;
+ signal_data->subscribers = g_ptr_array_new_with_free_func ((GDestroyNotify) signal_subscriber_unref);
+ return g_steal_pointer (&signal_data);
+}
+
+static void
+signal_data_free (SignalData *signal_data)
+{
+ /* The SignalData should not be freed while it still has subscribers */
+ g_assert (signal_data->subscribers->len == 0);
+
+ /* The SignalData should not be freed while it is watching for
+ * NameOwnerChanged on behalf of another SignalData */
+ g_assert (signal_data->watched_name == NULL);
+
+ /* The SignalData should be detached from its name watcher, if any,
+ * before it is freed */
+ g_assert (signal_data->shared_name_watcher == NULL);
+
+ g_free (signal_data->rule);
+ g_free (signal_data->sender);
+ g_free (signal_data->interface_name);
+ g_free (signal_data->member);
+ g_free (signal_data->object_path);
+ g_free (signal_data->arg0);
+ g_ptr_array_unref (signal_data->subscribers);
+
+ g_free (signal_data);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
#ifdef G_OS_WIN32
#define CONNECTION_ENSURE_LOCK(obj) do { ; } while (FALSE)
#else
@@ -405,6 +552,7 @@ struct _GDBusConnection
/* Map used for managing method replies, protected by @lock */
GHashTable *map_method_serial_to_task; /* guint32 -> owned GTask* */
+ GHashTable *map_method_serial_to_name_watcher; /* guint32 -> unowned SignalData* */
/* Maps used for managing signal subscription, protected by @lock */
GHashTable *map_rule_to_signal_data; /* match rule (gchar*) -> SignalData */
@@ -653,6 +801,7 @@ g_dbus_connection_finalize (GObject *object)
g_error_free (connection->initialization_error);
g_hash_table_unref (connection->map_method_serial_to_task);
+ g_hash_table_unref (connection->map_method_serial_to_name_watcher);
g_hash_table_unref (connection->map_rule_to_signal_data);
g_hash_table_unref (connection->map_id_to_signal_data);
@@ -1039,6 +1188,7 @@ g_dbus_connection_init (GDBusConnection *connection)
g_mutex_init (&connection->init_lock);
connection->map_method_serial_to_task = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
+ connection->map_method_serial_to_name_watcher = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, NULL);
connection->map_rule_to_signal_data = g_hash_table_new (g_str_hash,
g_str_equal);
@@ -2167,6 +2317,191 @@ g_dbus_connection_send_message_with_reply_sync (GDBusConnection *connecti
/* ---------------------------------------------------------------------------------------------------- */
+/*
+ * Called in any thread.
+ * Must hold the connection lock when calling this, unless
+ * connection->finalizing is TRUE.
+ */
+static void
+name_watcher_unref_watched_name (GDBusConnection *connection,
+ SignalData *name_watcher)
+{
+ WatchedName *watched_name = name_watcher->watched_name;
+
+ g_assert (watched_name != NULL);
+
+ if (!g_ref_count_dec (&watched_name->ref_count))
+ return;
+
+ /* Removing watched_name from the name_watcher may result in
+ * name_watcher being freed, so we must make sure name_watcher is no
+ * longer in map_method_serial_to_name_watcher.
+ *
+ * If we stop watching the name while our GetNameOwner call was still
+ * in-flight, then when the reply eventually arrives, we will not find
+ * its serial number in the map and harmlessly ignore it as a result. */
+ if (watched_name->get_name_owner_serial != 0)
+ g_hash_table_remove (connection->map_method_serial_to_name_watcher,
+ GUINT_TO_POINTER (watched_name->get_name_owner_serial));
+
+ name_watcher->watched_name = NULL;
+ g_free (watched_name->owner);
+ g_free (watched_name);
+}
+
+/* called in GDBusWorker thread with lock held */
+static void
+name_watcher_set_name_owner_unlocked (SignalData *name_watcher,
+ const char *new_owner)
+{
+ if (new_owner != NULL && new_owner[0] == '\0')
+ new_owner = NULL;
+
+ g_assert (name_watcher->watched_name != NULL);
+ g_set_str (&name_watcher->watched_name->owner, new_owner);
+}
+
+/* called in GDBusWorker thread with lock held */
+static void
+name_watcher_deliver_name_owner_changed_unlocked (SignalData *name_watcher,
+ GDBusMessage *message)
+{
+ GVariant *body;
+
+ body = g_dbus_message_get_body (message);
+
+ if (G_LIKELY (body != NULL && g_variant_is_of_type (body, G_VARIANT_TYPE ("(sss)"))))
+ {
+ const char *name;
+ const char *new_owner;
+
+ g_variant_get (body, "(&s&s&s)", &name, NULL, &new_owner);
+
+ /* Our caller already checked this */
+ g_assert (g_strcmp0 (name_watcher->arg0, name) == 0);
+
+ if (G_LIKELY (new_owner[0] == '\0' || g_dbus_is_unique_name (new_owner)))
+ name_watcher_set_name_owner_unlocked (name_watcher, new_owner);
+ else
+ g_warning ("Received NameOwnerChanged signal with invalid owner \"%s\" for \"%s\"",
+ new_owner, name);
+ }
+ else
+ {
+ g_warning ("Received NameOwnerChanged signal with unexpected "
+ "signature %s",
+ body == NULL ? "()" : g_variant_get_type_string (body));
+
+ }
+}
+
+/* called in GDBusWorker thread with lock held */
+static void
+name_watcher_deliver_get_name_owner_reply_unlocked (SignalData *name_watcher,
+ GDBusConnection *connection,
+ GDBusMessage *message)
+{
+ GDBusMessageType type;
+ GVariant *body;
+ WatchedName *watched_name;
+
+ watched_name = name_watcher->watched_name;
+ g_assert (watched_name != NULL);
+ g_assert (watched_name->get_name_owner_serial != 0);
+
+ type = g_dbus_message_get_message_type (message);
+ body = g_dbus_message_get_body (message);
+
+ if (type == G_DBUS_MESSAGE_TYPE_ERROR)
+ {
+ if (g_strcmp0 (g_dbus_message_get_error_name (message),
+ "org.freedesktop.DBus.Error.NameHasNoOwner"))
+ name_watcher_set_name_owner_unlocked (name_watcher, NULL);
+ /* else it's something like NoReply or AccessDenied, which tells
+ * us nothing - leave the owner set to whatever we most recently
+ * learned from NameOwnerChanged, or NULL */
+ }
+ else if (type != G_DBUS_MESSAGE_TYPE_METHOD_RETURN)
+ {
+ g_warning ("Received GetNameOwner reply with unexpected type %d",
+ type);
+ }
+ else if (G_LIKELY (body != NULL && g_variant_is_of_type (body, G_VARIANT_TYPE ("(s)"))))
+ {
+ const char *new_owner;
+
+ g_variant_get (body, "(&s)", &new_owner);
+
+ if (G_LIKELY (g_dbus_is_unique_name (new_owner)))
+ name_watcher_set_name_owner_unlocked (name_watcher, new_owner);
+ else
+ g_warning ("Received GetNameOwner reply with invalid owner \"%s\" for \"%s\"",
+ new_owner, name_watcher->arg0);
+ }
+ else
+ {
+ g_warning ("Received GetNameOwner reply with unexpected signature %s",
+ body == NULL ? "()" : g_variant_get_type_string (body));
+ }
+
+ g_hash_table_remove (connection->map_method_serial_to_name_watcher,
+ GUINT_TO_POINTER (watched_name->get_name_owner_serial));
+ watched_name->get_name_owner_serial = 0;
+}
+
+/* Called in a user thread, lock is held */
+static void
+name_watcher_call_get_name_owner_unlocked (GDBusConnection *connection,
+ SignalData *name_watcher)
+{
+ GDBusMessage *message;
+ GError *local_error = NULL;
+ WatchedName *watched_name;
+
+ g_assert (g_strcmp0 (name_watcher->sender, DBUS_SERVICE_DBUS) == 0);
+ g_assert (g_strcmp0 (name_watcher->interface_name, DBUS_INTERFACE_DBUS) == 0);
+ g_assert (g_strcmp0 (name_watcher->member, "NameOwnerChanged") == 0);
+ g_assert (g_strcmp0 (name_watcher->object_path, DBUS_PATH_DBUS) == 0);
+ /* arg0 of the NameOwnerChanged message is the well-known name whose owner
+ * we are interested in */
+ g_assert (g_dbus_is_name (name_watcher->arg0));
+ g_assert (name_watcher->flags == G_DBUS_SIGNAL_FLAGS_NONE);
+
+ watched_name = name_watcher->watched_name;
+ g_assert (watched_name != NULL);
+ g_assert (watched_name->owner == NULL);
+ g_assert (watched_name->get_name_owner_serial == 0);
+ g_assert (name_watcher->shared_name_watcher == NULL);
+
+ message = g_dbus_message_new_method_call (DBUS_SERVICE_DBUS,
+ DBUS_PATH_DBUS,
+ DBUS_INTERFACE_DBUS,
+ "GetNameOwner");
+ g_dbus_message_set_body (message, g_variant_new ("(s)", name_watcher->arg0));
+
+ if (g_dbus_connection_send_message_unlocked (connection, message,
+ G_DBUS_SEND_MESSAGE_FLAGS_NONE,
+ &watched_name->get_name_owner_serial,
+ &local_error))
+ {
+ g_assert (watched_name->get_name_owner_serial != 0);
+ g_hash_table_insert (connection->map_method_serial_to_name_watcher,
+ GUINT_TO_POINTER (watched_name->get_name_owner_serial),
+ name_watcher);
+ }
+ else
+ {
+ g_critical ("Error while sending GetNameOwner() message: %s",
+ local_error->message);
+ g_clear_error (&local_error);
+ g_assert (watched_name->get_name_owner_serial == 0);
+ }
+
+ g_object_unref (message);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
typedef struct
{
guint id;
@@ -2290,6 +2625,7 @@ on_worker_message_received (GDBusWorker *worker,
{
guint32 reply_serial;
GTask *task;
+ SignalData *name_watcher;
reply_serial = g_dbus_message_get_reply_serial (message);
CONNECTION_LOCK (connection);
@@ -2305,6 +2641,19 @@ on_worker_message_received (GDBusWorker *worker,
{
//g_debug ("message reply/error for serial %d but no SendMessageData found for %p", reply_serial, connection);
}
+
+ name_watcher = g_hash_table_lookup (connection->map_method_serial_to_name_watcher,
+ GUINT_TO_POINTER (reply_serial));
+
+ if (name_watcher != NULL)
+ {
+ g_assert (name_watcher->watched_name != NULL);
+ g_assert (name_watcher->watched_name->get_name_owner_serial == reply_serial);
+ name_watcher_deliver_get_name_owner_reply_unlocked (name_watcher,
+ connection,
+ message);
+ }
+
CONNECTION_UNLOCK (connection);
}
else if (message_type == G_DBUS_MESSAGE_TYPE_SIGNAL)
@@ -3248,69 +3597,6 @@ g_dbus_connection_remove_filter (GDBusConnection *connection,
/* ---------------------------------------------------------------------------------------------------- */
-typedef struct
-{
- gchar *rule;
- gchar *sender;
- gchar *sender_unique_name; /* if sender is unique or org.freedesktop.DBus, then that name... otherwise blank */
- gchar *interface_name;
- gchar *member;
- gchar *object_path;
- gchar *arg0;
- GDBusSignalFlags flags;
- GPtrArray *subscribers; /* (owned) (element-type SignalSubscriber) */
-} SignalData;
-
-static void
-signal_data_free (SignalData *signal_data)
-{
- g_free (signal_data->rule);
- g_free (signal_data->sender);
- g_free (signal_data->sender_unique_name);
- g_free (signal_data->interface_name);
- g_free (signal_data->member);
- g_free (signal_data->object_path);
- g_free (signal_data->arg0);
- g_ptr_array_unref (signal_data->subscribers);
- g_free (signal_data);
-}
-
-typedef struct
-{
- /* All fields are immutable after construction. */
- gatomicrefcount ref_count;
- GDBusSignalCallback callback;
- gpointer user_data;
- GDestroyNotify user_data_free_func;
- guint id;
- GMainContext *context;
-} SignalSubscriber;
-
-static SignalSubscriber *
-signal_subscriber_ref (SignalSubscriber *subscriber)
-{
- g_atomic_ref_count_inc (&subscriber->ref_count);
- return subscriber;
-}
-
-static void
-signal_subscriber_unref (SignalSubscriber *subscriber)
-{
- if (g_atomic_ref_count_dec (&subscriber->ref_count))
- {
- /* Destroy the user data. It doesn’t matter which thread
- * signal_subscriber_unref() is called in (or whether it’s called with a
- * lock held), as call_destroy_notify() always defers to the next
- * #GMainContext iteration. */
- call_destroy_notify (subscriber->context,
- subscriber->user_data_free_func,
- subscriber->user_data);
-
- g_main_context_unref (subscriber->context);
- g_free (subscriber);
- }
-}
-
static gchar *
args_to_rule (const gchar *sender,
const gchar *interface_name,
@@ -3422,7 +3708,7 @@ remove_match_rule (GDBusConnection *connection,
static gboolean
is_signal_data_for_name_lost_or_acquired (SignalData *signal_data)
{
- return g_strcmp0 (signal_data->sender_unique_name, "org.freedesktop.DBus") == 0 &&
+ return g_strcmp0 (signal_data->sender, "org.freedesktop.DBus") == 0 &&
g_strcmp0 (signal_data->interface_name, "org.freedesktop.DBus") == 0 &&
g_strcmp0 (signal_data->object_path, "/org/freedesktop/DBus") == 0 &&
(g_strcmp0 (signal_data->member, "NameLost") == 0 ||
@@ -3431,6 +3717,43 @@ is_signal_data_for_name_lost_or_acquired (SignalData *signal_data)
/* ---------------------------------------------------------------------------------------------------- */
+/* called in any thread, connection lock is held */
+static void
+add_signal_data (GDBusConnection *connection,
+ SignalData *signal_data,
+ const char *sender_unique_name)
+{
+ GPtrArray *signal_data_array;
+
+ g_hash_table_insert (connection->map_rule_to_signal_data,
+ signal_data->rule,
+ signal_data);
+
+ /* Add the match rule to the bus...
+ *
+ * Avoid adding match rules for NameLost and NameAcquired messages - the bus will
+ * always send such messages to us.
+ */
+ if (connection->flags & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION)
+ {
+ if (!is_signal_data_for_name_lost_or_acquired (signal_data))
+ add_match_rule (connection, signal_data->rule);
+ }
+
+ signal_data_array = g_hash_table_lookup (connection->map_sender_unique_name_to_signal_data_array,
+ sender_unique_name);
+ if (signal_data_array == NULL)
+ {
+ signal_data_array = g_ptr_array_new ();
+ g_hash_table_insert (connection->map_sender_unique_name_to_signal_data_array,
+ g_strdup (sender_unique_name),
+ signal_data_array);
+ }
+ g_ptr_array_add (signal_data_array, signal_data);
+}
+
+/* ---------------------------------------------------------------------------------------------------- */
+
/**
* g_dbus_connection_signal_subscribe:
* @connection: a #GDBusConnection
@@ -3519,8 +3842,9 @@ g_dbus_connection_signal_subscribe (GDBusConnection *connection,
{
gchar *rule;
SignalData *signal_data;
+ SignalData *name_watcher = NULL;
SignalSubscriber *subscriber;
- GPtrArray *signal_data_array;
+ gboolean sender_is_its_own_owner;
const gchar *sender_unique_name;
/* Right now we abort if AddMatch() fails since it can only fail with the bus being in
@@ -3556,6 +3880,11 @@ g_dbus_connection_signal_subscribe (GDBusConnection *connection,
rule = args_to_rule (sender, interface_name, member, object_path, arg0, flags);
if (sender != NULL && (g_dbus_is_unique_name (sender) || g_strcmp0 (sender, "org.freedesktop.DBus") == 0))
+ sender_is_its_own_owner = TRUE;
+ else
+ sender_is_its_own_owner = FALSE;
+
+ if (sender_is_its_own_owner)
sender_unique_name = sender;
else
sender_unique_name = "";
@@ -3577,43 +3906,62 @@ g_dbus_connection_signal_subscribe (GDBusConnection *connection,
goto out;
}
- signal_data = g_new0 (SignalData, 1);
- signal_data->rule = rule;
- signal_data->sender = g_strdup (sender);
- signal_data->sender_unique_name = g_strdup (sender_unique_name);
- signal_data->interface_name = g_strdup (interface_name);
- signal_data->member = g_strdup (member);
- signal_data->object_path = g_strdup (object_path);
- signal_data->arg0 = g_strdup (arg0);
- signal_data->flags = flags;
- signal_data->subscribers = g_ptr_array_new_with_free_func ((GDestroyNotify) signal_subscriber_unref);
+ signal_data = signal_data_new_take (g_steal_pointer (&rule),
+ g_strdup (sender),
+ g_strdup (interface_name),
+ g_strdup (member),
+ g_strdup (object_path),
+ g_strdup (arg0),
+ flags);
g_ptr_array_add (signal_data->subscribers, subscriber);
- g_hash_table_insert (connection->map_rule_to_signal_data,
- signal_data->rule,
- signal_data);
-
- /* Add the match rule to the bus...
- *
- * Avoid adding match rules for NameLost and NameAcquired messages - the bus will
- * always send such messages to us.
- */
- if (connection->flags & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION)
+ /* If subscribing to a signal from a specific sender with a well-known
+ * name, we must first subscribe to NameOwnerChanged signals for that
+ * well-known name, so that we can match the current owner of the name
+ * against the sender of each signal. */
+ if (sender != NULL && !sender_is_its_own_owner)
{
- if (!is_signal_data_for_name_lost_or_acquired (signal_data))
- add_match_rule (connection, signal_data->rule);
- }
+ gchar *name_owner_rule = NULL;
- signal_data_array = g_hash_table_lookup (connection->map_sender_unique_name_to_signal_data_array,
- signal_data->sender_unique_name);
- if (signal_data_array == NULL)
- {
- signal_data_array = g_ptr_array_new ();
- g_hash_table_insert (connection->map_sender_unique_name_to_signal_data_array,
- g_strdup (signal_data->sender_unique_name),
- signal_data_array);
+ /* We already checked that sender != NULL implies MESSAGE_BUS_CONNECTION */
+ g_assert (connection->flags & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION);
+
+ name_owner_rule = args_to_rule (DBUS_SERVICE_DBUS,
+ DBUS_INTERFACE_DBUS,
+ "NameOwnerChanged",
+ DBUS_PATH_DBUS,
+ sender,
+ G_DBUS_SIGNAL_FLAGS_NONE);
+ name_watcher = g_hash_table_lookup (connection->map_rule_to_signal_data, name_owner_rule);
+
+ if (name_watcher == NULL)
+ {
+ name_watcher = signal_data_new_take (g_steal_pointer (&name_owner_rule),
+ g_strdup (DBUS_SERVICE_DBUS),
+ g_strdup (DBUS_INTERFACE_DBUS),
+ g_strdup ("NameOwnerChanged"),
+ g_strdup (DBUS_PATH_DBUS),
+ g_strdup (sender),
+ G_DBUS_SIGNAL_FLAGS_NONE);
+ add_signal_data (connection, name_watcher, DBUS_SERVICE_DBUS);
+ }
+
+ if (name_watcher->watched_name == NULL)
+ {
+ name_watcher->watched_name = watched_name_new ();
+ name_watcher_call_get_name_owner_unlocked (connection, name_watcher);
+ }
+ else
+ {
+ g_ref_count_inc (&name_watcher->watched_name->ref_count);
+ }
+
+ signal_data->shared_name_watcher = name_watcher;
+
+ g_clear_pointer (&name_owner_rule, g_free);
}
- g_ptr_array_add (signal_data_array, signal_data);
+
+ add_signal_data (connection, signal_data, sender_unique_name);
out:
g_hash_table_insert (connection->map_id_to_signal_data,
@@ -3627,6 +3975,75 @@ g_dbus_connection_signal_subscribe (GDBusConnection *connection,
/* ---------------------------------------------------------------------------------------------------- */
+/*
+ * Called in any thread.
+ * Must hold the connection lock when calling this, unless
+ * connection->finalizing is TRUE.
+ * May free signal_data, so do not dereference it after this.
+ */
+static void
+remove_signal_data_if_unused (GDBusConnection *connection,
+ SignalData *signal_data)
+{
+ const gchar *sender_unique_name;
+ GPtrArray *signal_data_array;
+
+ /* Cannot remove while there are still subscribers */
+ if (signal_data->subscribers->len != 0)
+ return;
+
+ /* Cannot remove while another SignalData is still using this one
+ * as its shared_name_watcher, which holds watched_name->ref_count > 0 */
+ if (signal_data->watched_name != NULL)
+ return;
+
+ /* Point of no return: we have committed to removing it */
+
+ if (signal_data->sender != NULL && signal_data->shared_name_watcher == NULL)
+ sender_unique_name = signal_data->sender;
+ else
+ sender_unique_name = "";
+
+ g_warn_if_fail (g_hash_table_remove (connection->map_rule_to_signal_data, signal_data->rule));
+
+ signal_data_array = g_hash_table_lookup (connection->map_sender_unique_name_to_signal_data_array,
+ sender_unique_name);
+ g_warn_if_fail (signal_data_array != NULL);
+ g_warn_if_fail (g_ptr_array_remove (signal_data_array, signal_data));
+
+ if (signal_data_array->len == 0)
+ {
+ g_warn_if_fail (g_hash_table_remove (connection->map_sender_unique_name_to_signal_data_array,
+ sender_unique_name));
+ }
+
+ /* remove the match rule from the bus unless NameLost or NameAcquired (see subscribe()) */
+ if ((connection->flags & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) &&
+ !is_signal_data_for_name_lost_or_acquired (signal_data) &&
+ !g_dbus_connection_is_closed (connection) &&
+ !connection->finalizing)
+ {
+ /* The check for g_dbus_connection_is_closed() means that
+ * sending the RemoveMatch message can't fail with
+ * G_IO_ERROR_CLOSED, because we're holding the lock,
+ * so on_worker_closed() can't happen between the check we just
+ * did, and releasing the lock later.
+ */
+ remove_match_rule (connection, signal_data->rule);
+ }
+
+ if (signal_data->shared_name_watcher != NULL)
+ {
+ SignalData *name_watcher = g_steal_pointer (&signal_data->shared_name_watcher);
+
+ name_watcher_unref_watched_name (connection, name_watcher);
+ /* May free signal_data */
+ remove_signal_data_if_unused (connection, name_watcher);
+ }
+
+ signal_data_free (signal_data);
+}
+
/* called in any thread */
/* must hold lock when calling this (except if connection->finalizing is TRUE)
* returns the number of removed subscribers */
@@ -3635,7 +4052,6 @@ unsubscribe_id_internal (GDBusConnection *connection,
guint subscription_id)
{
SignalData *signal_data;
- GPtrArray *signal_data_array;
guint n;
guint n_removed = 0;
@@ -3662,40 +4078,8 @@ unsubscribe_id_internal (GDBusConnection *connection,
GUINT_TO_POINTER (subscription_id)));
n_removed++;
g_ptr_array_remove_index_fast (signal_data->subscribers, n);
-
- if (signal_data->subscribers->len == 0)
- {
- g_warn_if_fail (g_hash_table_remove (connection->map_rule_to_signal_data, signal_data->rule));
-
- signal_data_array = g_hash_table_lookup (connection->map_sender_unique_name_to_signal_data_array,
- signal_data->sender_unique_name);
- g_warn_if_fail (signal_data_array != NULL);
- g_warn_if_fail (g_ptr_array_remove (signal_data_array, signal_data));
-
- if (signal_data_array->len == 0)
- {
- g_warn_if_fail (g_hash_table_remove (connection->map_sender_unique_name_to_signal_data_array,
- signal_data->sender_unique_name));
- }
-
- /* remove the match rule from the bus unless NameLost or NameAcquired (see subscribe()) */
- if ((connection->flags & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) &&
- !is_signal_data_for_name_lost_or_acquired (signal_data) &&
- !g_dbus_connection_is_closed (connection) &&
- !connection->finalizing)
- {
- /* The check for g_dbus_connection_is_closed() means that
- * sending the RemoveMatch message can't fail with
- * G_IO_ERROR_CLOSED, because we're holding the lock,
- * so on_worker_closed() can't happen between the check we just
- * did, and releasing the lock later.
- */
- remove_match_rule (connection, signal_data->rule);
- }
-
- signal_data_free (signal_data);
- }
-
+ /* May free signal_data */
+ remove_signal_data_if_unused (connection, signal_data);
goto out;
}
@@ -3916,6 +4300,46 @@ schedule_callbacks (GDBusConnection *connection,
if (signal_data->object_path != NULL && g_strcmp0 (signal_data->object_path, path) != 0)
continue;
+ if (signal_data->shared_name_watcher != NULL)
+ {
+ /* We want signals from a specified well-known name, which means
+ * the signal's sender needs to be the unique name that currently
+ * owns that well-known name, and we will have found this
+ * SignalData in
+ * connection->map_sender_unique_name_to_signal_data_array[""]. */
+ const WatchedName *watched_name;
+ const char *current_owner;
+
+ g_assert (signal_data->sender != NULL);
+ /* Invariant: We never need to watch for the owner of a unique
+ * name, or for the owner of DBUS_SERVICE_DBUS, either of which
+ * is always its own owner */
+ g_assert (!g_dbus_is_unique_name (signal_data->sender));
+ g_assert (g_strcmp0 (signal_data->sender, DBUS_SERVICE_DBUS) != 0);
+
+ watched_name = signal_data->shared_name_watcher->watched_name;
+ g_assert (watched_name != NULL);
+ current_owner = watched_name->owner;
+
+ /* Skip the signal if the actual sender is not known to own
+ * the required name */
+ if (current_owner == NULL || g_strcmp0 (current_owner, sender) != 0)
+ continue;
+ }
+ else if (signal_data->sender != NULL)
+ {
+ /* We want signals from a unique name or o.fd.DBus... */
+ g_assert (g_dbus_is_unique_name (signal_data->sender)
+ || g_str_equal (signal_data->sender, DBUS_SERVICE_DBUS));
+
+ /* ... which means we must have found this SignalData in
+ * connection->map_sender_unique_name_to_signal_data_array[signal_data->sender],
+ * therefore we would only have found it if the signal's
+ * actual sender matches the required signal_data->sender */
+ g_assert (g_strcmp0 (signal_data->sender, sender) == 0);
+ }
+ /* else the sender is unspecified and we will accept anything */
+
if (signal_data->arg0 != NULL)
{
if (signal_data->flags & G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE)
@@ -3933,6 +4357,17 @@ schedule_callbacks (GDBusConnection *connection,
continue;
}
+ if (signal_data->watched_name != NULL)
+ {
+ /* Invariant: SignalData should only have a watched_name if it
+ * represents the NameOwnerChanged signal */
+ g_assert (g_strcmp0 (sender, DBUS_SERVICE_DBUS) == 0);
+ g_assert (g_strcmp0 (interface, DBUS_INTERFACE_DBUS) == 0);
+ g_assert (g_strcmp0 (path, DBUS_PATH_DBUS) == 0);
+ g_assert (g_strcmp0 (member, "NameOwnerChanged") == 0);
+ name_watcher_deliver_name_owner_changed_unlocked (signal_data, message);
+ }
+
for (m = 0; m < signal_data->subscribers->len; m++)
{
SignalSubscriber *subscriber = signal_data->subscribers->pdata[m];
@@ -4004,7 +4439,7 @@ distribute_signals (GDBusConnection *connection,
schedule_callbacks (connection, signal_data_array, message, sender);
}
- /* collect subscribers not matching on sender */
+ /* collect subscribers not matching on sender, or matching a well-known name */
signal_data_array = g_hash_table_lookup (connection->map_sender_unique_name_to_signal_data_array, "");
if (signal_data_array != NULL)
schedule_callbacks (connection, signal_data_array, message, sender);
diff --git a/gio/gdbusprivate.h b/gio/gdbusprivate.h
index e7a5bfa4f..57147e172 100644
--- a/gio/gdbusprivate.h
+++ b/gio/gdbusprivate.h
@@ -27,6 +27,11 @@
G_BEGIN_DECLS
+/* Bus name, interface and object path of the message bus itself */
+#define DBUS_SERVICE_DBUS "org.freedesktop.DBus"
+#define DBUS_INTERFACE_DBUS DBUS_SERVICE_DBUS
+#define DBUS_PATH_DBUS "/org/freedesktop/DBus"
+
/* ---------------------------------------------------------------------------------------------------- */
typedef struct GDBusWorker GDBusWorker;
diff --git a/gio/tests/gdbus-proxy.c b/gio/tests/gdbus-proxy.c
index ac5f720fa..ab36eae0e 100644
--- a/gio/tests/gdbus-proxy.c
+++ b/gio/tests/gdbus-proxy.c
@@ -780,6 +780,12 @@ kill_test_service (GDBusConnection *connection)
while (!name_disappeared)
g_main_context_iteration (NULL, TRUE);
+ /* GDBusConnection doesn't guarantee that different subscriptions to the
+ * same signal will get their callbacks scheduled in any particular order,
+ * so make sure they have all happened */
+ while (g_main_context_iteration (NULL, FALSE))
+ continue;
+
g_bus_unwatch_name (watch_id);
#else
g_warning ("Can't kill com.example.TestService");
diff --git a/gio/tests/gdbus-subscribe.c b/gio/tests/gdbus-subscribe.c
new file mode 100644
index 000000000..4cba4f565
--- /dev/null
+++ b/gio/tests/gdbus-subscribe.c
@@ -0,0 +1,1342 @@
+/*
+ * Copyright 2024 Collabora Ltd.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <gio/gio.h>
+
+#include "gdbus-tests.h"
+
+/* From the D-Bus Specification */
+#define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1
+
+#define DBUS_SERVICE_DBUS "org.freedesktop.DBus"
+#define DBUS_PATH_DBUS "/org/freedesktop/DBus"
+#define DBUS_INTERFACE_DBUS DBUS_SERVICE_DBUS
+#define NAME_OWNER_CHANGED "NameOwnerChanged"
+
+/* A signal that each connection emits to indicate that it has finished
+ * emitting other signals */
+#define FINISHED_PATH "/org/gtk/Test/Finished"
+#define FINISHED_INTERFACE "org.gtk.Test.Finished"
+#define FINISHED_SIGNAL "Finished"
+
+/* A signal emitted during testing */
+#define EXAMPLE_PATH "/org/gtk/GDBus/ExampleInterface"
+#define EXAMPLE_INTERFACE "org.gtk.GDBus.ExampleInterface"
+#define FOO_SIGNAL "Foo"
+
+#define ALREADY_OWNED_NAME "org.gtk.Test.AlreadyOwned"
+#define OWNED_LATER_NAME "org.gtk.Test.OwnedLater"
+
+/* Log @s in a debug message. */
+static inline const char *
+nonnull (const char *s,
+ const char *if_null)
+{
+ return (s == NULL) ? if_null : s;
+}
+
+typedef enum
+{
+ TEST_CONN_NONE,
+ TEST_CONN_FIRST,
+ /* A connection that subscribes to signals */
+ TEST_CONN_SUBSCRIBER = TEST_CONN_FIRST,
+ /* A mockup of a legitimate service */
+ TEST_CONN_SERVICE,
+ /* A mockup of a second legitimate service */
+ TEST_CONN_SERVICE2,
+ /* A connection that tries to trick @subscriber into processing its signals
+ * as if they came from @service */
+ TEST_CONN_ATTACKER,
+ NUM_TEST_CONNS
+} TestConn;
+
+static const char * const test_conn_descriptions[NUM_TEST_CONNS] =
+{
+ "(unused)",
+ "subscriber",
+ "service",
+ "service 2",
+ "attacker"
+};
+
+typedef enum
+{
+ SUBSCRIPTION_MODE_CONN,
+ SUBSCRIPTION_MODE_PROXY,
+ SUBSCRIPTION_MODE_PARALLEL
+} SubscriptionMode;
+
+typedef struct
+{
+ GDBusProxy *received_by_proxy;
+ TestConn sender;
+ char *path;
+ char *iface;
+ char *member;
+ GVariant *parameters;
+ char *arg0;
+ guint32 step;
+} ReceivedMessage;
+
+static void
+received_message_free (ReceivedMessage *self)
+{
+
+ g_clear_object (&self->received_by_proxy);
+ g_free (self->path);
+ g_free (self->iface);
+ g_free (self->member);
+ g_clear_pointer (&self->parameters, g_variant_unref);
+ g_free (self->arg0);
+ g_free (self);
+}
+
+typedef struct
+{
+ TestConn sender;
+ TestConn unicast_to;
+ const char *path;
+ const char *iface;
+ const char *member;
+ const char *arg0;
+ const char *args;
+ guint received_by_conn;
+ guint received_by_proxy;
+} TestEmitSignal;
+
+typedef struct
+{
+ const char *string_sender;
+ TestConn unique_sender;
+ const char *path;
+ const char *iface;
+ const char *member;
+ const char *arg0;
+ GDBusSignalFlags flags;
+ gboolean unsubscribe_immediately;
+} TestSubscribe;
+
+typedef struct
+{
+ const char *name;
+ TestConn owner;
+ guint received_by_conn;
+ guint received_by_proxy;
+} TestOwnName;
+
+typedef enum
+{
+ TEST_ACTION_NONE = 0,
+ TEST_ACTION_SUBSCRIBE,
+ TEST_ACTION_EMIT_SIGNAL,
+ TEST_ACTION_OWN_NAME,
+} TestAction;
+
+typedef struct
+{
+ TestAction action;
+ union {
+ TestEmitSignal signal;
+ TestSubscribe subscribe;
+ TestOwnName own_name;
+ guint unsubscribe_undo_step;
+ } u;
+} TestStep;
+
+/* Arbitrary, extend as necessary to accommodate the longest test */
+#define MAX_TEST_STEPS 10
+
+typedef struct
+{
+ const char *description;
+ TestStep steps[MAX_TEST_STEPS];
+} TestPlan;
+
+static const TestPlan plan_simple =
+{
+ .description = "A broadcast is only received after subscribing to it",
+ .steps = {
+ {
+ /* We don't receive a signal if we haven't subscribed yet */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* Now it works */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ /* The proxy can't be used in this case, because it needs
+ * a bus name to subscribe to */
+ .received_by_proxy = 0
+ },
+ },
+ },
+};
+
+static const TestPlan plan_broadcast_from_anyone =
+{
+ .description = "A subscription with NULL sender accepts broadcast and unicast",
+ .steps = {
+ {
+ /* Subscriber wants to receive signals from anyone */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* First service sends a broadcast */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Second service also sends a broadcast */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE2,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* First service sends a unicast signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Second service also sends a unicast signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE2,
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 0
+ },
+ },
+ },
+};
+
+static const TestPlan plan_match_twice =
+{
+ .description = "A message matching more than one subscription is received "
+ "once per subscription",
+ .steps = {
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .unique_sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .path = EXAMPLE_PATH,
+ },
+ },
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .unique_sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 4,
+ /* Only the first and last work with GDBusProxy */
+ .received_by_proxy = 2
+ },
+ },
+ },
+};
+
+static const TestPlan plan_limit_by_unique_name =
+{
+ .description = "A subscription via a unique name only accepts messages "
+ "sent by that same unique name",
+ .steps = {
+ {
+ /* Subscriber wants to receive signals from service */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .unique_sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* Attacker wants to trick subscriber into thinking that service
+ * sent a signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Attacker tries harder, by sending a signal unicast directly to
+ * the subscriber */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* When the real service sends a signal, it should still get through */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 1
+ },
+ },
+ },
+};
+
+static const TestPlan plan_nonexistent_unique_name =
+{
+ .description = "A subscription via a unique name that doesn't exist "
+ "accepts no messages",
+ .steps = {
+ {
+ /* Subscriber wants to receive signals from service */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ /* This relies on the implementation detail that the dbus-daemon
+ * (and presumably other bus implementations) never actually generates
+ * a unique name in this format */
+ .string_sender = ":0.this.had.better.not.exist",
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* Attacker wants to trick subscriber into thinking that service
+ * sent a signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Attacker tries harder, by sending a signal unicast directly to
+ * the subscriber */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ },
+};
+
+static const TestPlan plan_limit_by_well_known_name =
+{
+ .description = "A subscription via a well-known name only accepts messages "
+ "sent by the owner of that well-known name",
+ .steps = {
+ {
+ /* Service already owns one name */
+ .action = TEST_ACTION_OWN_NAME,
+ .u.own_name = {
+ .name = ALREADY_OWNED_NAME,
+ .owner = TEST_CONN_SERVICE
+ },
+ },
+ {
+ /* Subscriber wants to receive signals from service */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .string_sender = ALREADY_OWNED_NAME,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* Subscriber wants to receive signals from service by another name */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .string_sender = OWNED_LATER_NAME,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ },
+ },
+ {
+ /* Attacker wants to trick subscriber into thinking that service
+ * sent a signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Attacker tries harder, by sending a signal unicast directly to
+ * the subscriber */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* When the service sends a signal with the name it already owns,
+ * it should get through */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 1,
+ .received_by_proxy = 1
+ },
+ },
+ {
+ /* Service claims another name */
+ .action = TEST_ACTION_OWN_NAME,
+ .u.own_name = {
+ .name = OWNED_LATER_NAME,
+ .owner = TEST_CONN_SERVICE
+ },
+ },
+ {
+ /* Now the subscriber gets this signal twice, once for each
+ * subscription; and similarly each of the two proxies gets this
+ * signal twice */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 2,
+ .received_by_proxy = 2
+ },
+ },
+ },
+};
+
+static const TestPlan plan_unsubscribe_immediately =
+{
+ .description = "Unsubscribing before GetNameOwner can return doesn't result in a crash",
+ .steps = {
+ {
+ /* Service already owns one name */
+ .action = TEST_ACTION_OWN_NAME,
+ .u.own_name = {
+ .name = ALREADY_OWNED_NAME,
+ .owner = TEST_CONN_SERVICE
+ },
+ },
+ {
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .string_sender = ALREADY_OWNED_NAME,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .unsubscribe_immediately = TRUE
+ },
+ },
+ {
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_SERVICE,
+ .path = EXAMPLE_PATH,
+ .iface = EXAMPLE_INTERFACE,
+ .member = FOO_SIGNAL,
+ .received_by_conn = 0,
+ /* The proxy can't unsubscribe, except by destroying the proxy
+ * completely, which we don't currently implement in this test */
+ .received_by_proxy = 1
+ },
+ },
+ },
+};
+
+static const TestPlan plan_limit_to_message_bus =
+{
+ .description = "A subscription to the message bus only accepts messages "
+ "from the message bus",
+ .steps = {
+ {
+ /* Subscriber wants to receive signals from the message bus itself */
+ .action = TEST_ACTION_SUBSCRIBE,
+ .u.subscribe = {
+ .string_sender = DBUS_SERVICE_DBUS,
+ .path = DBUS_PATH_DBUS,
+ .iface = DBUS_INTERFACE_DBUS,
+ },
+ },
+ {
+ /* Attacker wants to trick subscriber into thinking that the message
+ * bus sent a signal */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .sender = TEST_CONN_ATTACKER,
+ .path = DBUS_PATH_DBUS,
+ .iface = DBUS_INTERFACE_DBUS,
+ .member = NAME_OWNER_CHANGED,
+ .arg0 = "would I lie to you?",
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* Attacker tries harder, by sending a signal unicast directly to
+ * the subscriber, and using more realistic arguments */
+ .action = TEST_ACTION_EMIT_SIGNAL,
+ .u.signal = {
+ .unicast_to = TEST_CONN_SUBSCRIBER,
+ .sender = TEST_CONN_ATTACKER,
+ .path = DBUS_PATH_DBUS,
+ .iface = DBUS_INTERFACE_DBUS,
+ .member = NAME_OWNER_CHANGED,
+ .args = "('com.example.Name', '', ':1.12')",
+ .received_by_conn = 0,
+ .received_by_proxy = 0
+ },
+ },
+ {
+ /* When the message bus sends a signal (in this case triggered by
+ * owning a name), it should still get through */
+ .action = TEST_ACTION_OWN_NAME,
+ .u.own_name = {
+ .name = OWNED_LATER_NAME,
+ .owner = TEST_CONN_SERVICE,
+ .received_by_conn = 1,
+ .received_by_proxy = 1
+ },
+ },
+ },
+};
+
+typedef struct
+{
+ const TestPlan *plan;
+ SubscriptionMode mode;
+ GError *error;
+ /* (element-type ReceivedMessage) */
+ GPtrArray *received;
+ /* conns[TEST_CONN_NONE] is unused and remains NULL */
+ GDBusConnection *conns[NUM_TEST_CONNS];
+ /* Proxies on conns[TEST_CONN_SUBSCRIBER] */
+ GPtrArray *proxies;
+ /* unique_names[TEST_CONN_NONE] is unused and remains NULL */
+ const char *unique_names[NUM_TEST_CONNS];
+ /* finished[TEST_CONN_NONE] is unused and remains FALSE */
+ gboolean finished[NUM_TEST_CONNS];
+ /* Remains 0 for any step that is not a subscription */
+ guint subscriptions[MAX_TEST_STEPS];
+ /* Number of times the signal from step n was received */
+ guint received_by_conn[MAX_TEST_STEPS];
+ /* Number of times the signal from step n was received */
+ guint received_by_proxy[MAX_TEST_STEPS];
+ guint finished_subscription;
+} Fixture;
+
+/* Wait for asynchronous messages from @conn to have been processed
+ * by the message bus, as a sequence point so that we can make
+ * "happens before" and "happens after" assertions relative to this.
+ * The easiest way to achieve this is to call a message bus method that has
+ * no arguments and wait for it to return: because the message bus processes
+ * messages in-order, anything we sent before this must have been processed
+ * by the time this call arrives. */
+static void
+connection_wait_for_bus (GDBusConnection *conn)
+{
+ GError *error = NULL;
+ GVariant *call_result;
+
+ call_result = g_dbus_connection_call_sync (conn,
+ DBUS_SERVICE_DBUS,
+ DBUS_PATH_DBUS,
+ DBUS_INTERFACE_DBUS,
+ "GetId",
+ NULL, /* arguments */
+ NULL, /* result type */
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (call_result);
+ g_variant_unref (call_result);
+}
+
+/*
+ * Called when the subscriber receives a message from any connection
+ * announcing that it has emitted all the signals that it plans to emit.
+ */
+static void
+subscriber_finished_cb (GDBusConnection *conn,
+ const char *sender_name,
+ const char *path,
+ const char *iface,
+ const char *member,
+ GVariant *parameters,
+ void *user_data)
+{
+ Fixture *f = user_data;
+ GDBusConnection *subscriber = f->conns[TEST_CONN_SUBSCRIBER];
+ guint i;
+
+ g_assert_true (conn == subscriber);
+
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ {
+ if (g_str_equal (sender_name, f->unique_names[i]))
+ {
+ g_assert_false (f->finished[i]);
+ f->finished[i] = TRUE;
+
+ g_test_message ("Received Finished signal from %s %s",
+ test_conn_descriptions[i], sender_name);
+ return;
+ }
+ }
+
+ g_error ("Received Finished signal from unknown sender %s", sender_name);
+}
+
+/*
+ * Called when we receive a signal, either via the GDBusProxy (proxy != NULL)
+ * or via the GDBusConnection (proxy == NULL).
+ */
+static void
+fixture_received_signal (Fixture *f,
+ GDBusProxy *proxy,
+ const char *sender_name,
+ const char *path,
+ const char *iface,
+ const char *member,
+ GVariant *parameters)
+{
+ guint i;
+ ReceivedMessage *received;
+
+ /* Ignore the Finished signal if it matches a wildcard subscription */
+ if (g_str_equal (member, FINISHED_SIGNAL))
+ return;
+
+ received = g_new0 (ReceivedMessage, 1);
+
+ if (proxy != NULL)
+ received->received_by_proxy = g_object_ref (proxy);
+ else
+ received->received_by_proxy = NULL;
+
+ received->path = g_strdup (path);
+ received->iface = g_strdup (iface);
+ received->member = g_strdup (member);
+ received->parameters = g_variant_ref (parameters);
+
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ {
+ if (g_str_equal (sender_name, f->unique_names[i]))
+ {
+ received->sender = i;
+ g_assert_false (f->finished[i]);
+ break;
+ }
+ }
+
+ if (g_str_equal (sender_name, DBUS_SERVICE_DBUS))
+ {
+ g_test_message ("Signal received from message bus %s",
+ sender_name);
+ }
+ else
+ {
+ g_test_message ("Signal received from %s %s",
+ test_conn_descriptions[received->sender],
+ sender_name);
+ g_assert_cmpint (received->sender, !=, TEST_CONN_NONE);
+ }
+
+ g_test_message ("Signal received from %s %s via %s",
+ test_conn_descriptions[received->sender],
+ sender_name,
+ proxy != NULL ? "proxy" : "connection");
+ g_test_message ("\tPath: %s", path);
+ g_test_message ("\tInterface: %s", iface);
+ g_test_message ("\tMember: %s", member);
+
+ if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(su)")))
+ {
+ g_variant_get (parameters, "(su)", &received->arg0, &received->step);
+ g_test_message ("\tString argument 0: %s", received->arg0);
+ g_test_message ("\tSent in step: %u", received->step);
+ }
+ else if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)")))
+ {
+ g_variant_get (parameters, "(uu)", NULL, &received->step);
+ g_test_message ("\tArgument 0: (not a string)");
+ g_test_message ("\tSent in step: %u", received->step);
+ }
+ else if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sss)")))
+ {
+ const char *name;
+ const char *old_owner;
+ const char *new_owner;
+
+ /* The only signal of this signature that we legitimately receive
+ * during this test is NameOwnerChanged, so just assert that it
+ * is from the message bus and can be matched to a plausible step.
+ * (This is less thorough than the above, and will not work if we
+ * add a test scenario where a name's ownership is repeatedly
+ * changed while watching NameOwnerChanged - so don't do that.) */
+ g_assert_cmpstr (sender_name, ==, DBUS_SERVICE_DBUS);
+ g_assert_cmpstr (path, ==, DBUS_PATH_DBUS);
+ g_assert_cmpstr (iface, ==, DBUS_INTERFACE_DBUS);
+ g_assert_cmpstr (member, ==, NAME_OWNER_CHANGED);
+
+ g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner);
+
+ for (i = 0; i < G_N_ELEMENTS (f->plan->steps); i++)
+ {
+ const TestStep *step = &f->plan->steps[i];
+
+ if (step->action == TEST_ACTION_OWN_NAME)
+ {
+ const TestOwnName *own_name = &step->u.own_name;
+
+ if (g_str_equal (name, own_name->name)
+ && g_str_equal (new_owner, f->unique_names[own_name->owner])
+ && own_name->received_by_conn > 0)
+ {
+ received->step = i;
+ break;
+ }
+ }
+
+ if (i >= G_N_ELEMENTS (f->plan->steps))
+ g_error ("Could not match message to a test step");
+ }
+ }
+ else
+ {
+ g_error ("Unexpected message received");
+ }
+
+ g_ptr_array_add (f->received, g_steal_pointer (&received));
+}
+
+static void
+proxy_signal_cb (GDBusProxy *proxy,
+ const char *sender_name,
+ const char *member,
+ GVariant *parameters,
+ void *user_data)
+{
+ Fixture *f = user_data;
+
+ fixture_received_signal (f, proxy, sender_name,
+ g_dbus_proxy_get_object_path (proxy),
+ g_dbus_proxy_get_interface_name (proxy),
+ member, parameters);
+}
+
+static void
+subscribed_signal_cb (GDBusConnection *conn,
+ const char *sender_name,
+ const char *path,
+ const char *iface,
+ const char *member,
+ GVariant *parameters,
+ void *user_data)
+{
+ Fixture *f = user_data;
+ GDBusConnection *subscriber = f->conns[TEST_CONN_SUBSCRIBER];
+
+ g_assert_true (conn == subscriber);
+
+ fixture_received_signal (f, NULL, sender_name, path, iface, member, parameters);
+}
+
+static void
+fixture_subscribe (Fixture *f,
+ const TestSubscribe *subscribe,
+ guint step_number)
+{
+ GDBusConnection *subscriber = f->conns[TEST_CONN_SUBSCRIBER];
+ const char *sender;
+
+ if (subscribe->string_sender != NULL)
+ {
+ sender = subscribe->string_sender;
+ g_test_message ("\tSender: %s", sender);
+ }
+ else if (subscribe->unique_sender != TEST_CONN_NONE)
+ {
+ sender = f->unique_names[subscribe->unique_sender];
+ g_test_message ("\tSender: %s %s",
+ test_conn_descriptions[subscribe->unique_sender],
+ sender);
+ }
+ else
+ {
+ sender = NULL;
+ g_test_message ("\tSender: (any)");
+ }
+
+ g_test_message ("\tPath: %s", nonnull (subscribe->path, "(any)"));
+ g_test_message ("\tInterface: %s",
+ nonnull (subscribe->iface, "(any)"));
+ g_test_message ("\tMember: %s",
+ nonnull (subscribe->member, "(any)"));
+ g_test_message ("\tString argument 0: %s",
+ nonnull (subscribe->arg0, "(any)"));
+ g_test_message ("\tFlags: %x", subscribe->flags);
+
+ if (f->mode != SUBSCRIPTION_MODE_PROXY)
+ {
+ /* CONN or PARALLEL */
+ guint id;
+
+ g_test_message ("\tSubscribing via connection");
+ id = g_dbus_connection_signal_subscribe (subscriber,
+ sender,
+ subscribe->iface,
+ subscribe->member,
+ subscribe->path,
+ subscribe->arg0,
+ subscribe->flags,
+ subscribed_signal_cb,
+ f, NULL);
+
+ g_assert_cmpuint (id, !=, 0);
+
+ if (subscribe->unsubscribe_immediately)
+ {
+ g_test_message ("\tImmediately unsubscribing");
+ g_dbus_connection_signal_unsubscribe (subscriber, id);
+ }
+ else
+ {
+ f->subscriptions[step_number] = id;
+ }
+ }
+
+ if (f->mode != SUBSCRIPTION_MODE_CONN)
+ {
+ /* PROXY or PARALLEL */
+
+ if (sender == NULL)
+ {
+ g_test_message ("\tCannot subscribe via proxy: no bus name");
+ }
+ else if (subscribe->path == NULL)
+ {
+ g_test_message ("\tCannot subscribe via proxy: no path");
+ }
+ else if (subscribe->iface == NULL)
+ {
+ g_test_message ("\tCannot subscribe via proxy: no interface");
+ }
+ else
+ {
+ GDBusProxy *proxy;
+
+ g_test_message ("\tSubscribing via proxy");
+ proxy = g_dbus_proxy_new_sync (subscriber,
+ (G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES
+ | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START),
+ NULL, /* GDBusInterfaceInfo */
+ sender,
+ subscribe->path,
+ subscribe->iface,
+ NULL, /* GCancellable */
+ &f->error);
+ g_assert_no_error (f->error);
+ g_assert_nonnull (proxy);
+ g_signal_connect (proxy, "g-signal", G_CALLBACK (proxy_signal_cb), f);
+ g_ptr_array_add (f->proxies, g_steal_pointer (&proxy));
+ }
+ }
+
+ /* As in setup(), we need to wait for AddMatch to happen. */
+ g_test_message ("Waiting for AddMatch to be processed");
+ connection_wait_for_bus (subscriber);
+}
+
+static void
+fixture_emit_signal (Fixture *f,
+ const TestEmitSignal *signal,
+ guint step_number)
+{
+ GVariant *body;
+ const char *destination;
+ gboolean ok;
+
+ g_test_message ("\tSender: %s",
+ test_conn_descriptions[signal->sender]);
+
+ if (signal->unicast_to != TEST_CONN_NONE)
+ {
+ destination = f->unique_names[signal->unicast_to];
+ g_test_message ("\tDestination: %s %s",
+ test_conn_descriptions[signal->unicast_to],
+ destination);
+ }
+ else
+ {
+ destination = NULL;
+ g_test_message ("\tDestination: (broadcast)");
+ }
+
+ g_assert_nonnull (signal->path);
+ g_test_message ("\tPath: %s", signal->path);
+ g_assert_nonnull (signal->iface);
+ g_test_message ("\tInterface: %s", signal->iface);
+ g_assert_nonnull (signal->member);
+ g_test_message ("\tMember: %s", signal->member);
+
+ /* If arg0 is non-NULL, put it in the message's argument 0.
+ * Otherwise put something that will not match any arg0.
+ * Either way, put the sequence number in argument 1 so we can
+ * correlate sent messages with received messages later. */
+ if (signal->args != NULL)
+ {
+ /* floating */
+ body = g_variant_new_parsed (signal->args);
+ g_assert_nonnull (body);
+ }
+ else if (signal->arg0 != NULL)
+ {
+ g_test_message ("\tString argument 0: %s", signal->arg0);
+ body = g_variant_new ("(su)", signal->arg0, (guint32) step_number);
+ }
+ else
+ {
+ g_test_message ("\tArgument 0: (not a string)");
+ body = g_variant_new ("(uu)", (guint32) 0, (guint32) step_number);
+ }
+
+ ok = g_dbus_connection_emit_signal (f->conns[signal->sender],
+ destination,
+ signal->path,
+ signal->iface,
+ signal->member,
+ /* steals floating reference */
+ g_steal_pointer (&body),
+ &f->error);
+ g_assert_no_error (f->error);
+ g_assert_true (ok);
+
+ /* Emitting the signal is asynchronous, so if we want subsequent steps
+ * to be guaranteed to happen after the signal from the message bus's
+ * perspective, we have to do a round-trip to the message bus to sync up. */
+ g_test_message ("Waiting for signal to reach message bus");
+ connection_wait_for_bus (f->conns[signal->sender]);
+}
+
+static void
+fixture_own_name (Fixture *f,
+ const TestOwnName *own_name)
+{
+ GVariant *call_result;
+ guint32 flags;
+ guint32 result_code;
+
+ g_test_message ("\tName: %s", own_name->name);
+ g_test_message ("\tOwner: %s",
+ test_conn_descriptions[own_name->owner]);
+
+ /* For simplicity, we do this via a direct bus call rather than
+ * using g_bus_own_name_on_connection(). The flags in
+ * GBusNameOwnerFlags are numerically equal to those in the
+ * D-Bus wire protocol. */
+ flags = G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE;
+ call_result = g_dbus_connection_call_sync (f->conns[own_name->owner],
+ DBUS_SERVICE_DBUS,
+ DBUS_PATH_DBUS,
+ DBUS_INTERFACE_DBUS,
+ "RequestName",
+ g_variant_new ("(su)",
+ own_name->name,
+ flags),
+ G_VARIANT_TYPE ("(u)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ &f->error);
+ g_assert_no_error (f->error);
+ g_assert_nonnull (call_result);
+ g_variant_get (call_result, "(u)", &result_code);
+ g_assert_cmpuint (result_code, ==, DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER);
+ g_variant_unref (call_result);
+}
+
+static void
+fixture_run_plan (Fixture *f,
+ const TestPlan *plan,
+ SubscriptionMode mode)
+{
+ guint i;
+
+ G_STATIC_ASSERT (G_N_ELEMENTS (plan->steps) == G_N_ELEMENTS (f->subscriptions));
+ G_STATIC_ASSERT (G_N_ELEMENTS (plan->steps) == G_N_ELEMENTS (f->received_by_conn));
+ G_STATIC_ASSERT (G_N_ELEMENTS (plan->steps) == G_N_ELEMENTS (f->received_by_proxy));
+
+ f->mode = mode;
+ f->plan = plan;
+
+ g_test_summary (plan->description);
+
+ for (i = 0; i < G_N_ELEMENTS (plan->steps); i++)
+ {
+ const TestStep *step = &plan->steps[i];
+
+ switch (step->action)
+ {
+ case TEST_ACTION_SUBSCRIBE:
+ g_test_message ("Step %u: adding subscription", i);
+ fixture_subscribe (f, &step->u.subscribe, i);
+ break;
+
+ case TEST_ACTION_EMIT_SIGNAL:
+ g_test_message ("Step %u: emitting signal", i);
+ fixture_emit_signal (f, &step->u.signal, i);
+ break;
+
+ case TEST_ACTION_OWN_NAME:
+ g_test_message ("Step %u: claiming bus name", i);
+ fixture_own_name (f, &step->u.own_name);
+ break;
+
+ case TEST_ACTION_NONE:
+ /* Padding to fill the rest of the array, do nothing */
+ break;
+
+ default:
+ g_return_if_reached ();
+ }
+ }
+
+ /* Now that we have done everything we wanted to do, emit Finished
+ * from each connection. */
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ {
+ gboolean ok;
+
+ ok = g_dbus_connection_emit_signal (f->conns[i],
+ NULL,
+ FINISHED_PATH,
+ FINISHED_INTERFACE,
+ FINISHED_SIGNAL,
+ NULL,
+ &f->error);
+ g_assert_no_error (f->error);
+ g_assert_true (ok);
+ }
+
+ /* Wait until we have seen the Finished signal from each sender */
+ while (TRUE)
+ {
+ gboolean all_finished = TRUE;
+
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ all_finished = all_finished && f->finished[i];
+
+ if (all_finished)
+ break;
+
+ g_main_context_iteration (NULL, TRUE);
+ }
+
+ /* Assert that the correct things happened before each Finished signal */
+ for (i = 0; i < f->received->len; i++)
+ {
+ const ReceivedMessage *received = g_ptr_array_index (f->received, i);
+
+ g_assert_cmpuint (received->step, <, G_N_ELEMENTS (f->received_by_conn));
+ g_assert_cmpuint (received->step, <, G_N_ELEMENTS (f->received_by_proxy));
+
+ if (received->received_by_proxy != NULL)
+ f->received_by_proxy[received->step] += 1;
+ else
+ f->received_by_conn[received->step] += 1;
+ }
+
+ for (i = 0; i < G_N_ELEMENTS (plan->steps); i++)
+ {
+ const TestStep *step = &plan->steps[i];
+
+ if (step->action == TEST_ACTION_EMIT_SIGNAL)
+ {
+ const TestEmitSignal *signal = &plan->steps[i].u.signal;
+
+ if (mode != SUBSCRIPTION_MODE_PROXY)
+ {
+ g_test_message ("Signal from step %u was received %u times by "
+ "GDBusConnection, expected %u",
+ i, f->received_by_conn[i], signal->received_by_conn);
+ g_assert_cmpuint (f->received_by_conn[i], ==, signal->received_by_conn);
+ }
+ else
+ {
+ g_assert_cmpuint (f->received_by_conn[i], ==, 0);
+ }
+
+ if (mode != SUBSCRIPTION_MODE_CONN)
+ {
+ g_test_message ("Signal from step %u was received %u times by "
+ "GDBusProxy, expected %u",
+ i, f->received_by_proxy[i], signal->received_by_proxy);
+ g_assert_cmpuint (f->received_by_proxy[i], ==, signal->received_by_proxy);
+ }
+ else
+ {
+ g_assert_cmpuint (f->received_by_proxy[i], ==, 0);
+ }
+ }
+ else if (step->action == TEST_ACTION_OWN_NAME)
+ {
+ const TestOwnName *own_name = &plan->steps[i].u.own_name;
+
+ if (mode != SUBSCRIPTION_MODE_PROXY)
+ {
+ g_test_message ("NameOwnerChanged from step %u was received %u "
+ "times by GDBusConnection, expected %u",
+ i, f->received_by_conn[i], own_name->received_by_conn);
+ g_assert_cmpuint (f->received_by_conn[i], ==, own_name->received_by_conn);
+ }
+ else
+ {
+ g_assert_cmpuint (f->received_by_conn[i], ==, 0);
+ }
+
+ if (mode != SUBSCRIPTION_MODE_CONN)
+ {
+ g_test_message ("NameOwnerChanged from step %u was received %u "
+ "times by GDBusProxy, expected %u",
+ i, f->received_by_proxy[i], own_name->received_by_proxy);
+ g_assert_cmpuint (f->received_by_proxy[i], ==, own_name->received_by_proxy);
+ }
+ else
+ {
+ g_assert_cmpuint (f->received_by_proxy[i], ==, 0);
+ }
+ }
+ }
+}
+
+static void
+setup (Fixture *f,
+ G_GNUC_UNUSED const void *context)
+{
+ GDBusConnection *subscriber;
+ guint i;
+
+ session_bus_up ();
+
+ f->proxies = g_ptr_array_new_full (MAX_TEST_STEPS, g_object_unref);
+ f->received = g_ptr_array_new_full (MAX_TEST_STEPS,
+ (GDestroyNotify) received_message_free);
+
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ {
+ f->conns[i] = _g_bus_get_priv (G_BUS_TYPE_SESSION, NULL, &f->error);
+ g_assert_no_error (f->error);
+ g_assert_nonnull (f->conns[i]);
+
+ f->unique_names[i] = g_dbus_connection_get_unique_name (f->conns[i]);
+ g_assert_nonnull (f->unique_names[i]);
+ g_test_message ("%s is %s",
+ test_conn_descriptions[i],
+ f->unique_names[i]);
+ }
+
+ subscriber = f->conns[TEST_CONN_SUBSCRIBER];
+
+ /* Used to wait for all connections to finish sending whatever they
+ * wanted to send */
+ f->finished_subscription = g_dbus_connection_signal_subscribe (subscriber,
+ NULL,
+ FINISHED_INTERFACE,
+ FINISHED_SIGNAL,
+ FINISHED_PATH,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ subscriber_finished_cb,
+ f, NULL);
+ /* AddMatch is sent asynchronously, so we don't know how
+ * soon it will be processed. Before emitting signals, we
+ * need to wait for the message bus to get as far as processing
+ * AddMatch. */
+ g_test_message ("Waiting for AddMatch to be processed");
+ connection_wait_for_bus (subscriber);
+}
+
+static void
+test_conn_subscribe (Fixture *f,
+ const void *context)
+{
+ fixture_run_plan (f, context, SUBSCRIPTION_MODE_CONN);
+}
+
+static void
+test_proxy_subscribe (Fixture *f,
+ const void *context)
+{
+ fixture_run_plan (f, context, SUBSCRIPTION_MODE_PROXY);
+}
+
+static void
+test_parallel_subscribe (Fixture *f,
+ const void *context)
+{
+ fixture_run_plan (f, context, SUBSCRIPTION_MODE_PARALLEL);
+}
+
+static void
+teardown (Fixture *f,
+ G_GNUC_UNUSED const void *context)
+{
+ GDBusConnection *subscriber = f->conns[TEST_CONN_SUBSCRIBER];
+ guint i;
+
+ g_ptr_array_unref (f->proxies);
+
+ if (f->finished_subscription != 0)
+ g_dbus_connection_signal_unsubscribe (subscriber, f->finished_subscription);
+
+ for (i = 0; i < G_N_ELEMENTS (f->subscriptions); i++)
+ {
+ if (f->subscriptions[i] != 0)
+ g_dbus_connection_signal_unsubscribe (subscriber, f->subscriptions[i]);
+ }
+
+ g_ptr_array_unref (f->received);
+
+ for (i = TEST_CONN_FIRST; i < G_N_ELEMENTS (f->conns); i++)
+ g_clear_object (&f->conns[i]);
+
+ g_clear_error (&f->error);
+
+ session_bus_down ();
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
+
+ g_test_dbus_unset ();
+
+#define ADD_SUBSCRIBE_TEST(name) \
+ do { \
+ g_test_add ("/gdbus/subscribe/conn/" #name, \
+ Fixture, &plan_ ## name, \
+ setup, test_conn_subscribe, teardown); \
+ g_test_add ("/gdbus/subscribe/proxy/" #name, \
+ Fixture, &plan_ ## name, \
+ setup, test_proxy_subscribe, teardown); \
+ g_test_add ("/gdbus/subscribe/parallel/" #name, \
+ Fixture, &plan_ ## name, \
+ setup, test_parallel_subscribe, teardown); \
+ } while (0)
+
+ ADD_SUBSCRIBE_TEST (simple);
+ ADD_SUBSCRIBE_TEST (broadcast_from_anyone);
+ ADD_SUBSCRIBE_TEST (match_twice);
+ ADD_SUBSCRIBE_TEST (limit_by_unique_name);
+ ADD_SUBSCRIBE_TEST (nonexistent_unique_name);
+ ADD_SUBSCRIBE_TEST (limit_by_well_known_name);
+ ADD_SUBSCRIBE_TEST (limit_to_message_bus);
+ ADD_SUBSCRIBE_TEST (unsubscribe_immediately);
+
+ return g_test_run();
+}
diff --git a/gio/tests/meson.build b/gio/tests/meson.build
index 9755bc90b..3bfb33307 100644
--- a/gio/tests/meson.build
+++ b/gio/tests/meson.build
@@ -493,6 +493,10 @@ if host_machine.system() != 'windows'
'extra_sources' : extra_sources,
'extra_programs': extra_programs,
},
+ 'gdbus-subscribe' : {
+ 'extra_sources' : extra_sources,
+ 'extra_programs': extra_programs,
+ },
'gdbus-test-codegen' : {
'extra_sources' : [extra_sources, gdbus_test_codegen_generated, gdbus_test_codegen_generated_interface_info],
'c_args' : ['-DGLIB_VERSION_MIN_REQUIRED=GLIB_VERSION_2_32'],