]> git.ipfire.org Git - thirdparty/dbus.git/commitdiff
New test for fd-passing
authorSimon McVittie <simon.mcvittie@collabora.co.uk>
Wed, 17 Sep 2014 16:19:53 +0000 (17:19 +0100)
committerSimon McVittie <simon.mcvittie@collabora.co.uk>
Wed, 17 Sep 2014 16:20:07 +0000 (17:20 +0100)
Bug: https://bugs.freedesktop.org/show_bug.cgi?id=83622
Reviewed-by: Alban Crequy <alban.crequy@collabora.co.uk>
[add dbus-sysdeps-unix.h as required for close-on-exec in master -smcv]

configure.ac
test/fdpass.c [new file with mode: 0644]

index 374a30e9cbebe68e82d1242586600edc394f5ad6..4443d057de17e0d0aebc290751d3a0d6d5027e42 100644 (file)
@@ -208,7 +208,7 @@ fi
 # or binaries.
 
 AC_DEFINE([GLIB_VERSION_MIN_REQUIRED], [GLIB_VERSION_2_26], [Ignore post-2.26 deprecations])
-AC_DEFINE([GLIB_VERSION_MAX_ALLOWED], [GLIB_VERSION_2_32], [Prevent post-2.32 APIs])
+AC_DEFINE([GLIB_VERSION_MAX_ALLOWED], [GLIB_VERSION_2_38], [Prevent post-2.38 APIs])
 
 with_glib=yes
 
