Skip to content

Commit 0fe9e1f

Browse files
committed
Merge branch 'master' into develop
2 parents ae18123 + 20c9e19 commit 0fe9e1f

File tree

6 files changed

+137
-105
lines changed

6 files changed

+137
-105
lines changed

CHANGES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# Synapse 1.105.1 (2024-04-23)
2+
3+
## Security advisory
4+
5+
The following issues are fixed in 1.105.1.
6+
7+
- [GHSA-3h7q-rfh9-xm4v](https://github.com/element-hq/synapse/security/advisories/GHSA-3h7q-rfh9-xm4v) / [CVE-2024-31208](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-31208) — High Severity
8+
9+
Weakness in auth chain indexing allows DoS from remote room members through disk fill and high CPU usage.
10+
11+
See the advisories for more details. If you have any questions, email security@element.io.
12+
13+
14+
115
# Synapse 1.105.0 (2024-04-16)
216

317
No significant changes since 1.105.0rc1.

debian/changelog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
matrix-synapse-py3 (1.105.1) stable; urgency=medium
2+
3+
* New Synapse release 1.105.1.
4+
5+
-- Synapse Packaging team <packages@matrix.org> Tue, 23 Apr 2024 15:56:18 +0100
6+
17
matrix-synapse-py3 (1.105.0) stable; urgency=medium
28

39
* New Synapse release 1.105.0.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ module-name = "synapse.synapse_rust"
9696

9797
[tool.poetry]
9898
name = "matrix-synapse"
99-
version = "1.105.0"
99+
version = "1.105.1"
100100
description = "Homeserver for the Matrix decentralised comms protocol"
101101
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
102102
license = "AGPL-3.0-or-later"

synapse/storage/databases/main/events.py

Lines changed: 37 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
# [This file includes modifications made by New Vector Limited]
2020
#
2121
#
22+
import collections
2223
import itertools
2324
import logging
2425
from collections import OrderedDict
@@ -53,6 +54,7 @@
5354
LoggingDatabaseConnection,
5455
LoggingTransaction,
5556
)
57+
from synapse.storage.databases.main.event_federation import EventFederationStore
5658
from synapse.storage.databases.main.events_worker import EventCacheEntry
5759
from synapse.storage.databases.main.search import SearchEntry
5860
from synapse.storage.engines import PostgresEngine
@@ -768,40 +770,26 @@ def _add_chain_cover_index(
768770
# that have the same chain ID as the event.
769771
# 2. For each retained auth event we:
770772
# a. Add a link from the event's to the auth event's chain
771-
# ID/sequence number; and
772-
# b. Add a link from the event to every chain reachable by the
773-
# auth event.
773+
# ID/sequence number
774774

775775
# Step 1, fetch all existing links from all the chains we've seen
776776
# referenced.
777777
chain_links = _LinkMap()
778-
auth_chain_rows = cast(
779-
List[Tuple[int, int, int, int]],
780-
db_pool.simple_select_many_txn(
781-
txn,
782-
table="event_auth_chain_links",
783-
column="origin_chain_id",
784-
iterable={chain_id for chain_id, _ in chain_map.values()},
785-
keyvalues={},
786-
retcols=(
787-
"origin_chain_id",
788-
"origin_sequence_number",
789-
"target_chain_id",
790-
"target_sequence_number",
791-
),
792-
),
793-
)
794-
for (
795-
origin_chain_id,
796-
origin_sequence_number,
797-
target_chain_id,
798-
target_sequence_number,
799-
) in auth_chain_rows:
800-
chain_links.add_link(
801-
(origin_chain_id, origin_sequence_number),
802-
(target_chain_id, target_sequence_number),
803-
new=False,
804-
)
778+
779+
for links in EventFederationStore._get_chain_links(
780+
txn, {chain_id for chain_id, _ in chain_map.values()}
781+
):
782+
for origin_chain_id, inner_links in links.items():
783+
for (
784+
origin_sequence_number,
785+
target_chain_id,
786+
target_sequence_number,
787+
) in inner_links:
788+
chain_links.add_link(
789+
(origin_chain_id, origin_sequence_number),
790+
(target_chain_id, target_sequence_number),
791+
new=False,
792+
)
805793

806794
# We do this in toplogical order to avoid adding redundant links.
807795
for event_id in sorted_topologically(
@@ -836,18 +824,6 @@ def _add_chain_cover_index(
836824
(chain_id, sequence_number), (auth_chain_id, auth_sequence_number)
837825
)
838826

839-
# Step 2b, add a link to chains reachable from the auth
840-
# event.
841-
for target_id, target_seq in chain_links.get_links_from(
842-
(auth_chain_id, auth_sequence_number)
843-
):
844-
if target_id == chain_id:
845-
continue
846-
847-
chain_links.add_link(
848-
(chain_id, sequence_number), (target_id, target_seq)
849-
)
850-
851827
db_pool.simple_insert_many_txn(
852828
txn,
853829
table="event_auth_chain_links",
@@ -2451,31 +2427,6 @@ def add_link(
24512427
current_links[src_seq] = target_seq
24522428
return True
24532429

2454-
def get_links_from(
2455-
self, src_tuple: Tuple[int, int]
2456-
) -> Generator[Tuple[int, int], None, None]:
2457-
"""Gets the chains reachable from the given chain/sequence number.
2458-
2459-
Yields:
2460-
The chain ID and sequence number the link points to.
2461-
"""
2462-
src_chain, src_seq = src_tuple
2463-
for target_id, sequence_numbers in self.maps.get(src_chain, {}).items():
2464-
for link_src_seq, target_seq in sequence_numbers.items():
2465-
if link_src_seq <= src_seq:
2466-
yield target_id, target_seq
2467-
2468-
def get_links_between(
2469-
self, source_chain: int, target_chain: int
2470-
) -> Generator[Tuple[int, int], None, None]:
2471-
"""Gets the links between two chains.
2472-
2473-
Yields:
2474-
The source and target sequence numbers.
2475-
"""
2476-
2477-
yield from self.maps.get(source_chain, {}).get(target_chain, {}).items()
2478-
24792430
def get_additions(self) -> Generator[Tuple[int, int, int, int], None, None]:
24802431
"""Gets any newly added links.
24812432
@@ -2502,9 +2453,24 @@ def exists_path_from(
25022453
if src_chain == target_chain:
25032454
return target_seq <= src_seq
25042455

2505-
links = self.get_links_between(src_chain, target_chain)
2506-
for link_start_seq, link_end_seq in links:
2507-
if link_start_seq <= src_seq and target_seq <= link_end_seq:
2508-
return True
2456+
# We have to graph traverse the links to check for indirect paths.
2457+
visited_chains = collections.Counter()
2458+
search = [(src_chain, src_seq)]
2459+
while search:
2460+
chain, seq = search.pop()
2461+
visited_chains[chain] = max(seq, visited_chains[chain])
2462+
for tc, links in self.maps.get(chain, {}).items():
2463+
for ss, ts in links.items():
2464+
# Don't revisit chains we've already seen, unless the target
2465+
# sequence number is higher than last time.
2466+
if ts <= visited_chains.get(tc, 0):
2467+
continue
2468+
2469+
if ss <= seq:
2470+
if tc == target_chain:
2471+
if target_seq <= ts:
2472+
return True
2473+
else:
2474+
search.append((tc, ts))
25092475

25102476
return False

synapse/storage/schema/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,16 @@
132132
133133
Changes in SCHEMA_VERSION = 83
134134
- The event_txn_id is no longer used.
135+
136+
Changes in SCHEMA_VERSION = 84
137+
- No longer assumes that `event_auth_chain_links` holds transitive links, and
138+
so read operations must do graph traversal.
135139
"""
136140

137141

138142
SCHEMA_COMPAT_VERSION = (
139-
# The event_txn_id table and tables from MSC2716 no longer exist.
140-
83
143+
# Transitive links are no longer written to `event_auth_chain_links`
144+
84
141145
)
142146
"""Limit on how far the synapse codebase can be rolled back without breaking db compat
143147

tests/storage/test_event_chain.py

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
from typing import Dict, List, Set, Tuple, cast
2323

24+
from parameterized import parameterized
25+
2426
from twisted.test.proto_helpers import MemoryReactor
2527
from twisted.trial import unittest
2628

@@ -45,14 +47,16 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
4547
self.store = hs.get_datastores().main
4648
self._next_stream_ordering = 1
4749

48-
def test_simple(self) -> None:
50+
@parameterized.expand([(False,), (True,)])
51+
def test_simple(self, batched: bool) -> None:
4952
"""Test that the example in `docs/auth_chain_difference_algorithm.md`
5053
works.
5154
"""
5255

5356
event_factory = self.hs.get_event_builder_factory()
5457
bob = "@creator:test"
5558
alice = "@alice:test"
59+
charlie = "@charlie:test"
5660
room_id = "!room:test"
5761

5862
# Ensure that we have a rooms entry so that we generate the chain index.
@@ -191,6 +195,26 @@ def test_simple(self) -> None:
191195
)
192196
)
193197

198+
charlie_invite = self.get_success(
199+
event_factory.for_room_version(
200+
RoomVersions.V6,
201+
{
202+
"type": EventTypes.Member,
203+
"state_key": charlie,
204+
"sender": alice,
205+
"room_id": room_id,
206+
"content": {"tag": "charlie_invite"},
207+
},
208+
).build(
209+
prev_event_ids=[],
210+
auth_event_ids=[
211+
create.event_id,
212+
alice_join2.event_id,
213+
power_2.event_id,
214+
],
215+
)
216+
)
217+
194218
events = [
195219
create,
196220
bob_join,
@@ -200,33 +224,41 @@ def test_simple(self) -> None:
200224
bob_join_2,
201225
power_2,
202226
alice_join2,
227+
charlie_invite,
203228
]
204229

205230
expected_links = [
206231
(bob_join, create),
207-
(power, create),
208232
(power, bob_join),
209-
(alice_invite, create),
210233
(alice_invite, power),
211-
(alice_invite, bob_join),
212234
(bob_join_2, power),
213235
(alice_join2, power_2),
236+
(charlie_invite, alice_join2),
214237
]
215238

216-
self.persist(events)
239+
# We either persist as a batch or one-by-one depending on test
240+
# parameter.
241+
if batched:
242+
self.persist(events)
243+
else:
244+
for event in events:
245+
self.persist([event])
246+
217247
chain_map, link_map = self.fetch_chains(events)
218248

219249
# Check that the expected links and only the expected links have been
220250
# added.
221-
self.assertEqual(len(expected_links), len(list(link_map.get_additions())))
222-
223-
for start, end in expected_links:
224-
start_id, start_seq = chain_map[start.event_id]
225-
end_id, end_seq = chain_map[end.event_id]
251+
event_map = {e.event_id: e for e in events}
252+
reverse_chain_map = {v: event_map[k] for k, v in chain_map.items()}
226253

227-
self.assertIn(
228-
(start_seq, end_seq), list(link_map.get_links_between(start_id, end_id))
229-
)
254+
self.maxDiff = None
255+
self.assertCountEqual(
256+
expected_links,
257+
[
258+
(reverse_chain_map[(s1, s2)], reverse_chain_map[(t1, t2)])
259+
for s1, s2, t1, t2 in link_map.get_additions()
260+
],
261+
)
230262

231263
# Test that everything can reach the create event, but the create event
232264
# can't reach anything.
@@ -368,24 +400,23 @@ def test_out_of_order_events(self) -> None:
368400

369401
expected_links = [
370402
(bob_join, create),
371-
(power, create),
372403
(power, bob_join),
373-
(alice_invite, create),
374404
(alice_invite, power),
375-
(alice_invite, bob_join),
376405
]
377406

378407
# Check that the expected links and only the expected links have been
379408
# added.
380-
self.assertEqual(len(expected_links), len(list(link_map.get_additions())))
409+
event_map = {e.event_id: e for e in events}
410+
reverse_chain_map = {v: event_map[k] for k, v in chain_map.items()}
381411

382-
for start, end in expected_links:
383-
start_id, start_seq = chain_map[start.event_id]
384-
end_id, end_seq = chain_map[end.event_id]
385-
386-
self.assertIn(
387-
(start_seq, end_seq), list(link_map.get_links_between(start_id, end_id))
388-
)
412+
self.maxDiff = None
413+
self.assertCountEqual(
414+
expected_links,
415+
[
416+
(reverse_chain_map[(s1, s2)], reverse_chain_map[(t1, t2)])
417+
for s1, s2, t1, t2 in link_map.get_additions()
418+
],
419+
)
389420

390421
def persist(
391422
self,
@@ -489,8 +520,6 @@ def test_simple(self) -> None:
489520
link_map = _LinkMap()
490521

491522
link_map.add_link((1, 1), (2, 1), new=False)
492-
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1)])
493-
self.assertCountEqual(link_map.get_links_from((1, 1)), [(2, 1)])
494523
self.assertCountEqual(link_map.get_additions(), [])
495524
self.assertTrue(link_map.exists_path_from((1, 5), (2, 1)))
496525
self.assertFalse(link_map.exists_path_from((1, 5), (2, 2)))
@@ -499,18 +528,31 @@ def test_simple(self) -> None:
499528

500529
# Attempting to add a redundant link is ignored.
501530
self.assertFalse(link_map.add_link((1, 4), (2, 1)))
502-
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1)])
531+
self.assertCountEqual(link_map.get_additions(), [])
503532

504533
# Adding new non-redundant links works
505534
self.assertTrue(link_map.add_link((1, 3), (2, 3)))
506-
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1), (3, 3)])
535+
self.assertCountEqual(link_map.get_additions(), [(1, 3, 2, 3)])
507536

508537
self.assertTrue(link_map.add_link((2, 5), (1, 3)))
509-
self.assertCountEqual(link_map.get_links_between(2, 1), [(5, 3)])
510-
self.assertCountEqual(link_map.get_links_between(1, 2), [(1, 1), (3, 3)])
511-
512538
self.assertCountEqual(link_map.get_additions(), [(1, 3, 2, 3), (2, 5, 1, 3)])
513539

540+
def test_exists_path_from(self) -> None:
541+
"Check that `exists_path_from` can handle non-direct links"
542+
link_map = _LinkMap()
543+
544+
link_map.add_link((1, 1), (2, 1), new=False)
545+
link_map.add_link((2, 1), (3, 1), new=False)
546+
547+
self.assertTrue(link_map.exists_path_from((1, 4), (3, 1)))
548+
self.assertFalse(link_map.exists_path_from((1, 4), (3, 2)))
549+
550+
link_map.add_link((1, 5), (2, 3), new=False)
551+
link_map.add_link((2, 2), (3, 3), new=False)
552+
553+
self.assertTrue(link_map.exists_path_from((1, 6), (3, 2)))
554+
self.assertFalse(link_map.exists_path_from((1, 4), (3, 2)))
555+
514556

515557
class EventChainBackgroundUpdateTestCase(HomeserverTestCase):
516558
servlets = [

0 commit comments

Comments
 (0)