]> git.ipfire.org Git - thirdparty/dbus.git/commitdiff
Add a test for header fields
authorSimon McVittie <smcv@collabora.com>
Mon, 27 Nov 2017 15:51:15 +0000 (15:51 +0000)
committerSimon McVittie <smcv@collabora.com>
Thu, 11 Jan 2018 18:34:10 +0000 (18:34 +0000)
Signed-off-by: Simon McVittie <smcv@collabora.com>
Bug: https://bugs.freedesktop.org/show_bug.cgi?id=100317
Reviewed-by: Philip Withnall <withnall@endlessm.com>
Signed-off-by: Simon McVittie <smcv@collabora.com>
dbus/dbus-marshal-header.c
dbus/dbus-marshal-recursive.h
dbus/dbus-string.h
test/Makefile.am
test/header-fields.c [new file with mode: 0644]

index 1984499eb427bddcf7912a648863215f4f5abce3..a3d73f8937c747d6ffbba06a9bb6c4e948f4151a 100644 (file)
@@ -612,6 +612,9 @@ _dbus_header_create (DBusHeader  *header,
         goto oom;
     }
 
+  /* Note that test/message.c relies on this being in the middle of the
+   * message: if you change the order of serialization here (but why
+   * would you?), please find some other way to retain test coverage. */
   if (interface != NULL)
     {
       if (!write_basic_field (&array,
index fa1d1efa419462536eb069f2477369f984fbe242..c20bb913a1aaedae7528da5fe51c6ecd90c08d6a 100644 (file)
@@ -111,6 +111,7 @@ DBUS_PRIVATE_EXPORT
 int         _dbus_type_reader_get_current_type          (const DBusTypeReader  *reader);
 DBUS_PRIVATE_EXPORT
 int         _dbus_type_reader_get_element_type          (const DBusTypeReader  *reader);
+DBUS_PRIVATE_EXPORT
 int         _dbus_type_reader_get_value_pos             (const DBusTypeReader  *reader);
 DBUS_PRIVATE_EXPORT
 void        _dbus_type_reader_read_basic                (const DBusTypeReader  *reader,
index 5e2d945e35e020fb3e8b0421335137280c52806a..5fff10445504b2604223576d424fb94f25ccc72d 100644 (file)
@@ -92,6 +92,7 @@ DBUS_PRIVATE_EXPORT
 void          _dbus_string_init_const_len        (DBusString        *str,
                                                   const char        *value,
                                                   int                len);
+DBUS_PRIVATE_EXPORT
 dbus_bool_t   _dbus_string_init_preallocated     (DBusString        *str,
                                                   int                allocate_size);
 
@@ -112,6 +113,7 @@ char*         _dbus_string_get_data              (DBusString        *str);
 DBUS_PRIVATE_EXPORT
 const char*   _dbus_string_get_const_data        (const DBusString  *str);
 #endif /* _dbus_string_get_const_data */
+DBUS_PRIVATE_EXPORT
 char*         _dbus_string_get_data_len          (DBusString        *str,
                                                   int                start,
                                                   int                len);
@@ -226,6 +228,7 @@ dbus_bool_t   _dbus_string_insert_2_aligned      (DBusString        *str,
 dbus_bool_t   _dbus_string_insert_4_aligned      (DBusString        *str,
                                                   int                insert_at,
                                                   const unsigned char octets[4]);
+DBUS_PRIVATE_EXPORT
 dbus_bool_t   _dbus_string_insert_8_aligned      (DBusString        *str,
                                                   int                insert_at,
                                                   const unsigned char octets[8]);
index 893b7387bba704ecde07ea031e1d0d0a54fcb27e..feb84a65bb7b5104f8432ce1e1ea866c08d5e846 100644 (file)
@@ -161,6 +161,7 @@ installable_tests += \
        test-dbus-daemon \
        test-dbus-daemon-eavesdrop \
        test-fdpass \
+       test-header-fields \
        test-message \
        test-monitor \
        test-loopback \
@@ -324,6 +325,15 @@ test_dbus_daemon_eavesdrop_LDADD = \
     $(GLIB_LIBS) \
     $(NULL)
 
+test_header_fields_SOURCES = \
+    header-fields.c \
+    $(NULL)
+test_header_fields_LDADD = \
+    libdbus-testutils.la \
+    $(top_builddir)/dbus/libdbus-internal.la \
+    $(GLIB_LIBS) \
+    $(NULL)
+
 if DBUS_UNIX
 test_sd_activation_SOURCES = \
     sd-activation.c \
diff --git a/test/header-fields.c b/test/header-fields.c
new file mode 100644 (file)
index 0000000..150a02a
--- /dev/null
@@ -0,0 +1,766 @@
+/* Unit tests for detailed header field manipulation
+ *
+ * Copyright © 2017 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 <glib.h>
+
+#include <dbus/dbus.h>
+#include "dbus/dbus-internals.h"
+#include "dbus/dbus-marshal-recursive.h"
+#include "dbus/dbus-message-private.h"
+#include "dbus/dbus-string.h"
+#include "dbus/dbus-test-tap.h"
+#include "test-utils-glib.h"
+
+typedef struct {
+    const gchar *mode;
+    TestMainContext *ctx;
+    DBusConnection *left_conn;
+    DBusConnection *right_conn;
+    GPid daemon_pid;
+    gchar *address;
+    GQueue held_messages;
+    gboolean skip;
+} Fixture;
+
+static DBusHandlerResult
+hold_filter (DBusConnection *connection,
+             DBusMessage *message,
+             void *user_data)
+{
+  Fixture *f = user_data;
+
+  if (dbus_message_get_type (message) != DBUS_MESSAGE_TYPE_METHOD_CALL)
+    return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+
+  g_queue_push_tail (&f->held_messages, dbus_message_ref (message));
+
+  return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void
+setup_dbus_daemon (Fixture *f,
+                   gconstpointer context G_GNUC_UNUSED)
+{
+  f->address = test_get_dbus_daemon (NULL, TEST_USER_ME, NULL, &f->daemon_pid);
+
+  if (f->address == NULL)
+    {
+      f->skip = TRUE;
+      return;
+    }
+}
+
+static void
+teardown_dbus_daemon (Fixture *f,
+                      gconstpointer context G_GNUC_UNUSED)
+{
+  if (f->daemon_pid != 0)
+    {
+      test_kill_pid (f->daemon_pid);
+      g_spawn_close_pid (f->daemon_pid);
+      f->daemon_pid = 0;
+    }
+
+  g_clear_pointer (&f->address, g_free);
+}
+
+/* Offset to byte order from start of header */
+#define BYTE_ORDER_OFFSET    0
+/* Offset to version from start of header */
+#define VERSION_OFFSET       3
+/* Offset to fields array length from start of header, in protocol v1 */
+#define FIELDS_ARRAY_LENGTH_OFFSET 12
+/** Offset to first field in header, in protocol v1 */
+#define FIRST_FIELD_OFFSET 16
+
+/* Offset from start of _dbus_header_signature_str to the signature of
+ * the fields array */
+#define FIELDS_ARRAY_SIGNATURE_OFFSET 6
+
+/* A byte that is not a DBUS_HEADER_FIELD_* */
+#define NOT_A_HEADER_FIELD 123
+
+_DBUS_STRING_DEFINE_STATIC(_dbus_header_signature_str, DBUS_HEADER_SIGNATURE);
+
+static void
+string_overwrite_n (DBusString *str,
+                    int         start,
+                    const void *bytes,
+                    int         len)
+{
+  unsigned char *data = _dbus_string_get_udata_len (str, start, len);
+
+  g_assert (data != NULL);
+  memcpy (data, bytes, len);
+}
+
+static void
+steal_reply_cb (DBusPendingCall *pc,
+                void *data)
+{
+  DBusMessage **message_p = data;
+
+  g_assert (message_p != NULL);
+  g_assert (*message_p == NULL);
+  *message_p = dbus_pending_call_steal_reply (pc);
+  g_assert (*message_p != NULL);
+}
+
+/*
+ * Test the handling of unknown header fields.
+ *
+ * Return TRUE if the right thing happens, but the right thing might include
+ * OOM.
+ */
+static dbus_bool_t
+test_weird_header_field (void        *user_data,
+                         dbus_bool_t  have_memory)
+{
+  Fixture *f = user_data;
+  const char *body = "hello";
+  const char *new_body = NULL;
+  DBusError error = DBUS_ERROR_INIT;
+  DBusMessage *original = NULL;
+  DBusMessage *modified = NULL;
+  DBusMessage *filtered = NULL;
+  DBusMessage *relayed = NULL;
+  DBusMessage *reply = NULL;
+  DBusPendingCall *pc = NULL;
+  char *blob = NULL;
+  int blob_len;
+  DBusString modified_blob = _DBUS_STRING_INIT_INVALID;
+  /* This is the serialization of a struct (uv), assumed to be at an
+   * 8-byte boundary. */
+  const unsigned char weird_header[8] = {
+      NOT_A_HEADER_FIELD,   /*< type code */
+      1,                    /*< length of signature */
+      'u',                  /*< signature: uint32 */
+      '\0',                 /*< end of signature */
+      /* no padding required */
+      '\x12',               /*< uint32 0x12345678 (BE) or 0x78563412 (LE) */
+      '\x34',
+      '\x56',
+      '\x78'
+  };
+  int bytes_needed;
+  DBusTypeReader reader;
+  DBusTypeReader array;
+  gboolean added_hold_filter = FALSE;
+  GError *gerror = NULL;
+
+  if (f->skip)
+    return TRUE;
+
+  /* We'd normally do this in setup_dbus_daemon(), but then we couldn't
+   * allocate memory that we want freed by dbus_shutdown(). */
+  g_assert_cmpint (_dbus_get_malloc_blocks_outstanding (), ==, 0);
+  f->ctx = test_main_context_try_get ();
+
+  if (f->ctx == NULL)
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  f->left_conn = test_try_connect_to_bus (f->ctx, f->address, &gerror);
+
+  if (f->left_conn == NULL)
+    {
+      g_assert_error (gerror, G_DBUS_ERROR, G_DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  f->right_conn = test_try_connect_to_bus (f->ctx, f->address, &gerror);
+
+  if (f->right_conn == NULL)
+    {
+      g_assert_error (gerror, G_DBUS_ERROR, G_DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  if (!dbus_connection_add_filter (f->right_conn, hold_filter, f, NULL))
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  added_hold_filter = TRUE;
+
+  original = dbus_message_new_method_call (dbus_bus_get_unique_name (f->right_conn),
+                                           "/com/example/Path",
+                                           "com.example.Interface",
+                                           "Method");
+
+  if (original == NULL ||
+      !dbus_message_append_args (original,
+                                 DBUS_TYPE_STRING, &body,
+                                 DBUS_TYPE_INVALID))
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* Messages with serial number 0 can't be demarshalled. */
+  dbus_message_set_serial (original, 42);
+
+  if (!dbus_message_marshal (original, &blob, &blob_len))
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* We will add up to 8 bytes, so preallocate that much. */
+  if (!_dbus_string_init_preallocated (&modified_blob, blob_len + 8) ||
+      !_dbus_string_append_len (&modified_blob, blob, blob_len))
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* If these are false then we need to change our byte-twiddling */
+  g_assert_cmpint (blob_len, >, FIRST_FIELD_OFFSET);
+  g_assert_cmpint (blob[VERSION_OFFSET], ==, 1);
+  g_assert_cmpint (blob[BYTE_ORDER_OFFSET], ==, DBUS_COMPILER_BYTE_ORDER);
+
+  if (f->mode == NULL)
+    {
+      /* Do nothing: don't insert a weird header field at all */
+    }
+  else if (g_str_equal (f->mode, "change"))
+    {
+      /* Replace the interface (which is optional anyway) with the
+       * weird header field */
+
+      _dbus_type_reader_init (&reader, DBUS_COMPILER_BYTE_ORDER,
+                              &_dbus_header_signature_str,
+                              FIELDS_ARRAY_SIGNATURE_OFFSET,
+                              &modified_blob,
+                              FIELDS_ARRAY_LENGTH_OFFSET);
+      _dbus_type_reader_recurse (&reader, &array);
+
+      while (_dbus_type_reader_get_current_type (&array) !=
+             DBUS_TYPE_INVALID)
+        {
+          DBusTypeReader sub;
+          unsigned char field_code;
+
+          _dbus_type_reader_recurse (&array, &sub);
+
+          g_assert_cmpint (_dbus_type_reader_get_current_type (&sub),
+                           ==, DBUS_TYPE_BYTE);
+          _dbus_type_reader_read_basic (&sub, &field_code);
+
+          if (field_code == DBUS_HEADER_FIELD_INTERFACE)
+            {
+              _dbus_string_set_byte (&modified_blob,
+                                     _dbus_type_reader_get_value_pos (&sub),
+                                     NOT_A_HEADER_FIELD);
+              break;
+            }
+
+          _dbus_type_reader_next (&array);
+        }
+    }
+  else if (g_str_equal (f->mode, "prepend"))
+    {
+      dbus_uint32_t header_fields_length;
+
+      memcpy (&header_fields_length, &blob[FIELDS_ARRAY_LENGTH_OFFSET], 4);
+
+      /* Insert a weird header field at the beginning of the fields
+       * array. We do this by byte manipulation rather than by using a
+       * DBusTypeReader, because we eventually want to get rid of the
+       * counterintuitive ability for a DBusTypeReader to write to the
+       * message: https://bugs.freedesktop.org/show_bug.cgi?id=38288 */
+      if (!_dbus_string_insert_8_aligned (&modified_blob,
+                                          FIRST_FIELD_OFFSET,
+                                          weird_header))
+        {
+          g_assert_false (have_memory);
+          goto out;
+        }
+
+      header_fields_length += 8;
+      string_overwrite_n (&modified_blob, FIELDS_ARRAY_LENGTH_OFFSET,
+                          &header_fields_length, 4);
+    }
+  else if (g_str_equal (f->mode, "append"))
+    {
+      dbus_uint32_t header_fields_length;
+
+      memcpy (&header_fields_length, &blob[FIELDS_ARRAY_LENGTH_OFFSET], 4);
+
+      /* Insert a weird header field at the end of the fields
+       * array, after the padding (which was previously the padding between
+       * header and body, and is now the padding between the last-but-one
+       * header field and the new header field). For simplicity, we've
+       * used a weird header field that does not ever need to be followed
+       * by padding itself.
+       *
+       * Old:
+       *  | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7    |
+       *  | ... 16-byte fixed-length part ...              |
+       *  | ... 16-byte fixed-length part ...              |
+       * [A] first header field ...                        |
+       *  | last header field    [B] padding              [C]
+       * [C] body...
+       *
+       * New:
+       *  | 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7    |
+       *  | ... 16-byte fixed-length part ...              |
+       *  | ... 16-byte fixed-length part ...              |
+       * [A] first header field ...                        |
+       *  | previously-last field [B] padding             [C]
+       *  | weird header field, exactly 8 bytes long    [B'=C']
+       * [B'=C'] body...
+       *
+       */
+      header_fields_length = _DBUS_ALIGN_VALUE (header_fields_length, 8);
+      g_assert_cmpint (header_fields_length % 8, ==, 0);
+
+      if (!_dbus_string_insert_8_aligned (&modified_blob,
+                                          (FIRST_FIELD_OFFSET +
+                                           header_fields_length),
+                                          weird_header))
+        {
+          g_assert_false (have_memory);
+          goto out;
+        }
+
+      header_fields_length += 8;
+      string_overwrite_n (&modified_blob, FIELDS_ARRAY_LENGTH_OFFSET,
+                          &header_fields_length, 4);
+    }
+  else
+    {
+      g_assert_not_reached ();
+    }
+
+  /* OK, now we've hacked up the message, compare it with the original. */
+  bytes_needed = dbus_message_demarshal_bytes_needed (_dbus_string_get_const_data (&modified_blob),
+                                                      _dbus_string_get_length (&modified_blob));
+
+  if (f->mode == NULL || g_str_equal (f->mode, "change"))
+    {
+      /* We edited the message in-place so its effective length didn't
+       * change */
+      g_assert_cmpint (bytes_needed, ==, blob_len);
+    }
+  else
+    {
+      g_assert_cmpint (bytes_needed, ==, blob_len + 8);
+    }
+
+  g_assert_cmpint (_dbus_string_get_length (&modified_blob), ==,
+                   bytes_needed);
+  modified = dbus_message_demarshal (_dbus_string_get_const_data (&modified_blob),
+                                     _dbus_string_get_length (&modified_blob),
+                                     &error);
+
+  if (modified == NULL)
+    {
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* The modified message has the same fields, except possibly the
+   * interface. */
+  g_assert_cmpint (dbus_message_get_type (modified), ==,
+                   dbus_message_get_type (original));
+  g_assert_cmpstr (dbus_message_get_path (modified), ==,
+                   dbus_message_get_path (original));
+  g_assert_cmpstr (dbus_message_get_member (modified), ==,
+                   dbus_message_get_member (original));
+  g_assert_cmpstr (dbus_message_get_error_name (modified), ==,
+                   dbus_message_get_error_name (original));
+  g_assert_cmpstr (dbus_message_get_destination (modified), ==,
+                   dbus_message_get_destination (original));
+  g_assert_cmpstr (dbus_message_get_sender (modified), ==,
+                   dbus_message_get_sender (original));
+  g_assert_cmpstr (dbus_message_get_signature (modified), ==,
+                   dbus_message_get_signature (original));
+  g_assert_cmpint (dbus_message_get_no_reply (modified), ==,
+                   dbus_message_get_no_reply (original));
+  g_assert_cmpint (dbus_message_get_serial (modified), ==,
+                   dbus_message_get_serial (original));
+  g_assert_cmpint (dbus_message_get_reply_serial (modified), ==,
+                   dbus_message_get_reply_serial (original));
+  g_assert_cmpint (dbus_message_get_auto_start (modified), ==,
+                   dbus_message_get_auto_start (original));
+  g_assert_cmpint (dbus_message_get_allow_interactive_authorization (modified),
+                   ==,
+                   dbus_message_get_allow_interactive_authorization (original));
+
+  if (dbus_message_get_args (modified, &error,
+                             DBUS_TYPE_STRING, &new_body,
+                             DBUS_TYPE_INVALID))
+    {
+      g_assert_cmpstr (new_body, ==, body);
+    }
+  else
+    {
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  if (f->mode != NULL && g_str_equal (f->mode, "change"))
+    {
+      /* We edited the interface field in-place to turn it into the
+       * unknown field, so it doesn't have an interface any more. */
+      g_assert_cmpstr (dbus_message_get_interface (modified), ==, NULL);
+    }
+  else
+    {
+      /* We didn't change the interface. */
+      g_assert_cmpstr (dbus_message_get_interface (modified), ==,
+                       dbus_message_get_interface (original));
+    }
+
+  /* Copy the modified message so we can filter it. */
+  filtered = dbus_message_demarshal (_dbus_string_get_const_data (&modified_blob),
+                                     _dbus_string_get_length (&modified_blob),
+                                     &error);
+
+  if (filtered == NULL)
+    {
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* TODO: Actually filter its header */
+
+  /* All known headers are the same as in the modified message that was
+   * deserialized from the same blob */
+  g_assert_cmpint (dbus_message_get_type (filtered), ==,
+                   dbus_message_get_type (modified));
+  g_assert_cmpstr (dbus_message_get_path (filtered), ==,
+                   dbus_message_get_path (modified));
+  g_assert_cmpstr (dbus_message_get_member (filtered), ==,
+                   dbus_message_get_member (modified));
+  g_assert_cmpstr (dbus_message_get_error_name (filtered), ==,
+                   dbus_message_get_error_name (modified));
+  g_assert_cmpstr (dbus_message_get_destination (filtered), ==,
+                   dbus_message_get_destination (modified));
+  g_assert_cmpstr (dbus_message_get_sender (filtered), ==,
+                   dbus_message_get_sender (modified));
+  g_assert_cmpstr (dbus_message_get_signature (filtered), ==,
+                   dbus_message_get_signature (modified));
+  g_assert_cmpint (dbus_message_get_no_reply (filtered), ==,
+                   dbus_message_get_no_reply (modified));
+  g_assert_cmpint (dbus_message_get_serial (filtered), ==,
+                   dbus_message_get_serial (modified));
+  g_assert_cmpint (dbus_message_get_reply_serial (filtered), ==,
+                   dbus_message_get_reply_serial (modified));
+  g_assert_cmpint (dbus_message_get_auto_start (filtered), ==,
+                   dbus_message_get_auto_start (modified));
+  g_assert_cmpint (dbus_message_get_allow_interactive_authorization (modified),
+                   ==,
+                   dbus_message_get_allow_interactive_authorization (original));
+
+  /* The body is also the same */
+  if (dbus_message_get_args (filtered, &error,
+                             DBUS_TYPE_STRING, &new_body,
+                             DBUS_TYPE_INVALID))
+    {
+      g_assert_cmpstr (new_body, ==, body);
+    }
+  else
+    {
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  /* We can't use _dbus_header_get_field_raw() because it asserts that
+   * the field in question is in range. */
+  _dbus_type_reader_init (&reader, DBUS_COMPILER_BYTE_ORDER,
+                          &_dbus_header_signature_str,
+                          FIELDS_ARRAY_SIGNATURE_OFFSET,
+                          &filtered->header.data,
+                          FIELDS_ARRAY_LENGTH_OFFSET);
+  _dbus_type_reader_recurse (&reader, &array);
+
+  while (_dbus_type_reader_get_current_type (&array) != DBUS_TYPE_INVALID)
+    {
+      DBusTypeReader sub;
+      unsigned char field_code;
+
+      _dbus_type_reader_recurse (&array, &sub);
+
+      g_assert_cmpint (_dbus_type_reader_get_current_type (&sub),
+                       ==, DBUS_TYPE_BYTE);
+      _dbus_type_reader_read_basic (&sub, &field_code);
+
+      if (field_code == NOT_A_HEADER_FIELD)
+        {
+          g_test_message ("TODO: header field was not filtered out");
+        }
+
+      _dbus_type_reader_next (&array);
+    }
+
+  /* Sending a message through the dbus-daemon currently preserves
+   * unknown header fields, but it should not.
+   * https://bugs.freedesktop.org/show_bug.cgi?id=100317 */
+
+  /* We have to use send_with_reply because if we don't, we won't know
+   * if the message was dropped on the floor due to out-of-memory. */
+  if (!dbus_connection_send_with_reply (f->left_conn, modified, &pc, -1))
+    {
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  if (dbus_pending_call_get_completed (pc))
+    {
+      steal_reply_cb (pc, &reply);
+    }
+  else if (!dbus_pending_call_set_notify (pc, steal_reply_cb, &reply, NULL))
+    {
+      dbus_pending_call_cancel (pc);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  while (g_queue_get_length (&f->held_messages) < 1 && reply == NULL)
+    test_main_context_iterate (f->ctx, TRUE);
+
+  if (reply != NULL)
+    {
+      dbus_set_error_from_message (&error, reply);
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  relayed = g_queue_pop_head (&f->held_messages);
+  g_assert_cmpint (g_queue_get_length (&f->held_messages), ==, 0);
+
+  /* The message relayed through the dbus-daemon has the same known
+   * fields and content as the one we sent, except that it has a sender
+   * and a serial number. */
+  g_assert_cmpint (dbus_message_get_type (relayed), ==,
+                   dbus_message_get_type (modified));
+  g_assert_cmpstr (dbus_message_get_path (relayed), ==,
+                   dbus_message_get_path (modified));
+  g_assert_cmpstr (dbus_message_get_member (relayed), ==,
+                   dbus_message_get_member (modified));
+  g_assert_cmpstr (dbus_message_get_error_name (relayed), ==,
+                   dbus_message_get_error_name (modified));
+  g_assert_cmpstr (dbus_message_get_destination (relayed), ==,
+                   dbus_message_get_destination (modified));
+  g_assert_cmpstr (dbus_message_get_sender (relayed), ==,
+                   dbus_bus_get_unique_name (f->left_conn));
+  g_assert_cmpstr (dbus_message_get_signature (relayed), ==,
+                   dbus_message_get_signature (modified));
+  g_assert_cmpint (dbus_message_get_no_reply (relayed), ==,
+                   dbus_message_get_no_reply (modified));
+  g_assert_cmpint (dbus_message_get_serial (relayed), !=, 0);
+  g_assert_cmpint (dbus_message_get_reply_serial (relayed), ==,
+                   dbus_message_get_reply_serial (modified));
+  g_assert_cmpint (dbus_message_get_auto_start (relayed), ==,
+                   dbus_message_get_auto_start (modified));
+  g_assert_cmpint (dbus_message_get_allow_interactive_authorization (modified),
+                   ==,
+                   dbus_message_get_allow_interactive_authorization (original));
+
+  if (dbus_message_get_args (relayed, &error,
+                             DBUS_TYPE_STRING, &new_body,
+                             DBUS_TYPE_INVALID))
+    {
+      g_assert_cmpstr (new_body, ==, body);
+    }
+  else
+    {
+      g_assert_cmpstr (error.name, ==, DBUS_ERROR_NO_MEMORY);
+      g_assert_false (have_memory);
+      goto out;
+    }
+
+  _dbus_type_reader_init (&reader, DBUS_COMPILER_BYTE_ORDER,
+                          &_dbus_header_signature_str,
+                          FIELDS_ARRAY_SIGNATURE_OFFSET,
+                          &relayed->header.data,
+                          FIELDS_ARRAY_LENGTH_OFFSET);
+  _dbus_type_reader_recurse (&reader, &array);
+
+  while (_dbus_type_reader_get_current_type (&array) != DBUS_TYPE_INVALID)
+    {
+      DBusTypeReader sub;
+      unsigned char field_code;
+
+      _dbus_type_reader_recurse (&array, &sub);
+
+      g_assert_cmpint (_dbus_type_reader_get_current_type (&sub),
+                       ==, DBUS_TYPE_BYTE);
+      _dbus_type_reader_read_basic (&sub, &field_code);
+
+      if (field_code == NOT_A_HEADER_FIELD)
+        {
+          g_test_message ("TODO: header field was passed through");
+        }
+
+      _dbus_type_reader_next (&array);
+    }
+
+  /* On success, we don't actually reply: it's one more thing that
+   * could hit OOM */
+
+out:
+  g_clear_error (&gerror);
+  _dbus_string_free (&modified_blob);
+  dbus_free (blob);
+
+  if (pc != NULL)
+    dbus_pending_call_cancel (pc);
+
+  dbus_clear_pending_call (&pc);
+  dbus_clear_message (&reply);
+  dbus_clear_message (&relayed);
+  dbus_clear_message (&filtered);
+  dbus_clear_message (&modified);
+  dbus_clear_message (&original);
+  dbus_error_free (&error);
+
+  /* We'd normally do this in teardown_dbus_daemon(), but for this test
+   * we want to do it here so we can dbus_shutdown(). */
+  if (f->left_conn != NULL)
+    {
+      dbus_connection_close (f->left_conn);
+      test_connection_shutdown (f->ctx, f->left_conn);
+    }
+
+  if (f->right_conn != NULL)
+    {
+      if (added_hold_filter)
+        dbus_connection_remove_filter (f->right_conn, hold_filter, f);
+
+      g_queue_foreach (&f->held_messages, (GFunc) dbus_message_unref, NULL);
+      g_queue_clear (&f->held_messages);
+
+      dbus_connection_close (f->right_conn);
+      test_connection_shutdown (f->ctx, f->right_conn);
+    }
+
+  dbus_clear_connection (&f->left_conn);
+  dbus_clear_connection (&f->right_conn);
+  g_clear_pointer (&f->ctx, test_main_context_unref);
+
+  dbus_shutdown ();
+  g_assert_cmpint (_dbus_get_malloc_blocks_outstanding (), ==, 0);
+
+  return !g_test_failed ();
+}
+
+typedef struct
+{
+  const gchar *name;
+  DBusTestMemoryFunction function;
+  const gchar *mode;
+} OOMTestCase;
+
+static void
+test_oom_wrapper (Fixture *f,
+                  gconstpointer data)
+{
+  const OOMTestCase *test = data;
+
+  f->mode = test->mode;
+
+  if (g_test_slow ())
+    {
+      /* When we say slow, we mean it. */
+      test_timeout_reset (30);
+    }
+  else
+    {
+      test_timeout_reset (1);
+    }
+
+  if (!_dbus_test_oom_handling (test->name, test->function, f))
+    {
+      g_test_message ("OOM test failed");
+      g_test_fail ();
+    }
+}
+
+static GQueue *test_cases_to_free = NULL;
+
+static void
+add_oom_test (const gchar *name,
+              DBusTestMemoryFunction function,
+              const gchar *mode)
+{
+  /* By using GLib memory allocation here, we avoid being affected by
+   * dbus_shutdown() or contributing to
+   * _dbus_get_malloc_blocks_outstanding() */
+  OOMTestCase *test_case = g_new0 (OOMTestCase, 1);
+
+  test_case->name = name;
+  test_case->function = function;
+  test_case->mode = mode;
+  g_test_add (name, Fixture, test_case, setup_dbus_daemon,
+              test_oom_wrapper, teardown_dbus_daemon);
+  g_queue_push_tail (test_cases_to_free, test_case);
+}
+
+int
+main (int argc,
+      char **argv)
+{
+  int ret;
+
+  test_init (&argc, &argv);
+
+  /* Normally we test up to 4 consecutive malloc failures, but that's
+   * painfully slow here. */
+  if (g_getenv ("DBUS_TEST_MALLOC_FAILURES") == NULL)
+    {
+      if (!g_test_slow ())
+        g_setenv ("DBUS_TEST_MALLOC_FAILURES", "2", TRUE);
+    }
+
+  test_cases_to_free = g_queue_new ();
+  add_oom_test ("/message/weird-header-field/none", test_weird_header_field,
+                NULL);
+  add_oom_test ("/message/weird-header-field/append", test_weird_header_field,
+                "append");
+  add_oom_test ("/message/weird-header-field/change", test_weird_header_field,
+                "change");
+  add_oom_test ("/message/weird-header-field/prepend", test_weird_header_field,
+                "prepend");
+
+  ret = g_test_run ();
+
+  g_queue_free_full (test_cases_to_free, g_free);
+  return ret;
+}