Repository: matrix-org/synapse

Issue:
Synapse accepts to_device messages to hidden devices
As part of cross-signing, devices for the master, self-signing and user-signing cross-signing keys are created and stored in the `devices` table.

These devices should not be getting `to_device` messages sent to them, as they will never be read by any client. It turns out that it is possible to have `to_device` messages aimed at them in the `device_inbox` table through the following:

1. Clients can directly send `to_device` messages to one of your cross-signing public keys. These happen to be the device IDs of these hidden devices. Synapse doesn't condition on the device being hidden, and so will accept them. It should be doing so here:

https://github.com/matrix-org/synapse/blob/6633a4015a7b4ba60f87c5e6f979a9c9d8f9d8fe/synapse/storage/databases/main/deviceinbox.py#L499-L506

2. Clients often send `to_device` messages to all devices of a user, by specifying `*` as the target. Synapse will then create a `to_device` message for all devices of a user, including hidden ones:

https://github.com/matrix-org/synapse/blob/c7b823525e9598ee4de76099d9ca5ea1ce3977f4/synapse/storage/databases/main/deviceinbox.py#L486-L498

These messages were never getting read, and thus piling up in the database forever. In my testing they didn't seem to be using *that* much space however. Upon removing them for all users on my homeserver and vacuuming the table, ~~the size of `device_inbox` only went from 3420MB to 3410MB.~~ Edit: I don't think this is accurate. After deleting over half the table in a different cleanup and REINDEX+VACUUMing I also only dropped after 10MB.

Initially presumed as a client-side issue, before we discovered Synapse was at fault: https://github.com/vector-im/element-web/issues/15638

Related: https://github.com/matrix-org/synapse/issues/3599
Synapse accepts to_device messages to hidden devices
As part of cross-signing, devices for the master, self-signing and user-signing cross-signing keys are created and stored in the `devices` table.

These devices should not be getting `to_device` messages sent to them, as they will never be read by any client. It turns out that it is possible to have `to_device` messages aimed at them in the `device_inbox` table through the following:

1. Clients can directly send `to_device` messages to one of your cross-signing public keys. These happen to be the device IDs of these hidden devices. Synapse doesn't condition on the device being hidden, and so will accept them. It should be doing so here:

https://github.com/matrix-org/synapse/blob/6633a4015a7b4ba60f87c5e6f979a9c9d8f9d8fe/synapse/storage/databases/main/deviceinbox.py#L499-L506

2. Clients often send `to_device` messages to all devices of a user, by specifying `*` as the target. Synapse will then create a `to_device` message for all devices of a user, including hidden ones:

https://github.com/matrix-org/synapse/blob/c7b823525e9598ee4de76099d9ca5ea1ce3977f4/synapse/storage/databases/main/deviceinbox.py#L486-L498

These messages were never getting read, and thus piling up in the database forever. In my testing they didn't seem to be using *that* much space however. Upon removing them for all users on my homeserver and vacuuming the table, ~~the size of `device_inbox` only went from 3420MB to 3410MB.~~ Edit: I don't think this is accurate. After deleting over half the table in a different cleanup and REINDEX+VACUUMing I also only dropped after 10MB.

Initially presumed as a client-side issue, before we discovered Synapse was at fault: https://github.com/vector-im/element-web/issues/15638

Related: https://github.com/matrix-org/synapse/issues/3599


Discussion:
Related to https://github.com/matrix-org/synapse/issues/3656
I'd like to see a solution which:

* Stops adding messages for hidden devices when the user specifies `*`
* Silently ignore messages directed at hidden devices (just as we do today for non-existent ones)
* Removes existing messages that are directed at hidden devices to cut down on database size. I ended up using:

```sql
delete from device_inbox where device_id in (select device_id from devices where hidden = true);
```
Related to https://github.com/matrix-org/synapse/issues/3656
I'd like to see a solution which:

* Stops adding messages for hidden devices when the user specifies `*`
* Silently ignore messages directed at hidden devices (just as we do today for non-existent ones)
* Removes existing messages that are directed at hidden devices to cut down on database size. I ended up using:

```sql
delete from device_inbox where device_id in (select device_id from devices where hidden = true);
```

Resolving patch:
diff --git a/synapse/storage/databases/main/deviceinbox.py b/synapse/storage/databases/main/deviceinbox.py
--- a/synapse/storage/databases/main/deviceinbox.py
+++ b/synapse/storage/databases/main/deviceinbox.py
@@ -485,10 +485,12 @@ def _add_messages_to_local_device_inbox_txn(
             devices = list(messages_by_device.keys())
             if len(devices) == 1 and devices[0] == "*":
                 # Handle wildcard device_ids.
+                # We exclude hidden devices (such as cross-signing keys) here as they are
+                # not expected to receive to-device messages.
                 devices = self.db_pool.simple_select_onecol_txn(
                     txn,
                     table="devices",
-                    keyvalues={"user_id": user_id},
+                    keyvalues={"user_id": user_id, "hidden": False},
                     retcol="device_id",
                 )
 
@@ -501,10 +503,12 @@ def _add_messages_to_local_device_inbox_txn(
                 if not devices:
                     continue
 
+                # We exclude hidden devices (such as cross-signing keys) here as they are
+                # not expected to receive to-device messages.
                 rows = self.db_pool.simple_select_many_txn(
                     txn,
                     table="devices",
-                    keyvalues={"user_id": user_id},
+                    keyvalues={"user_id": user_id, "hidden": False},
                     column="device_id",
                     iterable=devices,
                     retcols=("device_id",),


Test patch: