Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit a00462d

Browse files
authored
Implement cancellation support/protection for module callbacks (#12568)
There's no guarantee that module callbacks will handle cancellation appropriately. Protect module callbacks with read semantics from cancellation and avoid swallowing `CancelledError`s that arise. Other module callbacks, such as the `on_*` callbacks, are presumed to live on code paths that involve writes and aren't cancellation-friendly. These module callbacks have been left alone. Signed-off-by: Sean Quah <seanq@element.io>
1 parent 8de0fac commit a00462d

File tree

6 files changed

+86
-27
lines changed

6 files changed

+86
-27
lines changed

changelog.d/12568.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Protect module callbacks with read semantics against cancellation.

synapse/events/presence_router.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828

2929
from typing_extensions import ParamSpec
3030

31+
from twisted.internet.defer import CancelledError
32+
3133
from synapse.api.presence import UserPresenceState
32-
from synapse.util.async_helpers import maybe_awaitable
34+
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
3335

3436
if TYPE_CHECKING:
3537
from synapse.server import HomeServer
@@ -158,7 +160,9 @@ async def get_users_for_states(
158160
try:
159161
# Note: result is an object here, because we don't trust modules to
160162
# return the types they're supposed to.
161-
result: object = await callback(state_updates)
163+
result: object = await delay_cancellation(callback(state_updates))
164+
except CancelledError:
165+
raise
162166
except Exception as e:
163167
logger.warning("Failed to run module API callback %s: %s", callback, e)
164168
continue
@@ -210,7 +214,9 @@ async def get_interested_users(self, user_id: str) -> Union[Set[str], str]:
210214
# run all the callbacks for get_interested_users and combine the results
211215
for callback in self._get_interested_users_callbacks:
212216
try:
213-
result = await callback(user_id)
217+
result = await delay_cancellation(callback(user_id))
218+
except CancelledError:
219+
raise
214220
except Exception as e:
215221
logger.warning("Failed to run module API callback %s: %s", callback, e)
216222
continue

synapse/events/spamcheck.py

+25-11
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from synapse.rest.media.v1.media_storage import ReadableFileWrapper
3232
from synapse.spam_checker_api import RegistrationBehaviour
3333
from synapse.types import RoomAlias, UserProfile
34-
from synapse.util.async_helpers import maybe_awaitable
34+
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
3535

3636
if TYPE_CHECKING:
3737
import synapse.events
@@ -255,7 +255,7 @@ async def check_event_for_spam(
255255
will be used as the error message returned to the user.
256256
"""
257257
for callback in self._check_event_for_spam_callbacks:
258-
res: Union[bool, str] = await callback(event)
258+
res: Union[bool, str] = await delay_cancellation(callback(event))
259259
if res:
260260
return res
261261

@@ -276,7 +276,10 @@ async def user_may_join_room(
276276
Whether the user may join the room
277277
"""
278278
for callback in self._user_may_join_room_callbacks:
279-
if await callback(user_id, room_id, is_invited) is False:
279+
may_join_room = await delay_cancellation(
280+
callback(user_id, room_id, is_invited)
281+
)
282+
if may_join_room is False:
280283
return False
281284

282285
return True
@@ -297,7 +300,10 @@ async def user_may_invite(
297300
True if the user may send an invite, otherwise False
298301
"""
299302
for callback in self._user_may_invite_callbacks:
300-
if await callback(inviter_userid, invitee_userid, room_id) is False:
303+
may_invite = await delay_cancellation(
304+
callback(inviter_userid, invitee_userid, room_id)
305+
)
306+
if may_invite is False:
301307
return False
302308

303309
return True
@@ -322,7 +328,10 @@ async def user_may_send_3pid_invite(
322328
True if the user may send the invite, otherwise False
323329
"""
324330
for callback in self._user_may_send_3pid_invite_callbacks:
325-
if await callback(inviter_userid, medium, address, room_id) is False:
331+
may_send_3pid_invite = await delay_cancellation(
332+
callback(inviter_userid, medium, address, room_id)
333+
)
334+
if may_send_3pid_invite is False:
326335
return False
327336

328337
return True
@@ -339,7 +348,8 @@ async def user_may_create_room(self, userid: str) -> bool:
339348
True if the user may create a room, otherwise False
340349
"""
341350
for callback in self._user_may_create_room_callbacks:
342-
if await callback(userid) is False:
351+
may_create_room = await delay_cancellation(callback(userid))
352+
if may_create_room is False:
343353
return False
344354

345355
return True
@@ -359,7 +369,10 @@ async def user_may_create_room_alias(
359369
True if the user may create a room alias, otherwise False
360370
"""
361371
for callback in self._user_may_create_room_alias_callbacks:
362-
if await callback(userid, room_alias) is False:
372+
may_create_room_alias = await delay_cancellation(
373+
callback(userid, room_alias)
374+
)
375+
if may_create_room_alias is False:
363376
return False
364377

365378
return True
@@ -377,7 +390,8 @@ async def user_may_publish_room(self, userid: str, room_id: str) -> bool:
377390
True if the user may publish the room, otherwise False
378391
"""
379392
for callback in self._user_may_publish_room_callbacks:
380-
if await callback(userid, room_id) is False:
393+
may_publish_room = await delay_cancellation(callback(userid, room_id))
394+
if may_publish_room is False:
381395
return False
382396

383397
return True
@@ -400,7 +414,7 @@ async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
400414
for callback in self._check_username_for_spam_callbacks:
401415
# Make a copy of the user profile object to ensure the spam checker cannot
402416
# modify it.
403-
if await callback(user_profile.copy()):
417+
if await delay_cancellation(callback(user_profile.copy())):
404418
return True
405419

406420
return False
@@ -428,7 +442,7 @@ async def check_registration_for_spam(
428442
"""
429443

430444
for callback in self._check_registration_for_spam_callbacks:
431-
behaviour = await (
445+
behaviour = await delay_cancellation(
432446
callback(email_threepid, username, request_info, auth_provider_id)
433447
)
434448
assert isinstance(behaviour, RegistrationBehaviour)
@@ -472,7 +486,7 @@ async def check_media_file_for_spam(
472486
"""
473487

474488
for callback in self._check_media_file_for_spam_callbacks:
475-
spam = await callback(file_wrapper, file_info)
489+
spam = await delay_cancellation(callback(file_wrapper, file_info))
476490
if spam:
477491
return True
478492

synapse/events/third_party_rules.py

+30-6
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
import logging
1515
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple
1616

17+
from twisted.internet.defer import CancelledError
18+
1719
from synapse.api.errors import ModuleFailedException, SynapseError
1820
from synapse.events import EventBase
1921
from synapse.events.snapshot import EventContext
2022
from synapse.storage.roommember import ProfileInfo
2123
from synapse.types import Requester, StateMap
22-
from synapse.util.async_helpers import maybe_awaitable
24+
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
2325

2426
if TYPE_CHECKING:
2527
from synapse.server import HomeServer
@@ -263,7 +265,11 @@ async def check_event_allowed(
263265

264266
for callback in self._check_event_allowed_callbacks:
265267
try:
266-
res, replacement_data = await callback(event, state_events)
268+
res, replacement_data = await delay_cancellation(
269+
callback(event, state_events)
270+
)
271+
except CancelledError:
272+
raise
267273
except SynapseError as e:
268274
# FIXME: Being able to throw SynapseErrors is relied upon by
269275
# some modules. PR #10386 accidentally broke this ability.
@@ -333,8 +339,13 @@ async def check_threepid_can_be_invited(
333339

334340
for callback in self._check_threepid_can_be_invited_callbacks:
335341
try:
336-
if await callback(medium, address, state_events) is False:
342+
threepid_can_be_invited = await delay_cancellation(
343+
callback(medium, address, state_events)
344+
)
345+
if threepid_can_be_invited is False:
337346
return False
347+
except CancelledError:
348+
raise
338349
except Exception as e:
339350
logger.warning("Failed to run module API callback %s: %s", callback, e)
340351

@@ -361,8 +372,13 @@ async def check_visibility_can_be_modified(
361372

362373
for callback in self._check_visibility_can_be_modified_callbacks:
363374
try:
364-
if await callback(room_id, state_events, new_visibility) is False:
375+
visibility_can_be_modified = await delay_cancellation(
376+
callback(room_id, state_events, new_visibility)
377+
)
378+
if visibility_can_be_modified is False:
365379
return False
380+
except CancelledError:
381+
raise
366382
except Exception as e:
367383
logger.warning("Failed to run module API callback %s: %s", callback, e)
368384

@@ -400,8 +416,11 @@ async def check_can_shutdown_room(self, user_id: str, room_id: str) -> bool:
400416
"""
401417
for callback in self._check_can_shutdown_room_callbacks:
402418
try:
403-
if await callback(user_id, room_id) is False:
419+
can_shutdown_room = await delay_cancellation(callback(user_id, room_id))
420+
if can_shutdown_room is False:
404421
return False
422+
except CancelledError:
423+
raise
405424
except Exception as e:
406425
logger.exception(
407426
"Failed to run module API callback %s: %s", callback, e
@@ -422,8 +441,13 @@ async def check_can_deactivate_user(
422441
"""
423442
for callback in self._check_can_deactivate_user_callbacks:
424443
try:
425-
if await callback(user_id, by_admin) is False:
444+
can_deactivate_user = await delay_cancellation(
445+
callback(user_id, by_admin)
446+
)
447+
if can_deactivate_user is False:
426448
return False
449+
except CancelledError:
450+
raise
427451
except Exception as e:
428452
logger.exception(
429453
"Failed to run module API callback %s: %s", callback, e

synapse/handlers/account_validity.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from synapse.metrics.background_process_metrics import wrap_as_background_process
2424
from synapse.types import UserID
2525
from synapse.util import stringutils
26+
from synapse.util.async_helpers import delay_cancellation
2627

2728
if TYPE_CHECKING:
2829
from synapse.server import HomeServer
@@ -150,7 +151,7 @@ async def is_user_expired(self, user_id: str) -> bool:
150151
Whether the user has expired.
151152
"""
152153
for callback in self._is_user_expired_callbacks:
153-
expired = await callback(user_id)
154+
expired = await delay_cancellation(callback(user_id))
154155
if expired is not None:
155156
return expired
156157

synapse/handlers/auth.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import unpaddedbase64
4242
from pymacaroons.exceptions import MacaroonVerificationFailedException
4343

44+
from twisted.internet.defer import CancelledError
4445
from twisted.web.server import Request
4546

4647
from synapse.api.constants import LoginType
@@ -67,7 +68,7 @@
6768
from synapse.storage.roommember import ProfileInfo
6869
from synapse.types import JsonDict, Requester, UserID
6970
from synapse.util import stringutils as stringutils
70-
from synapse.util.async_helpers import maybe_awaitable
71+
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
7172
from synapse.util.macaroons import get_value_from_macaroon, satisfy_expiry
7273
from synapse.util.msisdn import phone_number_to_msisdn
7374
from synapse.util.stringutils import base62_encode
@@ -2202,7 +2203,11 @@ async def check_auth(
22022203
# other than None (i.e. until a callback returns a success)
22032204
for callback in self.auth_checker_callbacks[login_type]:
22042205
try:
2205-
result = await callback(username, login_type, login_dict)
2206+
result = await delay_cancellation(
2207+
callback(username, login_type, login_dict)
2208+
)
2209+
except CancelledError:
2210+
raise
22062211
except Exception as e:
22072212
logger.warning("Failed to run module API callback %s: %s", callback, e)
22082213
continue
@@ -2263,7 +2268,9 @@ async def check_3pid_auth(
22632268

22642269
for callback in self.check_3pid_auth_callbacks:
22652270
try:
2266-
result = await callback(medium, address, password)
2271+
result = await delay_cancellation(callback(medium, address, password))
2272+
except CancelledError:
2273+
raise
22672274
except Exception as e:
22682275
logger.warning("Failed to run module API callback %s: %s", callback, e)
22692276
continue
@@ -2345,7 +2352,7 @@ async def get_username_for_registration(
23452352
"""
23462353
for callback in self.get_username_for_registration_callbacks:
23472354
try:
2348-
res = await callback(uia_results, params)
2355+
res = await delay_cancellation(callback(uia_results, params))
23492356

23502357
if isinstance(res, str):
23512358
return res
@@ -2359,6 +2366,8 @@ async def get_username_for_registration(
23592366
callback,
23602367
res,
23612368
)
2369+
except CancelledError:
2370+
raise
23622371
except Exception as e:
23632372
logger.error(
23642373
"Module raised an exception in get_username_for_registration: %s",
@@ -2388,7 +2397,7 @@ async def get_displayname_for_registration(
23882397
"""
23892398
for callback in self.get_displayname_for_registration_callbacks:
23902399
try:
2391-
res = await callback(uia_results, params)
2400+
res = await delay_cancellation(callback(uia_results, params))
23922401

23932402
if isinstance(res, str):
23942403
return res
@@ -2402,6 +2411,8 @@ async def get_displayname_for_registration(
24022411
callback,
24032412
res,
24042413
)
2414+
except CancelledError:
2415+
raise
24052416
except Exception as e:
24062417
logger.error(
24072418
"Module raised an exception in get_displayname_for_registration: %s",
@@ -2429,7 +2440,7 @@ async def is_3pid_allowed(
24292440
"""
24302441
for callback in self.is_3pid_allowed_callbacks:
24312442
try:
2432-
res = await callback(medium, address, registration)
2443+
res = await delay_cancellation(callback(medium, address, registration))
24332444

24342445
if res is False:
24352446
return res
@@ -2443,6 +2454,8 @@ async def is_3pid_allowed(
24432454
callback,
24442455
res,
24452456
)
2457+
except CancelledError:
2458+
raise
24462459
except Exception as e:
24472460
logger.error("Module raised an exception in is_3pid_allowed: %s", e)
24482461
raise SynapseError(code=500, msg="Internal Server Error")

0 commit comments

Comments
 (0)