Repository: matrix-org/synapse

Issue:
Internal error due to NUL byte in Conduit federation
This internal error just happened to sbd. when joining #conduit:fachschaften.org (the main Conduit room) with Synapse:

```
8ZhxlGdY - Error attempting to resolve state at missing prev_events
Sep 22 10:50:55 debian matrix-synapse[3275]: Traceback (most recent call last):
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/handlers/federation_event.py", line 710, in _resolve_state_at_missing_prevs
Sep 22 10:50:55 debian matrix-synapse[3275]:     remote_state = await self._get_state_after_missing_prev_event(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/handlers/federation_event.py", line 817, in _get_state_after_missing_prev_event
Sep 22 10:50:55 debian matrix-synapse[3275]:     await self._get_events_and_persist(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/handlers/federation_event.py", line 1171, in _get_events_and_persist
Sep 22 10:50:55 debian matrix-synapse[3275]:     await self._auth_and_persist_fetched_events(destination, room_id, roots)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/handlers/federation_event.py", line 1240, in _auth_and_persist_fetched_events
Sep 22 10:50:55 debian matrix-synapse[3275]:     await self.persist_events_and_notify(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/handlers/federation_event.py", line 1767, in persist_events_and_notify
Sep 22 10:50:55 debian matrix-synapse[3275]:     events, max_stream_token = await self._storage.persistence.persist_events(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/persist_events.py", line 322, in persist_events
Sep 22 10:50:55 debian matrix-synapse[3275]:     ret_vals = await yieldable_gather_results(enqueue, partitioned.items())
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/persist_events.py", line 239, in handle_queue_loop
Sep 22 10:50:55 debian matrix-synapse[3275]:     ret = await self._per_item_callback(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/persist_events.py", line 577, in _persist_event_batch
Sep 22 10:50:55 debian matrix-synapse[3275]:     await self.persist_events_store._persist_events_and_state_updates(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/databases/main/events.py", line 174, in _persist_events_and_state_updates
Sep 22 10:50:55 debian matrix-synapse[3275]:     await self.db_pool.runInteraction(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 686, in runInteraction
Sep 22 10:50:55 debian matrix-synapse[3275]:     result = await self.runWithConnection(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 791, in runWithConnection
Sep 22 10:50:55 debian matrix-synapse[3275]:     return await make_deferred_yieldable(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/threadpool.py", line 238, in inContext
Sep 22 10:50:55 debian matrix-synapse[3275]:     result = inContext.theWork()  # type: ignore[attr-defined]
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/threadpool.py", line 254, in <lambda>
Sep 22 10:50:55 debian matrix-synapse[3275]:     inContext.theWork = lambda: context.call(  # type: ignore[attr-defined]
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/context.py", line 118, in callWithContext
Sep 22 10:50:55 debian matrix-synapse[3275]:     return self.currentContext().callWithContext(ctx, func, *args, **kw)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/context.py", line 83, in callWithContext
Sep 22 10:50:55 debian matrix-synapse[3275]:     return func(*args, **kw)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/enterprise/adbapi.py", line 293, in _runWithConnection
Sep 22 10:50:55 debian matrix-synapse[3275]:     compat.reraise(excValue, excTraceback)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/deprecate.py", line 298, in deprecatedFunction
Sep 22 10:50:55 debian matrix-synapse[3275]:     return function(*args, **kwargs)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/python/compat.py", line 404, in reraise
Sep 22 10:50:55 debian matrix-synapse[3275]:     raise exception.with_traceback(traceback)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/twisted/enterprise/adbapi.py", line 284, in _runWithConnection
Sep 22 10:50:55 debian matrix-synapse[3275]:     result = func(conn, *args, **kw)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 786, in inner_func
Sep 22 10:50:55 debian matrix-synapse[3275]:     return func(db_conn, *args, **kwargs)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 554, in new_transaction
Sep 22 10:50:55 debian matrix-synapse[3275]:     r = func(cursor, *args, **kwargs)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/logging/utils.py", line 69, in wrapped
Sep 22 10:50:55 debian matrix-synapse[3275]:     return f(*args, **kwargs)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/databases/main/events.py", line 396, in _persist_events_txn
Sep 22 10:50:55 debian matrix-synapse[3275]:     self._update_metadata_tables_txn(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/databases/main/events.py", line 1528, in _update_metadata_tables_txn
Sep 22 10:50:55 debian matrix-synapse[3275]:     self._store_room_members_txn(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/databases/main/events.py", line 1666, in _store_room_members_txn
Sep 22 10:50:55 debian matrix-synapse[3275]:     self.db_pool.simple_insert_many_txn(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 931, in simple_insert_many_txn
Sep 22 10:50:55 debian matrix-synapse[3275]:     txn.execute_values(sql, vals, fetch=False)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 293, in execute_values
Sep 22 10:50:55 debian matrix-synapse[3275]:     return self._do_execute(
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 331, in _do_execute
Sep 22 10:50:55 debian matrix-synapse[3275]:     return func(sql, *args)
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/synapse/storage/database.py", line 294, in <lambda>
Sep 22 10:50:55 debian matrix-synapse[3275]:     lambda *x: execute_values(self.txn, *x, fetch=fetch), sql, *args
Sep 22 10:50:55 debian matrix-synapse[3275]:   File "/home/synapse/synapse/env/lib/python3.9/site-packages/psycopg2/extras.py", line 1267, in execute_values
Sep 22 10:50:55 debian matrix-synapse[3275]:     parts.append(cur.mogrify(template, args))
Sep 22 10:50:55 debian matrix-synapse[3275]: ValueError: A string literal cannot contain NUL (0x00) characters.
```

