From fd9a6a42c2ba02a04d9daa9ad5b261fa2d35cbab Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:24:23 +0100 Subject: [PATCH] Block other federation endpoints for MSC4242 rooms; ensure the join event is connected to the state DAG This PR only handles /send_join and connected events in /send, so everything else should return early with coherent error messages. The join event must be connected to the main state DAG but we previously didn't ensure this. --- synapse/federation/federation_server.py | 21 +++++++++++++++++++++ synapse/handlers/federation.py | 10 ++++++++++ synapse/handlers/federation_event.py | 21 ++++++++++++++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 95aff08ddf..3e051d8306 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -233,12 +233,22 @@ class FederationServer(FederationBase): # We pause a bit so that we don't start handling all rooms at once. await self._clock.sleep(Duration(seconds=random.uniform(0, 0.1))) + async def _assert_not_state_dag_room(self, room_id: str, endpoint: str) -> None: + room_version = await self.store.get_room_version(room_id) + if room_version.msc4242_state_dags: + raise SynapseError( + 500, + f"{endpoint} is not implemented for MSC4242 state DAG rooms", + errcode=Codes.UNKNOWN, + ) + async def on_backfill_request( self, origin: str, room_id: str, versions: list[str], limit: int ) -> tuple[int, dict[str, Any]]: async with self._server_linearizer.queue((origin, room_id)): origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) + await self._assert_not_state_dag_room(room_id, "/backfill") pdus = await self.handler.on_backfill_request( origin, room_id, versions, limit @@ -654,6 +664,7 @@ class FederationServer(FederationBase): await self._event_auth_handler.assert_host_in_room(room_id, origin) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) + await self._assert_not_state_dag_room(room_id, "/state") # we grab the linearizer to protect ourselves from servers which hammer # us. In theory we might already have the response to this query @@ -694,6 +705,7 @@ class FederationServer(FederationBase): await self._event_auth_handler.assert_host_in_room(room_id, origin) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) + await self._assert_not_state_dag_room(room_id, "/state_ids") resp = await self._state_ids_resp_cache.wrap( (room_id, event_id), @@ -1154,6 +1166,13 @@ class FederationServer(FederationBase): # Note that get_room_version throws if the room does not exist here. room_version = await self.store.get_room_version(room_id) + if room_version.msc4242_state_dags and membership_type != Membership.JOIN: + raise SynapseError( + 500, + f"/send_{membership_type} is not implemented for MSC4242 state DAG rooms", + errcode=Codes.UNKNOWN, + ) + if await self.store.is_partial_state_room(room_id): # If our server is still only partially joined, we can't give a complete # response to /send_join, /send_knock or /send_leave. @@ -1247,6 +1266,7 @@ class FederationServer(FederationBase): await self._event_auth_handler.assert_host_in_room(room_id, origin) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, room_id) + await self._assert_not_state_dag_room(room_id, "/event_auth") time_now = self._clock.time_msec() auth_pdus = await self.handler.on_event_auth(event_id, room_id) @@ -1332,6 +1352,7 @@ class FederationServer(FederationBase): origin, room_id, earliest_events, latest_events, limit ) else: + await self._assert_not_state_dag_room(room_id, "/get_missing_events") missing_events = await self.handler.on_get_missing_events( origin, room_id, earliest_events, latest_events, limit ) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index d76e58108d..f3a024da3a 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -254,6 +254,9 @@ class FederationHandler: Returns: True if we actually tried to backfill something, otherwise False. """ + if (await self.store.get_room_version(room_id)).msc4242_state_dags: + return False + # Starting the processing time here so we can include the room backfill # linearizer lock queue in the timing processing_start_time = self.clock.time_msec() if record_time else 0 @@ -1459,6 +1462,13 @@ class FederationHandler: if event.state_key is None: raise SynapseError(400, "The invite event did not have a state key") + if room_version.msc4242_state_dags: + raise SynapseError( + 500, + "/invite is not implemented for MSC4242 state DAG rooms", + errcode=Codes.UNKNOWN, + ) + is_blocked = await self.store.is_room_blocked(event.room_id) if is_blocked: raise SynapseError(403, "This room has been blocked on this server") diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py index 293f99e38c..11b507d4a5 100644 --- a/synapse/handlers/federation_event.py +++ b/synapse/handlers/federation_event.py @@ -559,7 +559,11 @@ class FederationEventHandler: raise SynapseError(400, "Room version mismatch") if room_version.msc4242_state_dags and not is_state_dag_connected( - [ev for ev in state if supports_msc4242_state_dag(ev)] + [ + ev + for ev in itertools.chain(state, [event]) + if supports_msc4242_state_dag(ev) + ] ): # We should have been given a connected state DAG. If there is a gap in it we # cannot calculate the state, so the response is invalid and we refuse the join. @@ -1256,6 +1260,15 @@ class FederationEventHandler: if not missing_prevs: return await self._state_handler.compute_event_context(event) + if event.room_version.msc4242_state_dags: + raise FederationError( + "ERROR", + 500, + "Fetching the state at missing prev_events is not implemented for " + "MSC4242 state DAG rooms", + affected=event_id, + ) + logger.info( "_compute_event_context_with_maybe_missing_prevs(event_id=%s): Event in room %s is missing prev_events %s: " "calculating state for a backwards extremity", @@ -2684,6 +2697,12 @@ class FederationEventHandler: if supports_msc4242_state_dag(event): return await self._load_calculated_auth_events(event, context) return event_auth_events.values() + if supports_msc4242_state_dag(event): + raise AuthError( + code=HTTPStatus.FORBIDDEN, + msg=f"Unknown prev_state_events {missing_auth_event_ids}; fetching them " + "is not implemented for MSC4242 state DAG rooms", + ) if destination is None: # this shouldn't happen: destination must be set unless we know we have already # persisted the auth events.