@@ -591,7 +591,7 @@ AC_DEFINE_UNQUOTED([DBUS_USE_SYNC], [$have_sync], [Use the gcc __sync extension]
 AC_SEARCH_LIBS(socket,[socket network])
 AC_CHECK_FUNC(gethostbyname,,[AC_CHECK_LIB(nsl,gethostbyname)])
 
-AC_CHECK_FUNCS(vsnprintf vasprintf nanosleep usleep setenv clearenv unsetenv socketpair getgrouplist fpathconf setrlimit poll setlocale localeconv strtoll strtoull issetugid getresuid)
+AC_CHECK_FUNCS([vsnprintf vasprintf nanosleep usleep setenv clearenv unsetenv socketpair getgrouplist fpathconf setrlimit poll setlocale localeconv strtoll strtoull issetugid getresuid getrlimit])
 
 AC_CHECK_HEADERS([syslog.h])
 if test "x$ac_cv_header_syslog_h" = "xyes"; then
diff --git a/test/fdpass.c b/test/fdpass.c
new file mode 100644 (file)
index 0000000..d41dac5
--- /dev/null
@@ -0,0 +1,853 @@
+/*
+ * Copyright © 2010-2012 Nokia Corporation
+ * Copyright © 2014 Collabora Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation files
+ * (the "Software"), to deal in the Software without restriction,
+ * including without limitation the rights to use, copy, modify, merge,
+ * publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <config.h>
+
+#include <dbus/dbus.h>
+#include <dbus/dbus-internals.h>
+#include <dbus/dbus-sysdeps.h>
+
+#include <glib.h>
+
+#include <string.h>
+
+#ifdef G_OS_UNIX
+# include <dbus/dbus-sysdeps-unix.h>
+
+# include <errno.h>
+# include <fcntl.h>
+# ifdef HAVE_SYS_RESOURCE_H
+#   include <sys/resource.h>
+# endif
+# include <sys/stat.h>
+# include <sys/time.h>
+# include <sys/types.h>
+# include <unistd.h>
+#endif
+
+#include "test-utils.h"
+
+/* Arbitrary; included here to avoid relying on the default */
+#define MAX_MESSAGE_UNIX_FDS 20
+/* This test won't work on Linux unless this is true. */
+_DBUS_STATIC_ASSERT (MAX_MESSAGE_UNIX_FDS <= 253);
+
+/* Arbitrary; included here to avoid relying on the default. */
+#define MAX_INCOMING_UNIX_FDS (MAX_MESSAGE_UNIX_FDS * 4)
+
+/* Arbitrary, except that MAX_MESSAGE_UNIX_FDS * SOME_MESSAGES must be
+ * less than the process's file descriptor limit. */
+#define SOME_MESSAGES 50
+
+/* Linux won't allow more than 253 fds per sendmsg(). */
+#define TOO_MANY_FDS 255
+_DBUS_STATIC_ASSERT (MAX_MESSAGE_UNIX_FDS < TOO_MANY_FDS);
+
+/* As in test/relay.c, this is a miniature dbus-daemon: we relay messages
+ * from the client on the left to the client on the right.
+ *
+ * left      socket     left      dispatch     right    socket     right
+ * client ===========>  server --------------> server ===========> client
+ * conn                 conn                   conn                conn
+ */
+
+typedef struct {
+    TestMainContext *ctx;
+    DBusError e;
+
+    DBusServer *server;
+
+    DBusConnection *left_client_conn;
+    DBusConnection *left_server_conn;
+
+    DBusConnection *right_server_conn;
+    DBusConnection *right_client_conn;
+    /* queue of DBusMessage received by right_client_conn */
+    GQueue messages;
+
+    int fd_before;
+} Fixture;
+
+#if !GLIB_CHECK_VERSION (2, 38, 0)
+#define g_test_skip(s) my_test_skip (s)
+static inline void my_test_skip (const gchar *s)
+{
+  g_message ("SKIP: %s", s);
+}
+#endif
+
+#ifdef HAVE_UNIX_FD_PASSING
+
+static void oom (const gchar *doing) G_GNUC_NORETURN;
+
+static void
+oom (const gchar *doing)
+{
+  g_error ("out of memory (%s)", doing);
+}
+
+static void
+assert_no_error (const DBusError *e)
+{
+  if (G_UNLIKELY (dbus_error_is_set (e)))
+    g_error ("expected success but got error: %s: %s", e->name, e->message);
+}
+
+static DBusHandlerResult
+left_server_message_cb (DBusConnection *server_conn,
+    DBusMessage *message,
+    void *data)
+{
+  Fixture *f = data;
+
+  g_assert (server_conn == f->left_server_conn);
+  g_assert (f->right_server_conn != NULL);
+
+  dbus_connection_send (f->right_server_conn, message, NULL);
+
+  return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static DBusHandlerResult
+right_client_message_cb (DBusConnection *client_conn,
+    DBusMessage *message,
+    void *data)
+{
+  Fixture *f = data;
+
+  g_assert (client_conn == f->right_client_conn);
+  g_queue_push_tail (&f->messages, dbus_message_ref (message));
+
+  return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void
+new_conn_cb (DBusServer *server,
+    DBusConnection *server_conn,
+    void *data)
+{
+  Fixture *f = data;
+
+  dbus_connection_set_max_message_unix_fds (server_conn,
+      MAX_MESSAGE_UNIX_FDS);
+  dbus_connection_set_max_received_unix_fds (server_conn,
+      MAX_INCOMING_UNIX_FDS);
+
+  if (f->left_server_conn == NULL)
+    {
+      f->left_server_conn = dbus_connection_ref (server_conn);
+
+      if (!dbus_connection_add_filter (server_conn,
+          left_server_message_cb, f, NULL))
+        oom ("adding filter");
+    }
+  else
+    {
+      g_assert (f->right_server_conn == NULL);
+      f->right_server_conn = dbus_connection_ref (server_conn);
+    }
+
+  test_connection_setup (f->ctx, server_conn);
+}
+
+static void
+test_connect (Fixture *f,
+    gconstpointer data G_GNUC_UNUSED)
+{
+  char *address;
+
+  g_assert (f->left_server_conn == NULL);
+  g_assert (f->right_server_conn == NULL);
+
+  address = dbus_server_get_address (f->server);
+  g_assert (address != NULL);
+
+  f->left_client_conn = dbus_connection_open_private (address, &f->e);
+  assert_no_error (&f->e);
+  g_assert (f->left_client_conn != NULL);
+  test_connection_setup (f->ctx, f->left_client_conn);
+
+  /* The left client connection is allowed to behave abusively. */
+  dbus_connection_set_max_message_unix_fds (f->left_client_conn, 1000);
+  dbus_connection_set_max_received_unix_fds (f->left_client_conn, 1000000);
+
+  while (f->left_server_conn == NULL)
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  f->right_client_conn = dbus_connection_open_private (address, &f->e);
+  assert_no_error (&f->e);
+  g_assert (f->right_client_conn != NULL);
+  test_connection_setup (f->ctx, f->right_client_conn);
+
+  dbus_free (address);
+
+  while (f->right_server_conn == NULL)
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  if (!dbus_connection_add_filter (f->right_client_conn,
+      right_client_message_cb, f, NULL))
+    oom ("adding filter");
+
+  /* The right client connection is allowed to queue all the messages. */
+  dbus_connection_set_max_message_unix_fds (f->right_client_conn, 1000);
+  dbus_connection_set_max_received_unix_fds (f->right_client_conn, 1000000);
+
+  while (!dbus_connection_get_is_authenticated (f->left_client_conn) ||
+      !dbus_connection_get_is_authenticated (f->right_client_conn) ||
+      !dbus_connection_get_is_authenticated (f->left_server_conn) ||
+      !dbus_connection_get_is_authenticated (f->right_server_conn))
+    {
+      g_printerr ("*");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  if (!dbus_connection_can_send_type (f->left_client_conn,
+        DBUS_TYPE_UNIX_FD))
+    g_error ("left client connection cannot send Unix fds");
+
+  if (!dbus_connection_can_send_type (f->left_server_conn,
+        DBUS_TYPE_UNIX_FD))
+    g_error ("left server connection cannot send Unix fds");
+
+  if (!dbus_connection_can_send_type (f->right_client_conn,
+        DBUS_TYPE_UNIX_FD))
+    g_error ("right client connection cannot send Unix fds");
+
+  if (!dbus_connection_can_send_type (f->right_server_conn,
+        DBUS_TYPE_UNIX_FD))
+    g_error ("right server connection cannot send Unix fds");
+}
+#endif
+
+static void
+setup (Fixture *f,
+    gconstpointer data G_GNUC_UNUSED)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  /* We assume that anything with fd-passing supports the unix: transport */
+
+  f->ctx = test_main_context_get ();
+  dbus_error_init (&f->e);
+  g_queue_init (&f->messages);
+
+  f->server = dbus_server_listen ("unix:tmpdir=/tmp", &f->e);
+  assert_no_error (&f->e);
+  g_assert (f->server != NULL);
+
+  dbus_server_set_new_connection_function (f->server,
+      new_conn_cb, f, NULL);
+  test_server_setup (f->ctx, f->server);
+
+  f->fd_before = open ("/dev/null", O_RDONLY);
+
+  /* this should succeed on any reasonable Unix */
+  if (f->fd_before < 0)
+    g_error ("cannot open /dev/null for reading: %s", g_strerror (errno));
+
+  _dbus_fd_set_close_on_exec (f->fd_before);
+#endif
+}
+
+static void
+test_relay (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  /* We assume that any platform with working fd-passing is POSIX,
+   * and therefore has open() and fstat() */
+  dbus_uint32_t serial;
+  DBusMessage *outgoing, *incoming;
+  int fd_after;
+  struct stat stat_before;
+  struct stat stat_after;
+
+  test_connect (f, data);
+
+  outgoing = dbus_message_new_signal ("/com/example/Hello",
+      "com.example.Hello", "Greeting");
+  g_assert (outgoing != NULL);
+
+  if (!dbus_message_append_args (outgoing,
+        DBUS_TYPE_UNIX_FD, &f->fd_before,
+        DBUS_TYPE_INVALID))
+    oom ("appending fd");
+
+  if (!dbus_connection_send (f->left_client_conn, outgoing, &serial))
+    oom ("sending message");
+
+  dbus_message_unref (outgoing);
+
+  while (g_queue_get_length (&f->messages) < 1)
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 1);
+
+  incoming = g_queue_pop_head (&f->messages);
+
+  g_assert (dbus_message_contains_unix_fds (incoming));
+  g_assert_cmpstr (dbus_message_get_destination (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_error_name (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_interface (incoming), ==,
+      "com.example.Hello");
+  g_assert_cmpstr (dbus_message_get_member (incoming), ==, "Greeting");
+  g_assert_cmpstr (dbus_message_get_sender (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_signature (incoming), ==,
+      DBUS_TYPE_UNIX_FD_AS_STRING);
+  g_assert_cmpstr (dbus_message_get_path (incoming), ==, "/com/example/Hello");
+  g_assert_cmpuint (dbus_message_get_serial (incoming), ==, serial);
+
+  if (!dbus_message_get_args (incoming,
+        &f->e,
+        DBUS_TYPE_UNIX_FD, &fd_after,
+        DBUS_TYPE_INVALID))
+    g_error ("%s: %s", f->e.name, f->e.message);
+
+  assert_no_error (&f->e);
+
+  if (fstat (f->fd_before, &stat_before) < 0)
+    g_error ("%s", g_strerror (errno));
+
+  if (fstat (fd_after, &stat_after) < 0)
+    g_error ("%s", g_strerror (errno));
+
+  /* this seems like enough to say "it's the same file" */
+  g_assert_cmpint (stat_before.st_dev, ==, stat_after.st_dev);
+  g_assert_cmpint (stat_before.st_ino, ==, stat_after.st_ino);
+  g_assert_cmpint (stat_before.st_rdev, ==, stat_after.st_rdev);
+
+  dbus_message_unref (incoming);
+
+  if (close (fd_after) < 0)
+    g_error ("%s", g_strerror (errno));
+
+  g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_server_conn));
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+test_limit (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  dbus_uint32_t serial;
+  DBusMessage *outgoing, *incoming;
+  int i;
+
+  test_connect (f, data);
+
+  outgoing = dbus_message_new_signal ("/com/example/Hello",
+      "com.example.Hello", "Greeting");
+  g_assert (outgoing != NULL);
+
+  for (i = 0; i < MAX_MESSAGE_UNIX_FDS; i++)
+    {
+      if (!dbus_message_append_args (outgoing,
+            DBUS_TYPE_UNIX_FD, &f->fd_before,
+            DBUS_TYPE_INVALID))
+        oom ("appending fd");
+    }
+
+  if (!dbus_connection_send (f->left_client_conn, outgoing, &serial))
+    oom ("sending message");
+
+  dbus_message_unref (outgoing);
+
+  while (g_queue_get_length (&f->messages) < 1)
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 1);
+
+  incoming = g_queue_pop_head (&f->messages);
+
+  g_assert (dbus_message_contains_unix_fds (incoming));
+  g_assert_cmpstr (dbus_message_get_destination (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_error_name (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_interface (incoming), ==,
+      "com.example.Hello");
+  g_assert_cmpstr (dbus_message_get_member (incoming), ==, "Greeting");
+  g_assert_cmpstr (dbus_message_get_sender (incoming), ==, NULL);
+  g_assert_cmpstr (dbus_message_get_path (incoming), ==, "/com/example/Hello");
+  g_assert_cmpuint (dbus_message_get_serial (incoming), ==, serial);
+
+  dbus_message_unref (incoming);
+
+  g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_server_conn));
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+test_too_many (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  DBusMessage *outgoing;
+  int i;
+
+  test_connect (f, data);
+
+  outgoing = dbus_message_new_signal ("/com/example/Hello",
+      "com.example.Hello", "Greeting");
+  g_assert (outgoing != NULL);
+
+  for (i = 0; i < MAX_MESSAGE_UNIX_FDS + GPOINTER_TO_UINT (data); i++)
+    {
+      if (!dbus_message_append_args (outgoing,
+            DBUS_TYPE_UNIX_FD, &f->fd_before,
+            DBUS_TYPE_INVALID))
+        oom ("appending fd");
+    }
+
+  if (!dbus_connection_send (f->left_client_conn, outgoing, NULL))
+    oom ("sending message");
+
+  dbus_message_unref (outgoing);
+
+  /* The sender is unceremoniously disconnected. */
+  while (dbus_connection_get_is_connected (f->left_client_conn) ||
+      dbus_connection_get_is_connected (f->left_server_conn))
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  /* The message didn't get through without its fds. */
+  g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 0);
+
+  /* The intended victim is unaffected by the left connection's
+   * misbehaviour. */
+  g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+test_too_many_split (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  DBusMessage *outgoing;
+  int i;
+  int left_client_socket;
+  char *payload;
+  int payload_len;
+  DBusString buffer;
+  int fds[TOO_MANY_FDS];
+  int done;
+
+  /* This test deliberately pushes up against OS limits, so skip it
+   * if we don't have enough fds. 4 times the maximum per message
+   * ought to be enough: that will cover the message, the dup'd fds
+   * we actually send, the copy that we potentially receive, and some
+   * spare capacity for everything else. */
+#ifdef HAVE_GETRLIMIT
+  struct rlimit lim;
+
+  if (getrlimit (RLIMIT_NOFILE, &lim) == 0)
+    {
+      if (lim.rlim_cur != RLIM_INFINITY &&
+          lim.rlim_cur < 4 * TOO_MANY_FDS)
+        {
+          g_test_skip ("not enough RLIMIT_NOFILE");
+          return;
+        }
+    }
+#endif
+
+  test_connect (f, data);
+
+  outgoing = dbus_message_new_signal ("/com/example/Hello",
+      "com.example.Hello", "Greeting");
+  g_assert (outgoing != NULL);
+
+  /* TOO_MANY_FDS fds are far too many: in particular, Linux doesn't allow
+   * sending this many in a single sendmsg(). libdbus never splits
+   * a message between two sendmsg() calls if it can help it, and
+   * in particular it always sends all the fds with the first sendmsg(),
+   * but malicious senders might not be so considerate. */
+  for (i = 0; i < TOO_MANY_FDS; i++)
+    {
+      if (!dbus_message_append_args (outgoing,
+            DBUS_TYPE_UNIX_FD, &f->fd_before,
+            DBUS_TYPE_INVALID))
+        oom ("appending fd");
+    }
+
+  /* This probably shouldn't work for messages with fds, but it does,
+   * which is convenient for this sort of trickery. */
+  if (!dbus_message_marshal (outgoing, &payload, &payload_len))
+    oom ("marshalling message");
+
+  _dbus_string_init_const_len (&buffer, payload, payload_len);
+
+  for (i = 0; i < TOO_MANY_FDS; i++)
+    {
+      fds[i] = dup (f->fd_before);
+
+      if (fds[i] < 0)
+        g_error ("could not dup fd: %s", g_strerror (errno));
+    }
+
+  /* This is blatant cheating, and the API documentation specifically
+   * tells you not use this function in this way. Never do this
+   * in application code. */
+  if (!dbus_connection_get_socket (f->left_client_conn, &left_client_socket))
+    g_error ("'unix:' DBusConnection should have had a socket");
+
+  /* Just to be sure that we're at a message boundary. */
+  dbus_connection_flush (f->left_client_conn);
+
+  /* We have too many fds for one sendmsg(), so send the first half
+   * (rounding down if odd) with the first byte... */
+  done = _dbus_write_socket_with_unix_fds (left_client_socket, &buffer, 0, 1,
+        &fds[0], TOO_MANY_FDS / 2);
+
+  if (done < 0)
+    g_error ("could not send first byte and first batch of fds: %s",
+        g_strerror (errno));
+
+  /* ... and the second half (rounding up if odd) with the rest of
+   * the message */
+  done = _dbus_write_socket_with_unix_fds (left_client_socket, &buffer, 1,
+        payload_len - 1, &fds[TOO_MANY_FDS / 2],
+        TOO_MANY_FDS - (TOO_MANY_FDS / 2));
+
+  if (done < 0)
+    {
+      g_error ("could not send rest of message and rest of fds: %s",
+          g_strerror (errno));
+    }
+  else if (done < payload_len - 1)
+    {
+      /* For simplicity, assume the socket buffer is big enough for the
+       * whole message, which should be < 2 KiB. If this fails on some
+       * OS, redo this test code to use a proper loop like the real
+       * libdbus does. */
+      g_error ("short write in sendmsg(), fix this test: %d/%d",
+          done, payload_len - 1);
+    }
+
+  dbus_free (payload);
+
+  for (i = 0; i < TOO_MANY_FDS; i++)
+    close (fds[i]);
+
+  dbus_message_unref (outgoing);
+
+  /* The sender is unceremoniously disconnected. */
+  while (dbus_connection_get_is_connected (f->left_client_conn) ||
+      dbus_connection_get_is_connected (f->left_server_conn))
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  /* The message didn't get through without its fds. */
+  g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 0);
+
+  /* The intended victim is unaffected by the left connection's
+   * misbehaviour. */
+  g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+test_flood (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  int i, j;
+  DBusMessage *outgoing[SOME_MESSAGES];
+  dbus_uint32_t serial;
+
+  test_connect (f, data);
+
+  for (j = 0; j < SOME_MESSAGES; j++)
+    {
+      outgoing[j] = dbus_message_new_signal ("/com/example/Hello",
+          "com.example.Hello", "Greeting");
+      g_assert (outgoing[j] != NULL);
+
+      for (i = 0; i < GPOINTER_TO_UINT (data); i++)
+        {
+          if (!dbus_message_append_args (outgoing[j],
+                DBUS_TYPE_UNIX_FD, &f->fd_before,
+                DBUS_TYPE_INVALID))
+            oom ("appending fd");
+        }
+    }
+
+  /* This is in its own loop so we do it as fast as possible */
+  for (j = 0; j < SOME_MESSAGES; j++)
+    {
+      if (!dbus_connection_send (f->left_client_conn, outgoing[j], &serial))
+        oom ("sending message");
+    }
+
+  for (j = 0; j < SOME_MESSAGES; j++)
+    {
+      dbus_message_unref (outgoing[j]);
+    }
+
+  while (g_queue_get_length (&f->messages) < SOME_MESSAGES)
+    {
+      g_print (".");
+      test_main_context_iterate (f->ctx, TRUE);
+    }
+
+  g_assert_cmpuint (g_queue_get_length (&f->messages), ==, SOME_MESSAGES);
+
+  for (j = 0; j < SOME_MESSAGES; j++)
+    {
+      DBusMessage *incoming;
+
+      incoming = g_queue_pop_head (&f->messages);
+
+      g_assert (dbus_message_contains_unix_fds (incoming));
+      g_assert_cmpstr (dbus_message_get_destination (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_error_name (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_interface (incoming), ==,
+          "com.example.Hello");
+      g_assert_cmpstr (dbus_message_get_member (incoming), ==, "Greeting");
+      g_assert_cmpstr (dbus_message_get_sender (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_path (incoming), ==, "/com/example/Hello");
+
+      dbus_message_unref (incoming);
+    }
+
+  g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_client_conn));
+  g_assert (dbus_connection_get_is_connected (f->left_server_conn));
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+test_odd_limit (Fixture *f,
+    gconstpointer data)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  DBusMessage *outgoing;
+  int i;
+
+  test_connect (f, data);
+  dbus_connection_set_max_message_unix_fds (f->left_server_conn, 7);
+  dbus_connection_set_max_message_unix_fds (f->right_server_conn, 7);
+
+  outgoing = dbus_message_new_signal ("/com/example/Hello",
+      "com.example.Hello", "Greeting");
+  g_assert (outgoing != NULL);
+
+  for (i = 0; i < 7 + GPOINTER_TO_INT (data); i++)
+    {
+      if (!dbus_message_append_args (outgoing,
+            DBUS_TYPE_UNIX_FD, &f->fd_before,
+            DBUS_TYPE_INVALID))
+        oom ("appending fd");
+    }
+
+  if (!dbus_connection_send (f->left_client_conn, outgoing, NULL))
+    oom ("sending message");
+
+  dbus_message_unref (outgoing);
+
+  if (GPOINTER_TO_INT (data) > 0)
+    {
+      /* The sender is unceremoniously disconnected. */
+      while (dbus_connection_get_is_connected (f->left_client_conn) ||
+          dbus_connection_get_is_connected (f->left_server_conn))
+        {
+          g_print (".");
+          test_main_context_iterate (f->ctx, TRUE);
+        }
+
+      /* The message didn't get through without its fds. */
+      g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 0);
+
+      /* The intended victim is unaffected by the left connection's
+       * misbehaviour. */
+      g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+      g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+    }
+  else
+    {
+      DBusMessage *incoming;
+
+      /* We're at or under the limit. The message gets through intact. */
+      while (g_queue_get_length (&f->messages) < 1)
+        {
+          g_print (".");
+          test_main_context_iterate (f->ctx, TRUE);
+        }
+
+      g_assert_cmpuint (g_queue_get_length (&f->messages), ==, 1);
+
+      incoming = g_queue_pop_head (&f->messages);
+
+      g_assert (dbus_message_contains_unix_fds (incoming));
+      g_assert_cmpstr (dbus_message_get_destination (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_error_name (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_interface (incoming), ==,
+          "com.example.Hello");
+      g_assert_cmpstr (dbus_message_get_member (incoming), ==, "Greeting");
+      g_assert_cmpstr (dbus_message_get_sender (incoming), ==, NULL);
+      g_assert_cmpstr (dbus_message_get_path (incoming), ==,
+          "/com/example/Hello");
+
+      dbus_message_unref (incoming);
+
+      g_assert (dbus_connection_get_is_connected (f->right_client_conn));
+      g_assert (dbus_connection_get_is_connected (f->right_server_conn));
+      g_assert (dbus_connection_get_is_connected (f->left_client_conn));
+      g_assert (dbus_connection_get_is_connected (f->left_server_conn));
+    }
+#else
+  g_test_skip ("fd-passing not supported on this platform");
+#endif
+}
+
+static void
+teardown (Fixture *f,
+    gconstpointer data G_GNUC_UNUSED)
+{
+#ifdef HAVE_UNIX_FD_PASSING
+  if (f->left_client_conn != NULL)
+    {
+      dbus_connection_close (f->left_client_conn);
+      dbus_connection_unref (f->left_client_conn);
+      f->left_client_conn = NULL;
+    }
+
+  if (f->right_client_conn != NULL)
+    {
+      dbus_connection_close (f->right_client_conn);
+      dbus_connection_unref (f->right_client_conn);
+      f->right_client_conn = NULL;
+    }
+
+  if (f->left_server_conn != NULL)
+    {
+      dbus_connection_close (f->left_server_conn);
+      dbus_connection_unref (f->left_server_conn);
+      f->left_server_conn = NULL;
+    }
+
+  if (f->right_server_conn != NULL)
+    {
+      dbus_connection_close (f->right_server_conn);
+      dbus_connection_unref (f->right_server_conn);
+      f->right_server_conn = NULL;
+    }
+
+  if (f->server != NULL)
+    {
+      dbus_server_disconnect (f->server);
+      dbus_server_unref (f->server);
+      f->server = NULL;
+    }
+
+  test_main_context_unref (f->ctx);
+
+  if (f->fd_before >= 0 && close (f->fd_before) < 0)
+    g_error ("%s", g_strerror (errno));
+#endif
+}
+
+int
+main (int argc,
+    char **argv)
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_bug_base ("https://bugs.freedesktop.org/show_bug.cgi?id=");
+
+  g_test_add ("/relay", Fixture, NULL, setup,
+      test_relay, teardown);
+  g_test_add ("/limit", Fixture, NULL, setup,
+      test_limit, teardown);
+
+  g_test_add ("/too-many/plus1", Fixture, GUINT_TO_POINTER (1), setup,
+      test_too_many, teardown);
+  g_test_add ("/too-many/plus2", Fixture, GUINT_TO_POINTER (2), setup,
+      test_too_many, teardown);
+  g_test_add ("/too-many/plus17", Fixture, GUINT_TO_POINTER (17), setup,
+      test_too_many, teardown);
+
+  g_test_add ("/too-many/split", Fixture, NULL, setup,
+      test_too_many_split, teardown);
+
+  g_test_add ("/flood/1", Fixture, GUINT_TO_POINTER (1),
+      setup, test_flood, teardown);
+#if MAX_MESSAGE_UNIX_FDS >= 2
+  g_test_add ("/flood/half-limit", Fixture,
+      GUINT_TO_POINTER (MAX_MESSAGE_UNIX_FDS / 2),
+      setup, test_flood, teardown);
+#endif
+#if MAX_MESSAGE_UNIX_FDS >= 4
+  g_test_add ("/flood/over-half-limit", Fixture,
+      GUINT_TO_POINTER (3 * MAX_MESSAGE_UNIX_FDS / 4),
+      setup, test_flood, teardown);
+#endif
+  g_test_add ("/flood/limit", Fixture,
+      GUINT_TO_POINTER (MAX_MESSAGE_UNIX_FDS), setup, test_flood, teardown);
+
+  g_test_add ("/odd-limit/minus1", Fixture, GINT_TO_POINTER (-1), setup,
+      test_odd_limit, teardown);
+  g_test_add ("/odd-limit/at", Fixture, GINT_TO_POINTER (0), setup,
+      test_odd_limit, teardown);
+  g_test_add ("/odd-limit/plus1", Fixture, GINT_TO_POINTER (+1), setup,
+      test_odd_limit, teardown);
+  g_test_add ("/odd-limit/plus2", Fixture, GINT_TO_POINTER (+2), setup,
+      test_odd_limit, teardown);
+
+  return g_test_run ();
+}