From 2b40cd004c69be7a1c6d51161c7ffccf4024f232 Mon Sep 17 00:00:00 2001 From: Kegan Dougal <7190048+kegsay@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:45:26 +0100 Subject: [PATCH] bugfix: rejoining a room with rejected events incorrectly authorised events that should have been rejected The cascade rejection only worked on in-flight events, not seen events. We now overwrite events in event_map with rejected events (thus setting .rejected_reason) to ensure we continue to cascade rejections correctly. Complement test asserts this. --- synapse/handlers/federation_event.py | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py index f55352fb6f..a24cbbf4be 100644 --- a/synapse/handlers/federation_event.py +++ b/synapse/handlers/federation_event.py @@ -1832,6 +1832,31 @@ class FederationEventHandler: rejected_event_ids = await self._store.has_rejected_event_ids(seen_remotes) if len(rejected_event_ids) > 0: has_rejected_events = True + if is_state_dag_room: + # The copies in `event_map` came off the wire, so they claim to be + # unrejected even for events we rejected when we first saw them, e.g + # during an earlier join of this room. Swap in the persisted copies, + # which carry `rejected_reason`, so that rejection cascades into the + # events in this batch which reference them via `prev_state_events`. + # + # Events rejected during *this* batch are not covered by this: they + # have not been persisted yet, so they are tracked separately and + # passed to the auth rules as `batched_rejections`. + persisted_rejected_events = await self._store.get_events( + rejected_event_ids, + allow_rejected=True, + redact_behaviour=EventRedactBehaviour.as_is, + ) + missing = rejected_event_ids - set(persisted_rejected_events) + if missing: + # We have seen these events and they have rejection rows, so we + # should be able to load them. If we cannot, rejection would + # silently stop cascading, so complain loudly instead. + raise AssertionError( + f"Could not load rejected events {missing} " + + f"out of {rejected_event_ids}" + ) + event_map.update(persisted_rejected_events) # XXX: it might be possible to kick this process off in parallel with fetching # the events. @@ -1981,7 +2006,9 @@ class FederationEventHandler: event_map: every event we were given, by event ID. This includes events we have already persisted and so are not persisting again, because we may still need them to work out the auth events of an event we have not seen - before. On /send_join this is the complete state DAG. + before. Any of those which were rejected are the persisted copies, + so they carry `rejected_reason`. On /send_join this is the complete + state DAG. sorted_events: the events being persisted, sorted so that an event's `prev_state_events` come before it from_send_join: True if `event_map` is the complete state DAG from a