Version in base suite: 0.1.6-1+deb13u2 Base version: xdg-dbus-proxy_0.1.6-1+deb13u2 Target version: xdg-dbus-proxy_0.1.6-1+deb13u3 Base file: /srv/ftp-master.debian.org/ftp/pool/main/x/xdg-dbus-proxy/xdg-dbus-proxy_0.1.6-1+deb13u2.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/x/xdg-dbus-proxy/xdg-dbus-proxy_0.1.6-1+deb13u3.dsc changelog | 13 patches/CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch | 144 +++ patches/CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch | 138 +++ patches/CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch | 361 ++++++++++ patches/CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch | 216 +++++ patches/CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch | 194 +++++ patches/CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch | 123 +++ patches/series | 6 8 files changed, 1195 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp35n8s7hw/xdg-dbus-proxy_0.1.6-1+deb13u2.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp35n8s7hw/xdg-dbus-proxy_0.1.6-1+deb13u3.dsc: no acceptable signature found diff -Nru xdg-dbus-proxy-0.1.6/debian/changelog xdg-dbus-proxy-0.1.6/debian/changelog --- xdg-dbus-proxy-0.1.6/debian/changelog 2026-08-11 12:43:30.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/changelog 2026-09-23 12:16:29.000000000 +0000 @@ -1,3 +1,15 @@ +xdg-dbus-proxy (0.1.6-1+deb13u3) trixie-security; urgency=high + + * d/p/CVE-2026-94422/*.patch: + Apply patches from upstream release 0.1.9 to fix message filtering + bypass vulnerabilities. + A malicious or compromised Flatpak app could use these vulnerabilities + to escape the sandbox and execute arbitrary code on the host. + (CVE-2026-94422, GHSA-2cgv-pwcq-wvpq, Closes: #1148782) + * Mention CVE-2026-93676, #1144129 in previous changelog entry + + -- Simon McVittie Wed, 23 Sep 2026 13:16:29 +0100 + xdg-dbus-proxy (0.1.6-1+deb13u2) trixie-security; urgency=high * d/p/GHSA-r7hp-698j-2h6c/*.patch: @@ -5,6 +17,7 @@ In 0.1.6 and 0.1.7, a malicious or compromised Flatpak app could receive any broadcast D-Bus message on the session bus or the AT-SPI bus, leading to unintended information disclosure. + (CVE-2026-93676, GHSA-r7hp-698j-2h6c, Closes: #1144129) * d/p/tests/*.patch: Add automated test coverage for message filtering diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,144 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:06:35 +0100 +Subject: proxy: Don't assume that only returns and errors have a reply-serial + +The D-Bus session bus (which is not designed to be a security boundary) +does not actually prevent the "in reply to" field from being set on +method calls and signals, even though that makes little sense. + +We must not treat messages as being a legitimate reply unless they +are genuinely of a message type that acts as a reply: otherwise, a +malicious sandboxed app could receive a method call, delay or suppress +the reply, and send back its own method call or unicast signal that +pretends to be a reply to the method call, which could trick the +caller into doing something undesirable (potentially arbitrary +code execution, depending on the caller). + +Partially addresses CVE-2026-94422 + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + flatpak-proxy.c | 45 ++++++++++++++++++++++++++++++--------------- + 1 file changed, 30 insertions(+), 15 deletions(-) + +diff --git a/flatpak-proxy.c b/flatpak-proxy.c +index a97e62b..ab50168 100644 +--- a/flatpak-proxy.c ++++ b/flatpak-proxy.c +@@ -252,7 +252,6 @@ typedef struct + const char *destination; + const char *sender; + const char *signature; +- gboolean has_reply_serial; + guint32 reply_serial; + guint32 unix_fds; + } Header; +@@ -1219,6 +1218,22 @@ header_debug_str (GString *s, Header *header) + return s->str; + } + ++static gboolean ++is_reply (Header *header) ++{ ++ switch (header->type) ++ { ++ case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: ++ case G_DBUS_MESSAGE_TYPE_ERROR: ++ return TRUE; ++ ++ case G_DBUS_MESSAGE_TYPE_METHOD_CALL: ++ case G_DBUS_MESSAGE_TYPE_SIGNAL: ++ default: ++ return FALSE; ++ } ++} ++ + static Header * + parse_header (Buffer *buffer, GError **error) + { +@@ -1226,6 +1241,7 @@ parse_header (Buffer *buffer, GError **error) + guint32 offset, end_offset; + guint8 header_type; + const char *signature; ++ gboolean has_reply_serial = FALSE; + g_autoptr(GError) str_error = NULL; + g_autoptr(GString) header_str = NULL; + +@@ -1450,7 +1466,7 @@ parse_header (Buffer *buffer, GError **error) + return NULL; + } + +- header->has_reply_serial = TRUE; ++ has_reply_serial = TRUE; + header->reply_serial = read_uint32 (header, &buffer->data[offset]); + offset += 4; + break; +@@ -1568,7 +1584,7 @@ parse_header (Buffer *buffer, GError **error) + break; + + case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: +- if (!header->has_reply_serial) ++ if (!has_reply_serial) + { + g_set_error (error, + G_IO_ERROR, +@@ -1580,7 +1596,7 @@ parse_header (Buffer *buffer, GError **error) + break; + + case G_DBUS_MESSAGE_TYPE_ERROR: +- if (header->error_name == NULL || !header->has_reply_serial) ++ if (header->error_name == NULL || !has_reply_serial) + { + g_set_error (error, + G_IO_ERROR, +@@ -1626,6 +1642,12 @@ parse_header (Buffer *buffer, GError **error) + return NULL; + } + ++ /* Invariant: every reply has a reply serial. ++ * (Note that the converse is not true: it is technically possible to ++ * send a method call or signal that claims to be a reply.) */ ++ if (is_reply (header)) ++ g_assert (has_reply_serial); ++ + return g_steal_pointer (&header); + } + +@@ -1978,7 +2000,7 @@ get_dbus_method_handler (FlatpakProxyClient *client, Header *header) + + g_autoptr(GList) filters = NULL; + +- if (header->has_reply_serial) ++ if (is_reply (header)) + { + ExpectedReplyType expected_reply = + steal_expected_reply (&client->bus_side, +@@ -2670,7 +2692,7 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + if (client->proxy->log_messages) + print_incoming_header (header); + +- if (header->has_reply_serial) ++ if (is_reply (header)) + { + expected_reply = steal_expected_reply (get_other_side (side), header->reply_serial); + +@@ -2779,15 +2801,8 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + } + else /* Not reply */ + { +- +- /* Don't allow reply types with no reply_serial */ +- if (header->type == G_DBUS_MESSAGE_TYPE_METHOD_RETURN || +- header->type == G_DBUS_MESSAGE_TYPE_ERROR) +- { +- if (client->proxy->log_messages) +- g_print ("*Invalid reply*\n"); +- g_clear_pointer (&buffer, buffer_unref); +- } ++ g_assert (header->type != G_DBUS_MESSAGE_TYPE_METHOD_RETURN); ++ g_assert (header->type != G_DBUS_MESSAGE_TYPE_ERROR); + + /* We filter all NameOwnerChanged signal according to the policy */ + if (message_is_name_owner_changed (client, header)) diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,138 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:14:08 +0100 +Subject: proxy: Make it clearer which direction messages are going in + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + flatpak-proxy.c | 40 +++++++++++++++++++++++++++++++--------- + 1 file changed, 31 insertions(+), 9 deletions(-) + +diff --git a/flatpak-proxy.c b/flatpak-proxy.c +index ab50168..61d7094 100644 +--- a/flatpak-proxy.c ++++ b/flatpak-proxy.c +@@ -1071,6 +1071,13 @@ queue_expected_reply (ProxySide *side, guint32 serial, ExpectedReplyType type) + GUINT_TO_POINTER (type)); + } + ++/* ++ * @side: Either the bus or the sandboxed client ++ * @serial: The reply_serial field of the reply ++ * ++ * Returns: The type of the matching reply, or %EXPECTED_REPLY_NONE ++ * if no match was found. ++ */ + static ExpectedReplyType + steal_expected_reply (ProxySide *side, guint32 serial) + { +@@ -1992,6 +1999,9 @@ is_introspection_call (Header *header) + g_strcmp0 (header->interface, "org.freedesktop.DBus.Introspectable") == 0; + } + ++/* ++ * @header: an outgoing message from the sandboxed client to the bus ++ */ + static BusHandler + get_dbus_method_handler (FlatpakProxyClient *client, Header *header) + { +@@ -2493,12 +2503,17 @@ queue_wildcard_initial_name_ops (FlatpakProxyClient *client, Header *header, Buf + } + } + +- ++/* ++ * Called when we received @buffer from the client side of @client: ++ * it's an outgoing message from the sandboxed client to the bus. ++ */ + static void + got_buffer_from_client (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer) + { + ExpectedReplyType expecting_reply = EXPECTED_REPLY_NONE; + ++ g_assert (side == &client->client_side); ++ + if (client->auth_state == AUTH_COMPLETE && client->proxy->filter) + { + g_autoptr(Header) header = NULL; +@@ -2512,18 +2527,18 @@ got_buffer_from_client (FlatpakProxyClient *client, ProxySide *side, Buffer *buf + { + g_warning ("Invalid message header format from client: %s", + error->message); +- side_closed (side); ++ side_closed (&client->client_side); + buffer_unref (buffer); + return; + } + +- if (!update_socket_messages (side, buffer, header)) ++ if (!update_socket_messages (&client->client_side, buffer, header)) + return; + + if (header->serial > MAX_CLIENT_SERIAL) + { + g_warning ("Invalid client serial: Exceeds maximum value of %u", MAX_CLIENT_SERIAL); +- side_closed (side); ++ side_closed (&client->client_side); + buffer_unref (buffer); + return; + } +@@ -2653,7 +2668,7 @@ handle_deny: + } + + if (buffer != NULL && expecting_reply != EXPECTED_REPLY_NONE) +- queue_expected_reply (side, header->serial, expecting_reply); ++ queue_expected_reply (&client->client_side, header->serial, expecting_reply); + } + + if (buffer) +@@ -2663,9 +2678,15 @@ handle_deny: + queue_initial_name_ops (client); + } + ++/* ++ * Called when we received @buffer from the bus side of @client: ++ * it's an incoming message from the bus to the sandboxed client. ++ */ + static void + got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer) + { ++ g_assert (side == &client->bus_side); ++ + if (client->auth_state == AUTH_COMPLETE && client->proxy->filter) + { + g_autoptr(Header) header = NULL; +@@ -2682,11 +2703,11 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + g_warning ("Invalid message header format from bus: %s", + error->message); + buffer_unref (buffer); +- side_closed (side); ++ side_closed (&client->bus_side); + return; + } + +- if (!update_socket_messages (side, buffer, header)) ++ if (!update_socket_messages (&client->bus_side, buffer, header)) + return; + + if (client->proxy->log_messages) +@@ -2694,7 +2715,8 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + + if (is_reply (header)) + { +- expected_reply = steal_expected_reply (get_other_side (side), header->reply_serial); ++ expected_reply = steal_expected_reply (&client->client_side, ++ header->reply_serial); + + switch (expected_reply) + { +@@ -2842,7 +2864,7 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + flatpak_proxy_client_update_unique_id_policy (client, header->sender, FLATPAK_POLICY_SEE); + + if (buffer && client_message_generates_reply (header)) +- queue_expected_reply (side, header->serial, EXPECTED_REPLY_NORMAL); ++ queue_expected_reply (&client->bus_side, header->serial, EXPECTED_REPLY_NORMAL); + } + + if (buffer) diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,361 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:15:10 +0100 +Subject: proxy: Only allow replies to go to the correct destination + +If peer `:1.0` (perhaps a MPRIS client) calls a method on the sandboxed +process with serial number 3, then it's invalid for the sandboxed process +to send a reply to an unrelated peer `:1.1`, even if that forged reply +has also been marked as "in reply to serial number 3". + +This introduces an asymmetry into the tracking of expected replies, +so move the data structures used for that tracking from the ProxySide +into the Client. When the bus side calls a method on the sandboxed side, +we need to track (serial, caller) pairs to enforce that the reply goes +back to the original caller. However, when the sandboxed side calls a +method on the bus side, we don't need to to track the caller because +there is only one possible caller on the sandboxed side. We also +*can't* track the caller on the sandboxed side, because to do that we +would need to know its D-Bus unique name, and until it has successfully +called Hello() no such name has been assigned. + +Resolves CVE-2026-94422 when combined with earlier fixes. + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + flatpak-proxy.c | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++------ + 1 file changed, 183 insertions(+), 21 deletions(-) + +diff --git a/flatpak-proxy.c b/flatpak-proxy.c +index 61d7094..d691b65 100644 +--- a/flatpak-proxy.c ++++ b/flatpak-proxy.c +@@ -123,9 +123,7 @@ + * Once authenticated we receive incoming messages one at a time, + * and then we demarshal the message headers to make routing decisions. + * This means we trust the bus to do message format validation, etc. +- * (because we don't parse the body). Also we assume that the bus verifies +- * reply_serials, i.e. that a reply can only be sent once and by the real +- * recipient of an previously sent method call. ++ * (because we don't parse the body). + * + * Serial numbers larger than MAX_CLIENT_SERIAL reserved for messages created by the + * proxy itself (fake messages). This limits the possible values of serials +@@ -280,6 +278,61 @@ typedef struct + static void header_free (Header *header); + G_DEFINE_AUTOPTR_CLEANUP_FUNC (Header, header_free) + ++/* ++ * A pending method call from @caller on the bus to the client, ++ * for which we expect a reply from the client back to @caller, ++ * represented as a single memory block. ++ */ ++typedef struct ++{ ++ guint32 serial; ++ char caller[]; ++} ExpectedReplyFromClient; ++ ++static ExpectedReplyFromClient * ++expected_reply_from_client_new (guint32 serial, ++ const char *caller) ++{ ++ ExpectedReplyFromClient *ret; ++ size_t len; ++ ++ /* A missing destination on the method call means a method call from ++ * the message bus itself (unlikely, but possible). ++ * We represent this as the empty string (which is not a valid bus name) ++ * to avoid needing a separate representation for NULL. */ ++ if (caller == NULL) ++ caller = ""; ++ ++ len = strlen (caller) + 1; ++ ++ ret = g_malloc0 (sizeof (*ret) + len); ++ ret->serial = serial; ++ memcpy (&ret->caller, caller, len); ++ return ret; ++} ++ ++/* Because it's a single memory block, we can just g_free() it. */ ++#define expected_reply_from_client_free g_free ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (ExpectedReplyFromClient, expected_reply_from_client_free) ++ ++static gboolean ++expected_reply_from_client_equal (const void *v1, ++ const void *v2) ++{ ++ const ExpectedReplyFromClient *left = v1; ++ const ExpectedReplyFromClient *right = v2; ++ ++ return left->serial == right->serial && g_str_equal (&left->caller, &right->caller); ++} ++ ++static guint ++expected_reply_from_client_hash (const void *v) ++{ ++ const ExpectedReplyFromClient *self = v; ++ ++ return self->serial ^ g_str_hash (&self->caller); ++} ++ + typedef struct + { + gboolean got_first_byte; /* always true on bus side */ +@@ -296,8 +349,6 @@ typedef struct + + GList *buffers; /* to be sent */ + GList *control_messages; +- +- GHashTable *expected_replies; + } ProxySide; + + struct FlatpakProxyClient +@@ -314,6 +365,31 @@ struct FlatpakProxyClient + ProxySide client_side; + ProxySide bus_side; + ++ /* (element-type ExpectedReplyFromClient ExpectedReplyType) ++ * Map from message serial number and caller to expected replies. ++ * If this map contains ++ * { { serial, caller }: EXPECTED_REPLY_FOO }, ++ * then it means that @caller on the bus sent a method call to the ++ * sandboxed client with SERIAL=serial. ++ * This means that in future we're expecting the sandboxed client to ++ * respond with a message back to @caller with REPLY_SERIAL=serial. */ ++ GHashTable *expected_replies_from_client; ++ ++ /* (element-type guint ExpectedReplyType) ++ * Map from message serial number to expected reply type. ++ * If this map contains ++ * { serial: EXPECTED_REPLY_FOO }, ++ * then it means that the sandboxed client sent a method call to the ++ * bus with SERIAL=serial. ++ * This means that in future we're expecting something on the bus to ++ * respond with a message that has REPLY_SERIAL=serial. ++ * This direction is simpler than the other way round because we don't ++ * need to distinguish between destinations: we trust the bus, ++ * and there is only one possible destination on the sandboxed client ++ * side anyway. ++ */ ++ GHashTable *expected_replies_from_bus; ++ + /* Filtering data: */ + guint32 hello_serial; + guint32 last_fake_serial; +@@ -415,8 +491,6 @@ free_side (ProxySide *side) + g_source_destroy (side->in_source); + if (side->out_source) + g_source_destroy (side->out_source); +- +- g_hash_table_destroy (side->expected_replies); + } + + static void +@@ -432,6 +506,8 @@ flatpak_proxy_client_finalize (GObject *object) + g_hash_table_destroy (client->get_owner_reply); + g_hash_table_destroy (client->unique_id_policy); + g_hash_table_destroy (client->unique_id_owned_names); ++ g_hash_table_destroy (client->expected_replies_from_bus); ++ g_hash_table_destroy (client->expected_replies_from_client); + + free_side (&client->client_side); + free_side (&client->bus_side); +@@ -455,7 +531,6 @@ init_side (FlatpakProxyClient *client, ProxySide *side) + side->header_buffer.size = 16; + side->header_buffer.pos = 0; + side->current_read_buffer = &side->header_buffer; +- side->expected_replies = g_hash_table_new (g_direct_hash, g_direct_equal); + } + + static void +@@ -470,6 +545,11 @@ flatpak_proxy_client_init (FlatpakProxyClient *client) + client->get_owner_reply = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free); + client->unique_id_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + client->unique_id_owned_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) string_list_free); ++ client->expected_replies_from_client = g_hash_table_new_full (expected_reply_from_client_hash, ++ expected_reply_from_client_equal, ++ expected_reply_from_client_free, ++ NULL); ++ client->expected_replies_from_bus = g_hash_table_new (g_direct_hash, g_direct_equal); + } + + static FlatpakProxyClient * +@@ -1063,34 +1143,102 @@ side_out_cb (GSocket *socket, GIOCondition condition, gpointer user_data) + } + } + ++/* ++ * A method call was received from the sandboxed client, ++ * with serial number @serial. ++ * We now expect a reply from the bus side, of type @type. ++ * We do not need to track the expected destination, ++ * because there is only one destination on the client side ++ * (and we don't know its name until we have delivered the Hello reply). ++ */ + static void +-queue_expected_reply (ProxySide *side, guint32 serial, ExpectedReplyType type) ++queue_expected_reply_from_bus (FlatpakProxyClient *client, ++ guint32 serial, ++ ExpectedReplyType type) + { +- g_hash_table_replace (side->expected_replies, ++ g_return_if_fail (serial != 0); ++ g_return_if_fail (type != EXPECTED_REPLY_NONE); ++ ++ g_hash_table_replace (client->expected_replies_from_bus, + GUINT_TO_POINTER (serial), + GUINT_TO_POINTER (type)); + } + + /* +- * @side: Either the bus or the sandboxed client ++ * A method call was received from sender @caller on the bus, ++ * with serial number @serial. ++ * We now expect a reply from the sandboxed client of type @type. ++ */ ++static void ++queue_expected_reply_from_client (FlatpakProxyClient *client, ++ guint32 serial, ++ const char *caller, ++ ExpectedReplyType type) ++{ ++ g_return_if_fail (serial != 0); ++ g_return_if_fail (caller == NULL || caller[0] != '\0'); ++ g_return_if_fail (type != EXPECTED_REPLY_NONE); ++ ++ g_hash_table_replace (client->expected_replies_from_client, ++ expected_reply_from_client_new (serial, caller), ++ GUINT_TO_POINTER (type)); ++} ++ ++ ++/* ++ * @client: The client + * @serial: The reply_serial field of the reply + * + * Returns: The type of the matching reply, or %EXPECTED_REPLY_NONE + * if no match was found. + */ + static ExpectedReplyType +-steal_expected_reply (ProxySide *side, guint32 serial) ++steal_expected_reply_from_bus (FlatpakProxyClient *client, ++ guint32 serial) + { + ExpectedReplyType type; + +- type = GPOINTER_TO_UINT (g_hash_table_lookup (side->expected_replies, ++ g_return_val_if_fail (serial != 0, EXPECTED_REPLY_NONE); ++ ++ type = GPOINTER_TO_UINT (g_hash_table_lookup (client->expected_replies_from_bus, + GUINT_TO_POINTER (serial))); + if (type) +- g_hash_table_remove (side->expected_replies, ++ g_hash_table_remove (client->expected_replies_from_bus, + GUINT_TO_POINTER (serial)); + return type; + } + ++/* ++ * @client: The client ++ * @serial: The reply_serial field of the reply ++ * @caller: Only match pending method calls that came from @caller ++ * (and therefore the reply should be sent to @caller). ++ * ++ * Returns: The type of the matching reply, or %EXPECTED_REPLY_NONE ++ * if no match was found. ++ */ ++static ExpectedReplyType ++steal_expected_reply_from_client (FlatpakProxyClient *client, ++ guint32 serial, ++ const char *caller) ++{ ++ g_autoptr(ExpectedReplyFromClient) key = NULL; ++ void *type; ++ ++ g_return_val_if_fail (serial != 0, EXPECTED_REPLY_NONE); ++ g_return_val_if_fail (caller == NULL || caller[0] != '\0', EXPECTED_REPLY_NONE); ++ ++ key = expected_reply_from_client_new (serial, caller); ++ ++ if (g_hash_table_lookup_extended (client->expected_replies_from_client, key, ++ NULL, &type)) ++ { ++ g_hash_table_remove (client->expected_replies_from_client, key); ++ return GPOINTER_TO_UINT (type); ++ } ++ ++ return EXPECTED_REPLY_NONE; ++} + + static void + queue_outgoing_buffer (ProxySide *side, Buffer *buffer) +@@ -2010,11 +2158,17 @@ get_dbus_method_handler (FlatpakProxyClient *client, Header *header) + + g_autoptr(GList) filters = NULL; + ++ /* If the message claims to be a reply to a method call with serial number n, ++ * we allow if and only if there is indeed a pending method call from ++ * the reply's destination, with serial number n, and it has not yet had ++ * its reply */ + if (is_reply (header)) + { + ExpectedReplyType expected_reply = +- steal_expected_reply (&client->bus_side, +- header->reply_serial); ++ steal_expected_reply_from_client (client, ++ header->reply_serial, ++ header->destination); ++ + if (expected_reply == EXPECTED_REPLY_NONE) + return HANDLE_DENY; + +@@ -2378,7 +2532,7 @@ queue_fake_message (FlatpakProxyClient *client, GDBusMessage *message, ExpectedR + g_object_unref (message); + + queue_outgoing_buffer (&client->bus_side, buffer); +- queue_expected_reply (&client->client_side, client->last_fake_serial, reply_type); ++ queue_expected_reply_from_bus (client, client->last_fake_serial, reply_type); + } + + /* After the first Hello message we need to synthesize a bunch of messages to synchronize the +@@ -2668,7 +2822,7 @@ handle_deny: + } + + if (buffer != NULL && expecting_reply != EXPECTED_REPLY_NONE) +- queue_expected_reply (&client->client_side, header->serial, expecting_reply); ++ queue_expected_reply_from_bus (client, header->serial, expecting_reply); + } + + if (buffer) +@@ -2715,8 +2869,14 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + + if (is_reply (header)) + { +- expected_reply = steal_expected_reply (&client->client_side, +- header->reply_serial); ++ /* If the client previously made a method call out to the bus ++ * with serial number n, we allow one reply to come from ++ * the bus "in reply to: n" back to the client. ++ * We allow any destination - this is OK, because we trust ++ * the bus, and in any case there is only one valid destination ++ * on the client side (it's the sandboxed client). */ ++ expected_reply = steal_expected_reply_from_bus (client, ++ header->reply_serial); + + switch (expected_reply) + { +@@ -2863,8 +3023,10 @@ got_buffer_from_bus (FlatpakProxyClient *client, ProxySide *side, Buffer *buffer + if (buffer && header->sender && header->sender[0] == ':') + flatpak_proxy_client_update_unique_id_policy (client, header->sender, FLATPAK_POLICY_SEE); + ++ /* Remember that the sandboxed client is allowed to send a single ++ * reply back to header->sender, marked "in reply to" header->serial. */ + if (buffer && client_message_generates_reply (header)) +- queue_expected_reply (&client->bus_side, header->serial, EXPECTED_REPLY_NORMAL); ++ queue_expected_reply_from_client (client, header->serial, header->sender, EXPECTED_REPLY_NORMAL); + } + + if (buffer) diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,216 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:01:14 +0100 +Subject: tests: Add basic test coverage for reply handling + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + tests/test-proxy.c | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 145 insertions(+) + +diff --git a/tests/test-proxy.c b/tests/test-proxy.c +index 777c1d7..9bcf617 100644 +--- a/tests/test-proxy.c ++++ b/tests/test-proxy.c +@@ -40,6 +40,7 @@ + #define DBUS_REQUEST_NAME_REPLY_EXISTS 3 + #define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4 + ++#define CALLER_NAME "com.example.Caller" + #define CANNOT_ACCESS_NAME "com.example.CannotAccess" + #define CAN_SEE_NAME "com.example.CanSee" + #define CAN_TALK_NAME "com.example.CanTalk" +@@ -60,6 +61,9 @@ + #define CAN_RECEIVE_SOME_SIGNAL "JustThisSignal" + #define CAN_RECEIVE_SOME_PATH "/just/this/path" + ++#define IGNORE_IFACE "com.example.Ignore" ++#define IGNORE_METHOD "Ignore" ++ + static void + ready_cb (GObject *source_object, + GAsyncResult *result, +@@ -109,6 +113,7 @@ connection_clear (Connection *self) + typedef struct + { + Connection proxied; ++ Connection caller_conn; + Connection cannot_access_conn; + Connection can_see_conn; + Connection can_talk_conn; +@@ -167,6 +172,16 @@ conn_filter_cb (GDBusConnection *conn, + case G_DBUS_MESSAGE_TYPE_METHOD_CALL: + g_test_message ("%s got method call", conn_info->label); + g_atomic_int_inc (&conn_info->n_method_calls); ++ ++ if (g_strcmp0 (iface, IGNORE_IFACE) == 0 && ++ g_strcmp0 (member, IGNORE_METHOD) == 0) ++ { ++ g_test_message ("method call is %s.%s, ignoring", ++ iface, member); ++ g_clear_object (&message); ++ return NULL; ++ } ++ + break; + + case G_DBUS_MESSAGE_TYPE_SIGNAL: +@@ -268,6 +283,51 @@ fixture_connect (Fixture *f, + NULL); + } + ++static void ++do_round_trip (const Connection *caller, ++ const Connection *destination) ++{ ++ g_autoptr(GAsyncResult) result = NULL; ++ g_autoptr(GError) error = NULL; ++ g_autoptr(GVariant) tuple = NULL; ++ ++ g_dbus_connection_call (caller->conn, ++ destination->unique_name, ++ "/", ++ EXAMPLE_IFACE, ++ EXAMPLE_METHOD, ++ NULL, ++ G_VARIANT_TYPE ("()"), ++ G_DBUS_CALL_FLAGS_NONE, ++ -1, /* timeout */ ++ NULL, /* cancellable */ ++ ready_cb, ++ &result); ++ ++ while (result == NULL) ++ g_main_context_iteration (NULL, TRUE); ++ ++ tuple = g_dbus_connection_call_finish (caller->conn, result, &error); ++ ++ /* For simplicity we didn't actually implement any method calls, ++ * so the result should be an error. */ ++ g_assert_nonnull (error); ++ g_assert_null (tuple); ++ g_assert_cmpstr (g_quark_to_string (error->domain), ==, g_quark_to_string (G_DBUS_ERROR)); ++ ++ switch (error->code) ++ { ++ case G_DBUS_ERROR_UNKNOWN_METHOD: ++ case G_DBUS_ERROR_UNKNOWN_INTERFACE: ++ case G_DBUS_ERROR_UNKNOWN_OBJECT: ++ /* OK */ ++ break; ++ ++ default: ++ g_assert_not_reached (); ++ } ++} ++ + static void + setup (Fixture *f, + gconstpointer context G_GNUC_UNUSED) +@@ -346,6 +406,7 @@ setup (Fixture *f, + g_free, + NULL); + ++ fixture_connect (f, &f->caller_conn, CALLER_NAME); + fixture_connect (f, &f->cannot_access_conn, CANNOT_ACCESS_NAME); + fixture_connect (f, &f->can_see_conn, CAN_SEE_NAME); + fixture_connect (f, &f->can_talk_conn, CAN_TALK_NAME); +@@ -824,6 +885,89 @@ test_receive (Fixture *f, + } + } + ++typedef struct ++{ ++ const char *label; ++ GDBusMessageType type; ++ unsigned broadcast : 1; ++} ReplyTest; ++ ++static const ReplyTest reply_tests[] = ++{ ++ { "should be able to send a method return in reply", ++ G_DBUS_MESSAGE_TYPE_METHOD_RETURN }, ++ { "should be able to send an error in reply", ++ G_DBUS_MESSAGE_TYPE_ERROR }, ++}; ++ ++static void ++test_reply (Fixture *f, ++ gconstpointer context G_GNUC_UNUSED) ++{ ++ alarm (30); ++ fixture_start_proxy (f); ++ ++ for (size_t i = 0; i < G_N_ELEMENTS (reply_tests); i++) ++ { ++ const ReplyTest *t = &reply_tests[i]; ++ g_autoptr(GDBusMessage) call = NULL; ++ g_autoptr(GDBusMessage) reply = NULL; ++ g_autoptr(GError) error = NULL; ++ guint32 call_serial = 0; ++ g_test_message ("#%zu: %s", i, t->label); ++ ++ call = g_dbus_message_new_method_call (f->proxied.unique_name, ++ "/", ++ IGNORE_IFACE, ++ IGNORE_METHOD); ++ g_dbus_connection_send_message (f->caller_conn.conn, ++ call, ++ G_DBUS_SEND_MESSAGE_FLAGS_NONE, ++ &call_serial, ++ &error); ++ g_assert_no_error (error); ++ g_assert_cmpuint (call_serial, !=, 0); ++ g_test_message ("Method call was serial number %u", call_serial); ++ ++ /* Do a round-trip from the caller to the sandboxed connection and back. ++ * D-Bus messages are delivered sequentially, so by the time this call ++ * has finished, the method call will also have passed through the ++ * xdg-dbus-proxy and the dbus-daemon. */ ++ do_round_trip (&f->caller_conn, &f->proxied); ++ ++ switch (t->type) ++ { ++ case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: ++ g_test_message ("Sending legitimate reply as a reply"); ++ reply = g_dbus_message_new_method_reply (call); ++ break; ++ ++ case G_DBUS_MESSAGE_TYPE_ERROR: ++ g_test_message ("Sending legitimate error as a reply"); ++ reply = g_dbus_message_new_method_error (call, ++ "com.example.No", ++ "That didn't work"); ++ break; ++ ++ case G_DBUS_MESSAGE_TYPE_SIGNAL: ++ case G_DBUS_MESSAGE_TYPE_METHOD_CALL: ++ case G_DBUS_MESSAGE_TYPE_INVALID: ++ default: ++ g_assert_not_reached (); ++ } ++ ++ g_dbus_message_set_reply_serial (reply, call_serial); ++ g_dbus_connection_send_message (f->proxied.conn, reply, ++ G_DBUS_SEND_MESSAGE_FLAGS_NONE, ++ NULL, /* serial */ ++ &error); ++ g_assert_no_error (error); ++ ++ /* Do another round-trip, to make sure everything has been delivered */ ++ do_round_trip (&f->caller_conn, &f->proxied); ++ } ++} ++ + static void + teardown (Fixture *f, + gconstpointer context G_GNUC_UNUSED) +@@ -895,6 +1039,7 @@ main (int argc, + g_test_add ("/call", Fixture, NULL, setup, test_call, teardown); + g_test_add ("/own", Fixture, NULL, setup, test_own, teardown); + g_test_add ("/receive", Fixture, NULL, setup, test_receive, teardown); ++ g_test_add ("/reply", Fixture, NULL, setup, test_reply, teardown); + + return g_test_run (); + } diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,194 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:33:04 +0100 +Subject: tests: Assert that forged replies cannot be sent to wrong + destination + +Reproduces part of CVE-2026-94422 + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + tests/test-proxy.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++------- + 1 file changed, 86 insertions(+), 11 deletions(-) + +diff --git a/tests/test-proxy.c b/tests/test-proxy.c +index 21da01a..062e678 100644 +--- a/tests/test-proxy.c ++++ b/tests/test-proxy.c +@@ -66,6 +66,7 @@ + + #define MALICIOUS_IFACE "com.example.Malice" + #define MALICIOUS_MEMBER "ShouldNotBeAllowed" ++#define MALICIOUS_BODY "Message containing this body should have been blocked" + + static void + ready_cb (GObject *source_object, +@@ -206,11 +207,22 @@ conn_filter_cb (GDBusConnection *conn, + break; + + case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: +- g_test_message ("%s got method reply", conn_info->label); +- break; +- + case G_DBUS_MESSAGE_TYPE_ERROR: +- g_test_message ("%s got error reply", conn_info->label); ++ if (type == G_DBUS_MESSAGE_TYPE_METHOD_RETURN) ++ g_test_message ("%s got method reply", conn_info->label); ++ else ++ g_test_message ("%s got error reply", conn_info->label); ++ ++ if (g_str_equal (g_dbus_message_get_signature (message), "s")) ++ { ++ const char *s = NULL; ++ ++ g_variant_get (g_dbus_message_get_body (message), "(&s)", &s); ++ ++ if (g_str_equal (s, MALICIOUS_BODY)) ++ g_error ("Forged reply received"); ++ } ++ + break; + + case G_DBUS_MESSAGE_TYPE_INVALID: +@@ -893,6 +905,7 @@ typedef struct + const char *label; + GDBusMessageType type; + unsigned broadcast : 1; ++ unsigned misdirected : 1; + } ReplyTest; + + static const ReplyTest reply_tests[] = +@@ -901,12 +914,20 @@ static const ReplyTest reply_tests[] = + G_DBUS_MESSAGE_TYPE_SIGNAL }, + { "should not be able to send a broadcast signal in reply", + G_DBUS_MESSAGE_TYPE_SIGNAL, .broadcast = TRUE }, ++ { "should not be able to send a unicast signal to someone else in reply", ++ G_DBUS_MESSAGE_TYPE_SIGNAL, .misdirected = TRUE }, + { "should not be able to send a method call in reply", + G_DBUS_MESSAGE_TYPE_METHOD_CALL }, ++ { "should not be able to send a method call to someone else in reply", ++ G_DBUS_MESSAGE_TYPE_METHOD_CALL, .misdirected = TRUE }, + { "should be able to send a method return in reply", + G_DBUS_MESSAGE_TYPE_METHOD_RETURN }, ++ { "should not be able to send a method return to someone else in reply", ++ G_DBUS_MESSAGE_TYPE_METHOD_RETURN, .misdirected = TRUE }, + { "should be able to send an error in reply", + G_DBUS_MESSAGE_TYPE_ERROR }, ++ { "should not be able to send an error to someone else in reply", ++ G_DBUS_MESSAGE_TYPE_ERROR, .misdirected = TRUE }, + }; + + static void +@@ -930,12 +951,20 @@ test_reply (Fixture *f, + int caller_calls_before; + int caller_broadcast_before; + int caller_unicast_before; ++ int other_calls_before; ++ int other_broadcast_before; ++ int other_unicast_before; ++ + g_test_message ("#%zu: %s", i, t->label); + + caller_calls_before = g_atomic_int_get (&f->caller_conn.n_method_calls); + caller_unicast_before = g_atomic_int_get (&f->caller_conn.n_unicast_signals); + caller_broadcast_before = g_atomic_int_get (&f->caller_conn.n_broadcasts); + ++ other_calls_before = g_atomic_int_get (&f->cannot_access_conn.n_method_calls); ++ other_unicast_before = g_atomic_int_get (&f->cannot_access_conn.n_unicast_signals); ++ other_broadcast_before = g_atomic_int_get (&f->cannot_access_conn.n_broadcasts); ++ + call = g_dbus_message_new_method_call (f->proxied.unique_name, + "/", + IGNORE_IFACE, +@@ -966,6 +995,11 @@ test_reply (Fixture *f, + { + g_test_message ("Sending malicious broadcast signal as a reply"); + } ++ else if (t->misdirected) ++ { ++ g_test_message ("Sending malicious unicast signal reply to wrong destination"); ++ g_dbus_message_set_destination (reply, f->cannot_access_conn.unique_name); ++ } + else + { + g_test_message ("Sending malicious unicast signal as a reply"); +@@ -975,23 +1009,56 @@ test_reply (Fixture *f, + break; + + case G_DBUS_MESSAGE_TYPE_METHOD_CALL: +- g_test_message ("Sending malicious method call as a reply"); +- reply = g_dbus_message_new_method_call (f->caller_conn.unique_name, +- "/", +- MALICIOUS_IFACE, +- MALICIOUS_MEMBER); ++ if (t->misdirected) ++ { ++ g_test_message ("Sending malicious method call to wrong destination"); ++ reply = g_dbus_message_new_method_call (f->cannot_access_conn.unique_name, ++ "/", ++ MALICIOUS_IFACE, ++ MALICIOUS_MEMBER); ++ } ++ else ++ { ++ g_test_message ("Sending malicious method call as a reply"); ++ reply = g_dbus_message_new_method_call (f->caller_conn.unique_name, ++ "/", ++ MALICIOUS_IFACE, ++ MALICIOUS_MEMBER); ++ } + break; + + case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: +- g_test_message ("Sending legitimate reply as a reply"); + reply = g_dbus_message_new_method_reply (call); ++ ++ if (t->misdirected) ++ { ++ g_test_message ("Sending malicious reply to wrong destination"); ++ g_dbus_message_set_destination (reply, f->cannot_access_conn.unique_name); ++ g_dbus_message_set_body (reply, g_variant_new ("(s)", MALICIOUS_BODY)); ++ } ++ else ++ { ++ g_test_message ("Sending legitimate reply as a reply"); ++ } ++ + break; + + case G_DBUS_MESSAGE_TYPE_ERROR: +- g_test_message ("Sending legitimate error as a reply"); + reply = g_dbus_message_new_method_error (call, + "com.example.No", + "That didn't work"); ++ ++ if (t->misdirected) ++ { ++ g_test_message ("Sending malicious error to wrong destination"); ++ g_dbus_message_set_destination (reply, f->cannot_access_conn.unique_name); ++ g_dbus_message_set_body (reply, g_variant_new ("(s)", MALICIOUS_BODY)); ++ } ++ else ++ { ++ g_test_message ("Sending legitimate error as a reply"); ++ } ++ + break; + + case G_DBUS_MESSAGE_TYPE_INVALID: +@@ -1016,6 +1083,14 @@ test_reply (Fixture *f, + caller_unicast_before); + g_assert_cmpint (g_atomic_int_get (&f->caller_conn.n_broadcasts), ==, + caller_broadcast_before); ++ ++ /* The other connection didn't receive any messages at all */ ++ g_assert_cmpint (g_atomic_int_get (&f->cannot_access_conn.n_method_calls), ==, ++ other_calls_before); ++ g_assert_cmpint (g_atomic_int_get (&f->cannot_access_conn.n_unicast_signals), ==, ++ other_unicast_before); ++ g_assert_cmpint (g_atomic_int_get (&f->cannot_access_conn.n_broadcasts), ==, ++ other_broadcast_before); + } + } + diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch --- xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch 1970-01-01 00:00:00.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch 2026-09-23 12:16:29.000000000 +0000 @@ -0,0 +1,123 @@ +From: Simon McVittie +Date: Mon, 7 Sep 2026 15:30:32 +0100 +Subject: tests: Assert that "replies" of inappropriate types aren't accepted + +Regression test for part of CVE-2026-94422 + +Helps: CVE-2026-94422 +Helps: https://github.com/flatpak/xdg-dbus-proxy/security/advisories/GHSA-2cgv-pwcq-wvpq +Signed-off-by: Simon McVittie +--- + tests/test-proxy.c | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 53 insertions(+), 2 deletions(-) + +diff --git a/tests/test-proxy.c b/tests/test-proxy.c +index 9bcf617..21da01a 100644 +--- a/tests/test-proxy.c ++++ b/tests/test-proxy.c +@@ -64,6 +64,9 @@ + #define IGNORE_IFACE "com.example.Ignore" + #define IGNORE_METHOD "Ignore" + ++#define MALICIOUS_IFACE "com.example.Malice" ++#define MALICIOUS_MEMBER "ShouldNotBeAllowed" ++ + static void + ready_cb (GObject *source_object, + GAsyncResult *result, +@@ -894,6 +897,12 @@ typedef struct + + static const ReplyTest reply_tests[] = + { ++ { "should not be able to send a unicast signal in reply", ++ G_DBUS_MESSAGE_TYPE_SIGNAL }, ++ { "should not be able to send a broadcast signal in reply", ++ G_DBUS_MESSAGE_TYPE_SIGNAL, .broadcast = TRUE }, ++ { "should not be able to send a method call in reply", ++ G_DBUS_MESSAGE_TYPE_METHOD_CALL }, + { "should be able to send a method return in reply", + G_DBUS_MESSAGE_TYPE_METHOD_RETURN }, + { "should be able to send an error in reply", +@@ -907,6 +916,10 @@ test_reply (Fixture *f, + alarm (30); + fixture_start_proxy (f); + ++ /* If a process outside the sandbox calls a method on the ++ * sandboxed process, then the sandboxed process is allowed to reply, ++ * but is not allowed to fake a "reply" that is really a method call ++ * or signal. (GHSA-2cgv-pwcq-wvpq) */ + for (size_t i = 0; i < G_N_ELEMENTS (reply_tests); i++) + { + const ReplyTest *t = &reply_tests[i]; +@@ -914,8 +927,15 @@ test_reply (Fixture *f, + g_autoptr(GDBusMessage) reply = NULL; + g_autoptr(GError) error = NULL; + guint32 call_serial = 0; ++ int caller_calls_before; ++ int caller_broadcast_before; ++ int caller_unicast_before; + g_test_message ("#%zu: %s", i, t->label); + ++ caller_calls_before = g_atomic_int_get (&f->caller_conn.n_method_calls); ++ caller_unicast_before = g_atomic_int_get (&f->caller_conn.n_unicast_signals); ++ caller_broadcast_before = g_atomic_int_get (&f->caller_conn.n_broadcasts); ++ + call = g_dbus_message_new_method_call (f->proxied.unique_name, + "/", + IGNORE_IFACE, +@@ -937,6 +957,31 @@ test_reply (Fixture *f, + + switch (t->type) + { ++ case G_DBUS_MESSAGE_TYPE_SIGNAL: ++ reply = g_dbus_message_new_signal ("/", ++ MALICIOUS_IFACE, ++ MALICIOUS_MEMBER); ++ ++ if (t->broadcast) ++ { ++ g_test_message ("Sending malicious broadcast signal as a reply"); ++ } ++ else ++ { ++ g_test_message ("Sending malicious unicast signal as a reply"); ++ g_dbus_message_set_destination (reply, f->caller_conn.unique_name); ++ } ++ ++ break; ++ ++ case G_DBUS_MESSAGE_TYPE_METHOD_CALL: ++ g_test_message ("Sending malicious method call as a reply"); ++ reply = g_dbus_message_new_method_call (f->caller_conn.unique_name, ++ "/", ++ MALICIOUS_IFACE, ++ MALICIOUS_MEMBER); ++ break; ++ + case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: + g_test_message ("Sending legitimate reply as a reply"); + reply = g_dbus_message_new_method_reply (call); +@@ -949,8 +994,6 @@ test_reply (Fixture *f, + "That didn't work"); + break; + +- case G_DBUS_MESSAGE_TYPE_SIGNAL: +- case G_DBUS_MESSAGE_TYPE_METHOD_CALL: + case G_DBUS_MESSAGE_TYPE_INVALID: + default: + g_assert_not_reached (); +@@ -965,6 +1008,14 @@ test_reply (Fixture *f, + + /* Do another round-trip, to make sure everything has been delivered */ + do_round_trip (&f->caller_conn, &f->proxied); ++ ++ /* The caller didn't receive any extraneous messages */ ++ g_assert_cmpint (g_atomic_int_get (&f->caller_conn.n_method_calls), ==, ++ caller_calls_before); ++ g_assert_cmpint (g_atomic_int_get (&f->caller_conn.n_unicast_signals), ==, ++ caller_unicast_before); ++ g_assert_cmpint (g_atomic_int_get (&f->caller_conn.n_broadcasts), ==, ++ caller_broadcast_before); + } + } + diff -Nru xdg-dbus-proxy-0.1.6/debian/patches/series xdg-dbus-proxy-0.1.6/debian/patches/series --- xdg-dbus-proxy-0.1.6/debian/patches/series 2026-08-11 12:43:30.000000000 +0000 +++ xdg-dbus-proxy-0.1.6/debian/patches/series 2026-09-23 12:16:29.000000000 +0000 @@ -9,3 +9,9 @@ tests/tests-Test-method-calls-being-blocked-or-allowed.patch GHSA-r7hp-698j-2h6c/Revert-Don-t-require-TALK-permission-for-broadcast-rules.patch tests/tests-Test-that-messages-from-outside-sandbox-are-not-rec.patch +CVE-2026-94422/tests-Add-basic-test-coverage-for-reply-handling.patch +CVE-2026-94422/proxy-Don-t-assume-that-only-returns-and-errors-have-a-re.patch +CVE-2026-94422/tests-Assert-that-replies-of-inappropriate-types-aren-t-a.patch +CVE-2026-94422/proxy-Make-it-clearer-which-direction-messages-are-going-.patch +CVE-2026-94422/proxy-Only-allow-replies-to-go-to-the-correct-destination.patch +CVE-2026-94422/tests-Assert-that-forged-replies-cannot-be-sent-to-wrong-.patch