From 8b1d90457e2e72f1d7db7cb77b74754af3a5c4ef Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Fri, 8 Mar 2024 14:19:46 +0000
Subject: [PATCH 03/18] tests: Add a data-driven test for signal subscriptions

This somewhat duplicates test_connection_signals(), but is easier to
extend to cover different scenarios.

Each scenario is tested three times: once with lower-level
GDBusConnection APIs, once with the higher-level GDBusProxy (which
cannot implement all of the subscription scenarios, so some message
counts are lower), and once with both (to check that delivery of the
same message to multiple destinations is handled appropriately).

[Backported to glib-2-74, resolving conflicts in gio/tests/meson.build]
Signed-off-by: Simon McVittie <smcv@collabora.com>

CVE: CVE-2024-34397
Upstream-Status: Backport [https://gitlab.gnome.org/GNOME/glib/-/commit/8b1d90457e2e72f1d7db7cb77b74754af3a5c4ef]
Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 gio/tests/gdbus-subscribe.c | 938 ++++++++++++++++++++++++++++++++++++
 gio/tests/meson.build       |   1 +
 2 files changed, 939 insertions(+)
 create mode 100644 gio/tests/gdbus-subscribe.c

diff --git a/gio/tests/gdbus-subscribe.c b/gio/tests/gdbus-subscribe.c
new file mode 100644
index 000000000..3f53e1d7f
--- /dev/null
+++ b/gio/tests/gdbus-subscribe.c
@@ -0,0 +1,938 @@
+/*
+ * Copyright 2024 Collabora Ltd.
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <gio/gio.h>
+
+#include "gdbus-tests.h"
+
+#define DBUS_SERVICE_DBUS "org.freedesktop.DBus"
+#define DBUS_PATH_DBUS "/org/freedesktop/DBus"
+#define DBUS_INTERFACE_DBUS DBUS_SERVICE_DBUS
+
+/* 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"
+
+/* 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;
+  guint received_by_conn;
+  guint received_by_proxy;
+} TestEmitSignal;
+
+typedef struct
+{
+  TestConn sender;
+  const char *path;
+  const char *iface;
+  const char *member;
+  const char *arg0;
+  GDBusSignalFlags flags;
+} TestSubscribe;
+
+typedef enum
+{
+  TEST_ACTION_NONE = 0,
+  TEST_ACTION_SUBSCRIBE,
+  TEST_ACTION_EMIT_SIGNAL,
+} TestAction;
+
+typedef struct
+{
+  TestAction action;
+  union {
+    TestEmitSignal signal;
+    TestSubscribe subscribe;
+  } 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 = {
+        .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 = {
+        .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 = {
+        .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
+      },
+    },
+  },
+};
+
+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;
+        }
+    }
+
+  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
+    {
+      g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(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);
+    }
+
+  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->sender != TEST_CONN_NONE)
+    {
+      sender = f->unique_names[subscribe->sender];
+      g_test_message ("\tSender: %s %s",
+                      test_conn_descriptions[subscribe->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);
+      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->arg0 != NULL)
+    {
+      g_test_message ("\tString argument 0: %s", signal->arg0);
+      /* floating */
+      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_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_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));
+      g_assert_cmpint (plan->steps[received->step].action,
+                       ==, TEST_ACTION_EMIT_SIGNAL);
+
+      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);
+            }
+        }
+    }
+}
+
+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);
+
+  return g_test_run();
+}
diff --git a/gio/tests/meson.build b/gio/tests/meson.build
index 383d84319..a1551aa53 100644
--- a/gio/tests/meson.build
+++ b/gio/tests/meson.build
@@ -354,6 +354,7 @@ if host_machine.system() != 'windows'
       },
       'gdbus-proxy-unique-name' : {'extra_sources' : extra_sources},
       'gdbus-proxy-well-known-name' : {'extra_sources' : extra_sources},
+      'gdbus-subscribe' : {'extra_sources' : extra_sources},
       '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'],
-- 
2.30.2