_Originally posted by @jplatte in https://github.com/matrix-org/synapse/issues/9341#issuecomment-924790608_


Discussion:
This seems to be caused by this code (where we insert into the `room_memberships` table):

https://github.com/matrix-org/synapse/blob/94b620a5edd6b5bc55c8aad6e00a11cc6bf210fa/synapse/storage/databases/main/events.py#L1640-L1661

It's likely a null byte in a display name or avatar URL. (I wonder if it's possible for null bytes to be in the membership value at this point? Would have thought not as presumably that event would be soft-failed and not trigger this code, but I haven't checked that.)

It's not entirely clear what the best thing to do is here; it probably depends on where this table is used. It might be acceptable to convert display names/avatar URLs to `None` if they contain null bytes, or perhaps the types of the fields could be changed to be `BLOB` (or is it `BINARY`?) rather than `TEXT` so that null bytes are not a problem.
>  It might be acceptable to convert display names/avatar URLs to `None` if they contain null bytes,

I think we already do this for displaynames/avatar URLs which aren't strings somewhere. +1 to extending that to NUL bytes
aside: a concern was expressed about storing JSON which contains NULs in the database in general. What we forgot at the time is that, in JSON, NUL bytes within strings are represented as `"\0"` (or possibly `"\u0000"` or similar). It's only a problem when we take the strings out of the JSON and try to work with the string itself - as we do here.

Resolving patch:
diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py
--- a/synapse/storage/databases/main/events.py
+++ b/synapse/storage/databases/main/events.py
@@ -1641,8 +1641,8 @@ def _store_event_reference_hashes_txn(self, txn, events):
     def _store_room_members_txn(self, txn, events, backfilled):
         """Store a room member in the database."""
 
-        def str_or_none(val: Any) -> Optional[str]:
-            return val if isinstance(val, str) else None
+        def non_null_str_or_none(val: Any) -> Optional[str]:
+            return val if isinstance(val, str) and "\u0000" not in val else None
 
         self.db_pool.simple_insert_many_txn(
             txn,
@@ -1654,8 +1654,10 @@ def str_or_none(val: Any) -> Optional[str]:
                     "sender": event.user_id,
                     "room_id": event.room_id,
                     "membership": event.membership,
-                    "display_name": str_or_none(event.content.get("displayname")),
-                    "avatar_url": str_or_none(event.content.get("avatar_url")),
+                    "display_name": non_null_str_or_none(
+                        event.content.get("displayname")
+                    ),
+                    "avatar_url": non_null_str_or_none(event.content.get("avatar_url")),
                 }
                 for event in events
             ],


Test patch:
diff --git a/tests/storage/test_roommember.py b/tests/storage/test_roommember.py
--- a/tests/storage/test_roommember.py
+++ b/tests/storage/test_roommember.py
@@ -161,6 +161,54 @@ def test_get_joined_users_from_context(self):
         )
         self.assertEqual(users.keys(), {self.u_alice, self.u_bob})
 
+    def test__null_byte_in_display_name_properly_handled(self):
+        room = self.helper.create_room_as(self.u_alice, tok=self.t_alice)
+
+        res = self.get_success(
+            self.store.db_pool.simple_select_list(
+                "room_memberships",
+                {"user_id": "@alice:test"},
+                ["display_name", "event_id"],
+            )
+        )
+        # Check that we only got one result back
+        self.assertEqual(len(res), 1)
+
+        # Check that alice's display name is "alice"
+        self.assertEqual(res[0]["display_name"], "alice")
+
+        # Grab the event_id to use later
+        event_id = res[0]["event_id"]
+
+        # Create a profile with the offending null byte in the display name
+        new_profile = {"displayname": "ali\u0000ce"}
+
+        # Ensure that the change goes smoothly and does not fail due to the null byte
+        self.helper.change_membership(
+            room,
+            self.u_alice,
+            self.u_alice,
+            "join",
+            extra_data=new_profile,
+            tok=self.t_alice,
+        )
+
+        res2 = self.get_success(
+            self.store.db_pool.simple_select_list(
+                "room_memberships",
+                {"user_id": "@alice:test"},
+                ["display_name", "event_id"],
+            )
+        )
+        # Check that we only have two results
+        self.assertEqual(len(res2), 2)
+
+        # Filter out the previous event using the event_id we grabbed above
+        row = [row for row in res2 if row["event_id"] != event_id]
+
+        # Check that alice's display name is now None
+        self.assertEqual(row[0]["display_name"], None)
+
 
 class CurrentStateMembershipUpdateTestCase(unittest.HomeserverTestCase):
     def prepare(self, reactor, clock, homeserver):