Commit Graph
146 Commits
Author SHA1 Message Date
Kegan DougalandClaude Fable 5.1 6e7672f595 C3: Serve MSC4430 member key rooms to clients
What a client sees of a member key room: every event names a member key as
its sender and, through MSC4428, as its stable ID; `member_info` resolves
the keys to users; the requester is recognised by their key wherever the
server decides what they may see or which events are theirs; and `/members`
and `/joined_members` are keyed by key. One homeserver only, but the code
answers for members of other servers too (a verified link names the user it
claims, anything else is nobody).

`member_info`. MSC4428's map, which only named erased users (the stable ID
of an ordinary room is the user ID and the entry is omitted), now gives
every member key an entry: the user behind a key we hold, the user named by
a verified link for another server's key, and `null` for an erased user or a
key nobody vouches for (an unverified claim is anyone's to make). The
resolution is one batched store lookup (`get_user_ids_for_identities`,
exposed by the member key handler as `user_ids_for_identities`), so the map
costs rooms without member keys nothing. The map already covers `/sync`
(full and incremental), sliding sync, `/members` and `/joined_members`,
which are keyed by state key and so by key; nothing there changes. An
erasure is recorded on the change stream under the stable ID that changed,
which in these rooms is the member's key, so the entry that flips to `null`
is the one clients hold.

Visibility. `filter_and_transform_events_for_client` decided what the
requester may see from their membership under their user ID, which the state
of a member key room does not hold: a joined user saw nothing. It now looks
the requester up under their key in each member key room the batch touches,
for both the allow check and the membership the event is annotated with, and
maps senders back to users, per room (a key is only a user in the room it
was looked up in), for the two per-user checks: erased senders, whose events
are pruned for users who joined later, so a key with no attributable user is
not erased; and ignored senders, where a member may be ignored as the user
behind their key or as the key itself, since a key with no attributable user
is the only handle a client has on it.
`filter_event_for_clients_with_state`, which push evaluation uses to decide
who may see an event, does the same per local user, for the ignore lists as
for the memberships, including for the out-of-band membership events it
returns to their target. The push evaluator itself resolves the sender (so a
user is not notified of their own events), the invitee and the inviter (so
an invite notifies its target, and the invitee's invite rules see the
inviter as a user, where one is attributable).

The requester's own events. The serializer echoes `transaction_id` and
`delay_id` only to the sender's own session, by comparing the sender with
the requester's user ID, which in a member key room never matches.
`SerializeEventConfig.requester_member_keys` (a new keyword-only argument,
`None` by default, so existing constructions are unchanged) carries the
requester's keys and the Rust serializer accepts a sender among them;
`create_config` fills it from the cached per-user keys when the feature is
on. Keys are unique across rooms, so a key of the requester's can only ever
be their own sender.

Sync. Where the handlers compared the requester's user ID with a sender or
a membership state key, they now use the identity the room knows them by
(`room_identity_for_user`: their key where they have one, their user ID
otherwise, or the cached map of their keys per room): the incremental sync
reads the requester's previous membership under their key, so a change to
their own membership (a display name) is an ordinary timeline event rather
than the room sent again as newly joined, and a leave is newly left; the
heroes of a nameless room no longer include the requester; the lazy-loading
full sync fetches the requester's own membership under their key; a leave
the user sent themselves is told from a kick by their keys per room, in
sync and in the sliding sync room lists; the join-race guard asks the
user-keyed membership table; and sliding sync's `$ME` is the requester's
key, including in the computation of which required state has changed
between requests. An invite's sender is resolved to a user (where
attributable) for the ignore list and the invite rules, in sync and in
both sliding sync room lists: a key on the ignore list hides the invite
too, and an unattributable key no longer reaches the invite rules, which
parse user IDs and would have failed the whole sync. The members handed to
presence and to device lists, by sync, by the device handler for newly
joined and left rooms, and by the room initial sync, are mapped to users
first, so clients are told about users' devices and presence, not keys'.
The three storage queries that find a user's membership changes in
`current_state_delta_stream` and `room_memberships` share one clause that,
with the feature on, also matches the keys we hold for the user, each
scoped to its room (keys are public strings anyone can name as a state key
elsewhere) and spelled out from the cached per-user keys so that every arm
of the clause can use the tables' indexes; a user with no keys, and the
feature being off, get the legacy clause byte for byte. Sliding sync's
"newly joined" and "newly left" detection, and the device handler's
shared-room tracking, run on those queries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 09:22:39 +02:00
Kegan DougalandClaude Fable 5.1 b1228f6d9b C2: Create, join, send and leave in MSC4430 member key rooms
With `msc4430_enabled` on, the member key room version is registered and a
user can create such a room, join it, send events, change their own
membership, invite another user of this server and leave, all by user ID as
today, while every event in the room names member keys and is signed by
them. One homeserver only; members of other servers, moderation (kick, ban,
power levels), restricted joins and what clients are served back (`/sync`,
`/event`, `/members`) are outside this change.

Where a user ID becomes a key. Clients speak in user IDs, so the translation
happens once, in `EventCreationHandler.create_event`, before the event is
built: `MemberKeyHandler.translate_event_dict` replaces the `sender`, the
`state_key` of an `m.room.member` event and the `additional_creators` of a
create event with the users' member keys in the room, creating a key for a
user who has none (a key never changes, so a user who leaves and re-joins
keeps theirs). A membership event about one of our users gets our linkage
information (`content.localpart` and `content.domain`); whatever the client
put there is dropped first, since linkage information is a server's
assertion, and an erased user gets no `localpart`, so their name is not
published again. A user ID of another server, as sender, target or
additional creator, is refused (403 or 400) rather than quietly sent as a
user ID the room cannot hold, and so is a member key the client chose itself
as a target or additional creator: a key nobody vouches for would be a seat
in the room for whoever holds its private half (the membership servlets
still parse user IDs, so a key as a target is refused there too). A room
with a remote invitee is refused before anything is created, not by the
invite after the room exists, and a room cannot be upgraded into this
version (the clone names user IDs in power levels, bans and invites). The
handler is also where the rest of the server asks what an identity means:
`local_user_id_for_identity` (ours, by user ID or by a key we hold in that
room), `user_id_for_identity` (also the verified link of another server's
key), `domain_for_identity` (routing only: our name for our keys, the
claimed domain of a linked key) and `room_identity_for_user` (how to look a
user of ours up in a room's state: their key where they have one, their user
ID otherwise, which the state then does not contain, as it should not).
Three other places compared the sender with a user ID and now resolve it: a
suspended user redacting their own event, the admin-redaction rate limit,
and room participation.

The create event. In these room versions the room ID is derived from the
create event, and the create event names and is signed by the creator's key,
so the key has to exist before the room ID does. `translate_event_dict`
generates such keys as "pending" (an expiring in-memory cache), they sign the
create event from there, and once the event is built `create_event` stores
them under the new room ID; if building fails they are dropped at once. A
pending key is ours only for the create event itself (no room yet): in any
existing room it is nobody's, so a key minted for one room cannot act in
another while it waits. A user named twice by one create event (as creator
and additional creator) gets one key; a create event sent into an existing
room is refused before any key is made; and the room ID collision retry
starts each attempt from the content the client sent, not the translated
one. Room creation then looks the creator's membership up by their key in
the state map it batches the rest of the initial state against, and refuses
the version when the flag is off even though the registry (process wide) may
know it, which is also why the tests can run flag-off cases.
`default_room_version` cannot name this version: the server config validates
it before the experimental config registers it, so rooms are created with an
explicit `room_version`.

Signatures. `EventBuilder.build` asks `MemberKeyHandler.get_signers_for_event_dict`
who signs and `create_local_event_from_event_dict` signs with exactly those:
the sender's member key under the entity `sender`, if we hold it; our server
signing key on an `m.room.member` event whose target is a key we hold and
whose linkage information is exactly what we vouch for about that user (our
domain, their localpart unless they are erased, in which case none), which
is what lets other servers verify the member's link; and the authorising
member's key on a restricted join that names one of ours. An event whose
sender is not a key we hold gets no signature at all (it is only hashed), so
the server never signs as a key it does not own; the co-signature is decided
from the store, never from the dict, so no caller can have us vouch for a
name that is not the user's; and the signing helper refuses any signer the
event may not carry, so a wrong choice here could not produce one either. A
third-party rules module that replaces an event is as untrusted as a client:
its dict is translated again, and it may not change who authorised a join.
The builder's validation accepts canonical member keys as sender and
membership target in these rooms, and nothing else.

Membership handling. `update_membership_locked` looks the target and the
requester up in the room's state under their room identity (their key once
they have one), so re-joins, duplicate events and the ban checks see the
right membership; the previous membership of a new event is read under the
event's own state key; whether this server is in the room is answered from
the members that map to our users rather than by domain; a pending invite is
read by user ID (that table is keyed by user) and the inviter's server is
found through `domain_for_identity`, so a join via a remote inviter would
still route; the join notification maps the joining key to its user; and an
invite in such a room is refused with 400 unless the invitee is a key we
hold.

Hosts in a room. Registering the version makes the "which servers are in
this room" paths reachable for the first time, and they are what the
federation sender consults for every event. They took the domain of every
member; a member key has none, and on Postgres the SQL that extracts domains
from state keys returned a `None` host for such a room.
`get_hosts_for_identities` on the member keys store now gives the server
behind each identity (a user ID's domain, our own name for keys we hold, the
claimed domain of a key we have a link for, nothing for the rest), and
`_get_joined_hosts`, `get_current_hosts_in_room` and
`get_current_hosts_in_room_ordered` use it for member key rooms (the ordered
form is unordered there: nothing in the state keys to sort by). The sender
itself decided whether an event was ours by the sender's domain, which for a
key said "not ours" and dropped every event of such a room before reaching
any of this; it now asks the identity map, and finds the server of a
rescinded invite's target the same way rather than by parsing the key, which
would have raised outside the guard that protects the sending loop. A member
key room holds only our own members, so its hosts are this server and
nothing is sent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 09:22:39 +02:00
Kegan DougalandClaude Fable 5.1 5161187a50 B1: Define the MSC4430 room version, the MemberKey type and the signing helpers
Rust: `RoomVersion.msc4430_member_keys` and `MSC4430V12`
("org.matrix.msc4430.12" = `MSC4242V12` + the flag); redaction of
`m.room.member` keeps `content.domain` and drops `content.localpart` in such
rooms, so the reference hash, the event ID and the member key signature cover
the claimed domain but not the claimed localpart;
`membership_field::{DOMAIN,LOCALPART}`; `org.matrix.msc4430` in `/versions`;
`.pyi` updated. The version is defined but *not* registered in
`KNOWN_ROOM_VERSIONS`, not even when the flag is on.

Python: `MemberKey` in `synapse.types` next to the other identifier types, with
the grammar `^@[A-Za-z0-9_-]{43}$` (anything with a `:` is a user ID),
`is_member_key` (syntactic, regex only, for hot paths) and
`MemberKey.from_string` (strict: rejects non-canonical base64, so each public
key has exactly one member key string). `MemberKeys` constants
(`SIGNATURE_ENTITY = "sender"`, key ID `ed25519:1`) and
`EventContentFields.MSC4430_{LOCALPART,DOMAIN}`. `supports_msc4430_member_keys`
is a `TypeIs[MSC4430Event]` modelled on `supports_msc4242_state_dag`;
`MSC4430Event` subclasses `MSC4242Event`, since every member key room is a state
DAG room, so one predicate narrows to both.

Signing: `add_hashes_and_signatures_from_signers(room_version, event_dict,
signers, *, server_name)` hashes once and then applies `(entity, key)` signers
in order; `compute_member_key_room_signature(room_version, event_dict, signer,
*, server_name)` adds one signature to an already hashed event (the authoriser's
signature on `/send_join`, our co-signature on an inbound `/invite`) without
rehashing a PDU we have not checked. The three signer kinds are: the sender's
member key under `sender`; our own server name co-signing linkage information;
the authorising member key of a restricted join under the member key string
itself. Together these are the one choke point for signing in member key rooms,
and they refuse with `MemberKeySigningRefusedError` (500) any signer the MSC
does not allow: a `sender` signature by a key other than the one `sender` names,
a key ID other than `ed25519:1`, a server co-signature under any name but ours
or on anything but an `m.room.member` whose `content.domain` is us, or an
authorising signature on anything but a join that names that key in
`join_authorised_via_users_server`. A bug in choosing signers therefore cannot
make us sign as a key we do not hold or vouch for linkage information that is
not ours. An empty signer list hashes only (`/make_join` templates).

Loudness: `get_domain_from_id` and `get_localpart_from_id` raise
`MemberKeyHasNoDomainError` (a `SynapseError`, 500: it is a server bug) when
given a member key, on the no-colon branch only, so user IDs and aliases are
untouched. Because the HTTP layer logs every `SynapseError` at INFO without a
traceback, and the error may be caught before it gets there, both MSC4430 errors
log themselves at ERROR with the stack of the raise site: the point of them is
to name the unadapted code path. `hs.is_mine_id` is left alone: it is called on
raw client strings (state keys, sliding-sync `required_state`,
`/users/{id}/report`, module API) and on remote-controlled room content
(`/upgrade` moving a canonical alias), where "False if malformed" is relied upon
and a member-key-shaped string in a legacy room is just malformed.
`hs.is_mine_identity(room_id, identity)` is the sanctioned question for room
identities; it answers for user IDs and raises `MemberKeyHasNoDomainError` for
member keys. The Prometheus counter
`synapse_msc4430_unsupported_path_total{server_name,path}` and
`record_unsupported_path(server_name, path)` (counts always, warns once per path
per process) are for branches that deliberately skip member keys.

The identity module is `synapse/handlers/member_keys.py` (`MemberKeyHandler`,
`hs.get_member_key_handler()`). The `MemberKey` type and `is_member_key` live in
`synapse.types` because `get_domain_from_id` needs them and the module cannot
import handlers.

Config: `msc4430_enabled` implies `msc4428_enabled`; the Complement image sets
it.

None of this is reachable: no identity in any existing room can satisfy
`is_member_key`, and the room version cannot be used. Not gated, by design: the
`is_member_key`-guarded raises in `get_domain_from_id`/`get_localpart_from_id`,
which only fire for member-key-shaped strings and turn a 400 "Invalid ID" into a
500. The one remote-reachable path that was a clean 400 before,
`/make_join|leave|knock/{roomId}/{userId}` with a member-key-shaped path
parameter (the handlers compare `get_domain_from_id(user_id)` with the origin),
now validates the parameter as a user ID at the servlet (400 `M_INVALID_PARAM`),
so a malformed request cannot make the Synapse federation client fail over
across every resident on a 5xx. A PDU whose sender is member-key-shaped in a
legacy room already fails at `_check_sigs_on_pdu` and is logged at ERROR with a
traceback on `/send`; only the status code changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 09:22:38 +02:00
Kegan DougalandClaude Fable 5.1 45cfb3bc93 A1: Advertise MSC4428 and add unsigned.org.matrix.msc4428.stable_id
Add the `experimental_features.msc4428_enabled` flag. When set, `/versions`
advertises `org.matrix.msc4428` and the Rust event serializer adds
`unsigned.org.matrix.msc4428.stable_id` (the sender's user ID) to every
event served to clients, including the events embedded in `unsigned`
(`redacted_because`, bundled edits, thread summaries). The field is subject
to the client's field allowlist like any other unsigned key.

`SerializeEventConfig.__init__` gains a required `msc4428_enabled` argument
(not module API).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 09:22:38 +02:00
Paul Chobert b3b5f3b61d Advertise support for Matrix v1.15 (#20286)
Synapse now implements every item in the Matrix v1.15 changelog (all
ticked in #18731), so it can advertise `v1.15`.

Closes #18731.
2026-09-30 16:17:12 +01:00
dependabot[bot] 360ab6e002 Bump the all-rust-dependencies group across 1 directory with 6 updates (#20268) 2026-09-29 15:47:19 +01:00
Erik JohnstonandClaude Fable 5.1 6472b6a937 Remove Reactor::call_from_thread from the Rust side (#20267)
To use `reactor.callFromThread` we need to take the GIL. However, the
vast majority of use-cases on the Rust side involve using that from
Tokio reactor threads, where we do not want to take the GIL (as it can
potentially block for a long time).

In #20252 we added a pure Rust alternative to `callFromThread`.

`run_python_awaitable` is the last usage of `callFromThread`, and so we
replace it with `dispatch_to_twisted`.

---

The diff is a lot smaller if you hide pure whitespace changes.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 13:41:50 +01:00
Jason Little 0ea6b31f7c Advertise support for Matrix v1.14 (#20137)
As we now do everything from the list of features/changes for v1.13:
#18118
And it appears that v1.14 is already done: #18531
2026-09-29 12:08:40 +01:00
Johannes MarbachandAndrew Morgan b8bd6f93a1 Add push rules for MSC4075: MatrixRTC invites and notifications (#20227)
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
2026-09-25 10:40:24 +00:00
e863ce7fab Fire deferreds from Rust futures without taking the GIL (#20252)
Tokio tasks currently complete by taking the GIL and calling
`reactor.callFromThread`. Taking the GIL may block for a period of time,
and we do not want that to happen on the tokio reactor threads (as that
can block other work from happening).

To avoid this, we instead add a work queue that we can add to from Rust
without taking the GIL, which is drained by the reactor. We signal to
the Twisted reactor that it should wake up by using a unix socket pair,
which is exactly how `reactor.callFromThread` works.

---

The self-pipe trick is basically where you create a pair of unix sockets
connected to each other. One end is added to the reactor so the reactor
is woken up when there are bytes to read, and when another thread needs
to wake up the reactor it just needs to write a byte into the other unix
socket.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
2026-09-25 09:49:14 +01:00
Eric Eastwood 625b331768 MSC4311: Use full PDU's in stripped state (like invite_room_state) over federation and always include m.room.create event (#19723)
### Background

This PR was originally just trying to remove the flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation as client side API's like `/sync` should still use stripped events. But it turns out we were just re-using the client logic for the federation side and things might break if we didn't include the full `m.room.create` event so this PR now introduces MSC4311 support to use full PDU's in the `invite_room_state`/`knock_room_state` in the federation API's.

The flawed implementation was originally introduced in https://github.com/element-hq/synapse/commit/0eb7252a230811e59679cc7e55b92dac26532efc (no PR I assume because part of Hydra security fix) which was part of [Synapse v1.136.0](https://github.com/element-hq/synapse/blob/7530874a1250d6ad975b39582a784c594d29a505/CHANGES.md#synapse-11360-2025-08-12).

Spawning from reviewing https://github.com/element-hq/synapse/pull/19722 and noticing that we have [`TestMSC4311FullCreateEventOnStrippedState`](https://github.com/matrix-org/complement/blob/1e2e12eebc1edb27bbf12108ec849a8254b6ddcd/tests/v12_test.go#L1341-L1376) in Complement which already passes even though that test looks [flawed](https://github.com/matrix-org/complement/pull/791#discussion_r3132468346):

> I think this test is mixing up what [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) proposes. Perhaps these were changes to the MSC that came after?
> 
> For the client API's like `/sync`, it only proposes that `m.room.create` is a required *stripped* state event.
> 
> For the federation API's, alongside requiring `m.room.create`, it also mandates using the full event PDU format for all events in the `invite_room_state`/`knock_room_state` on `m.room.member` events (in `unsigned`)

### What does this PR do?

 1. Always use stripped state for client API's
    1. Remove flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation (as explained above)
    1. Sanitize stripped state when we receive events over federation
 1. Use full PDU's when sending `invite_room_state`/`knock_room_state` over federation
 1. Validate PDU's and warn when receiving `invite_room_state`/`knock_room_state` over federation
     1. In the future, we will strictly validate and reject

Complement tests: https://github.com/matrix-org/complement/pull/796

---

Part of https://github.com/element-hq/synapse/issues/19414
2026-09-22 11:57:54 -05:00
Erik JohnstonandClaude Opus 5 9b5697d378 Move per-homeserver Rust state into a RustRuntime object on the HomeServer (#20011)
Previously the tokio runtime was stashed in a hidden attribute on the
reactor object, installed lazily by whichever Rust code first needed it,
and started via `callWhenRunning`.

Instead, we create a `RustRuntime` (accessible via
`HomeServer.get_rust_runtime()`) that holds any per-reactor Rust state,
such as the tokio runtime. It is constructed lazily on use. Rust
consumers (`HttpClient`, `VersionsHandler`, the Python DB pool wrapper)
now receive the runtime or reactor handle explicitly, and the
`reactor.run()` / manual-startup workarounds in tests are no longer
needed.

We also add helper wrappers in Rust for `Reactor` and `HomeServer` that
exposes the needed functionality.

The aim is to allow us to have a Rust-side clock (mainly to get the
current time), that respects the unit test per-reactor time management.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:48:37 +01:00
Erik Johnston 26e46786b3 Port LogContext to Rust (#19979)
Ports the logcontext classes to Rust, and gives tokio tasks a captured
logcontext so that work running in (or spawned from) Rust is attributed
to the request that caused it.

1. **Add characterization tests for logcontext error messages and the
filter** — pins the exact `logcontext_error` message shapes, the
abuse-detection code paths and `LoggingContextFilter`'s observable
behaviour, *against the existing Python implementation* (this commit is
green on its own). These are the behavioural contract the port has to
satisfy.
2. **Port `ContextResourceUsage` to a Rust pyclass** — self-contained:
the new `synapse_rust.logcontext` module, the class, its stub and the
re-export.
3. **Move the logcontext storage and `LoggingContext` to Rust** — the
core change; the commit message carries detailed design notes.
Highlights:
- The slot is typed `Option<Py<LoggingContext>>`, with `None`
representing the sentinel. `_Sentinel`/`SENTINEL_CONTEXT` stay pure
Python (unchanged); thin wrappers on
`current_context`/`set_current_context` convert at the boundary, and
pyo3's extraction enforces the type (`TypeError` otherwise).
- The accounting is native: one `getrusage(RUSAGE_THREAD)` read per
switch via libc, inline `stop`/`start` bookkeeping for base
`LoggingContext`s, Python dispatch only for subclasses
(`BackgroundProcessLoggingContext`) so their overrides run. The thread
id comes from `PyThread_get_thread_ident` (the exact
`threading.get_ident()`
     value) without calling into Python.
- The hot paths avoid per-operation allocation: names are `Py<PyString>`
(the per-log-record `str(context)`/`server_name` reads are INCREF-only),
error branches materialise strings only when hit.
4. **Attribute Rust-spawned work to the caller's logcontext** —
`create_deferred` captures the caller's context and scopes it onto the
spawned task via a tokio task-local (`LogContextHandle`);
`current_context()` gives the task-local read precedence, so
`LoggingContextFilter`/`pyo3-log` resolve the right context on worker
threads with no per-record stamping. `run_python_awaitable` restores the
captured context (via a `with_logcontext` helper, the Rust
`PreserveLoggingContext`) around Python called back from Rust, so e.g.
`runInteraction` from the Rust `/versions` handler accounts its DB usage
against the right request. Integration tests exercise both guarantees
through real production code paths.

Follow-up work on top of this (separate PR): porting
`BackgroundProcessLoggingContext` natively and removing further `Py<_>`
indirections. The fact that `BackgroundProcessLoggingContext` is a
subclass is what forces some of the warts in this PR: e.g. having to use
`Py<LoggingContext>` everywhere, etc.

We don't try (yet) to make this pure Rust, instead we see this as simply
maintaining the Python logcontext machinery when crossing, rather than
trying to make a Rust equivalent that can be used by pure Rust
dependencies. We probably do want to do that in future, as well as wire
up e.g. CPU recording on Rust side, but that is unnecessary for now.
2026-09-21 10:00:53 +01:00
4bb07152c8 Support for profile updates over Sliding Sync (MSC4262) (#20003)
This PR implements support for profile updates over Sliding Sync:
https://github.com/matrix-org/matrix-spec-proposals/pull/4262. This pr
may be easier to review as a whole than commit by commit.

This builds on the legacy sync profile updates feature
https://github.com/element-hq/synapse/pull/19556, specifically the
profile updates stream it added.

Submitting for early review to get consensus on implementation. There
are some things we would like to add still, from spec, mainly:

* > Homeservers should only consider a profile field update "accepted"
by a client
> once the client returns with a new /sync request with the next /sync
token,
> NOT just after sending down the profile update. The client may never
receive
> response due to network conditions, or a bug in the client
implementation.
* > When a room enters this subset in this connection for the first
time, all requested
> fields from profiles of users in that room MAY be sent down. This
gives the client
> a base set of information for which future field updates can be
applied on top of.
> The homeserver MAY omit some fields and profiles if it believes that
the client has
> already received them, likewise repeat profiles MAY be sent down based
on homeserver
  > implementation.
* > Finally, if the list of fields expands to cover a new field ID,
those fields should
> be sent down for all users that are within the current room subset.
Future incremental
  > updates will then include changes to this field.
* Additionally, we would need to implement a lazy loading cache similar
to the legacy sync. (not part of MSC as such)

Depending on review these could either be added to this pr, or to keep
this pr from not growing too much, be added in a follow-up pr, as they
are more enhancement to this base sliding sync profile updates
functionality than a part of the core functionality.

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Olivier 'reivilibre' <olivier@librepush.net>
Co-authored-by: Olivier 'reivilibre <oliverw@element.io>
2026-08-25 14:37:03 +00:00
Johannes Marbach d0b105757e Add experimental support for MSC4502: Targeted and unrestricted room member queries (#19974)
This is another stopgap towards
https://github.com/element-hq/voip-internal/issues/641 and adds
experimental support for
[MSC4502](https://github.com/matrix-org/matrix-spec-proposals/pull/4502).

This adds a new endpoint `/rooms/{roomId}/is_joined` to query if a user
or server is joined to a room known to the homeserver. Access to the
endpoint is guarded behind a new OAuth scope that can be assigned when
registering application services.

Tracking issue: https://github.com/element-hq/synapse/issues/20118

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-08-20 15:27:11 +01:00
Andrew Ferrazzutti 11de9503fb Update Rust dependencies for security fixes (#20131) 2026-08-20 12:49:03 +01:00
c5f02a9313 Introduce support for MSC4429: Profile Updates for Legacy Sync (#19556)
Implements support for [MSC4429: Profile Updates for Legacy
Sync](https://github.com/matrix-org/matrix-spec-proposals/pull/4429).

Paired with https://github.com/matrix-org/complement/pull/849 and
https://github.com/matrix-org/sytest/tree/anoa/msc4429

Tracking issue for removing unstable identifiers in Synapse:
https://github.com/element-hq/synapse/issues/19891
Further improvements tracked in:
https://github.com/element-hq/synapse/issues/19981

---------

Co-authored-by: Half-Shot <will@half-shot.uk>
Co-authored-by: Jason Robinson <jasonr@element.io>
Co-authored-by: Olivier 'reivilibre' <oliverw@element.io>
2026-08-06 15:35:07 +01:00
Eric Eastwood c5e5abb822 Use weakref to Python DatabasePool to allow homeserver to cleanly shutdown (#20009)
Use weakref to Python `DatabasePool` to allow homeserver to cleanly
shutdown

Spawning from
[upgrading](https://github.com/element-hq/synapse-small-hosts/pull/420#discussion_r3661700252)
the Synapse version in Synapse Pro for small hosts and seeing our tenant
deprovision tests failing (end-to-end Complement tests). Specifically,
it was failing with a test in
[`TestLogging`](https://github.com/element-hq/synapse-small-hosts/blob/f24ee731ff419890680d1d80ef6eafe44f87f2c2/complement/tests/multi_synapse/logging_test.go#L328-L385)
in CI but was equally reproducible with any test where we deprovision
(`deployment.StopServer(...)`) like
[`TestProvisionHomeserverTenant/deprovision_homeserver_tenant`](https://github.com/element-hq/synapse-small-hosts/blob/f24ee731ff419890680d1d80ef6eafe44f87f2c2/complement/tests/multi_synapse/provision_test.go#L48-L58).

Clean homeserver shutdown specifically regressed with the changes from
https://github.com/element-hq/synapse/pull/19878. The
`PythonDatabasePoolWrapper` in Rust holds onto a reference to the Python
`DatabasePool` which references the Python `HomeServer` and keeps the
`HomeServer` from being garbage collected on the Python side.

This PR updates `PythonDatabasePoolWrapper` to use a weak reference.


### Dev notes

Previous PR where we had to deal with Rust <-> Python reference cycles,
https://github.com/element-hq/synapse/pull/19837#discussion_r3425358096

PyO3 garbage collector docs (`__traverse__`, `__clear__`):
https://pyo3.rs/v0.28.3/class/protocols#garbage-collector-integration


### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-07-28 10:04:48 +01:00
SpiritCrocandAndrew Morgan bf0f4f6a9d Support MSC4446: allow moving fully read markers backwards (#19663)
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
2026-07-13 16:12:26 +00:00
Andrew FerrazzuttiandEric Eastwood c0c2b37d5e MSC4140: update error responses (#19539)
- Impose limit of scheduled delayed events
- Update error codes to match latest draft of MSC4140 

---------

Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
2026-07-11 01:23:33 +00:00
Eric Eastwood c63d77a79d Rust database access via Python database connection pool v2 (#19878)
This is a stepping stone before we can go full Rust everywhere. We're
providing a generic interface as we want database access to work in
Synapse and `synapse-rust-apps`. In `synapse-rust-apps`, we will use a
`tokio-postgres` based database connection pool so it's full Rust.

We want to avoid the situation where we have two database connection
pools (one for Python, one for Rust) as we've run into connection
exhaustion problems on Matrix.org before.

As an example of using it and sanity check for all this work (including
tests), I've also ported over the `/versions` handler to the Rust side
with database access. The `/versions` endpoint is the simplest endpoint
I could find that still had some database access. Hopefully the refactor
on `/versions` isn't that controversial as it's not really the point of
this PR. We can always remove it from this PR but it's just here as a
sanity check that all of this works.


### Why `runInteraction(...)`?

Using the same `runInteraction` pattern that we already have in Synapse
means we can port over existing Synapse code/endpoints without much
thought. But this pattern also makes sense because we want[^1]
transactions to have repeatable-read isolation (easy to think about,
less foot-guns). Having everything thappen in a function callback means
we can do retries for serialization/deadlock errors.

[^1]: To note: Ideally, we'd want the least isolation possible but the
problem is that there is no tooling to yell at you when your
queries/logic is wrong so repeatable-read isolation is a great balance.


> When an application receives this error message, it should abort the
current transaction and retry the whole transaction from the beginning.
The second time through, the transaction will see the
previously-committed change as part of its initial view of the database,
so there is no logical conflict in using the new version of the row as
the starting point for the new transaction's update.
> 
> Note that only updating transactions might need to be retried;
read-only transactions will never have serialization conflicts.
>
> *--
https://www.postgresql.org/docs/current/transaction-iso.html#XACT-REPEATABLE-READ*


As a note, this strategy is less of an impedance mismatch (aligns more
closely) with Synapse so the glue code for the `python_db_pool` should
also be simpler.


### How does this interact with logcontext (`LoggingContext`)?

See [docs on log
contexts](https://github.com/element-hq/synapse/blob/4e9f7757f17ba81b8747b7f8f9646d17df145aa3/docs/log_contexts.md)
for more background.

We already support normal logging from Rust -> Python with `pyo3-log`
and `log` but as soon as we pass a thread boundary, everything is logged
against the `sentinel` log context. Normally, we want logs and CPU/DB
usage correlated with the request that spawned the work.

You can see how I took a stab at fixing this in
https://github.com/element-hq/synapse/pull/19846 by capturing the
logcontext in a Tokio task local and re-activating as necessary. For
example, in that PR, I reactivated the logcontext in
`run_python_awaitable(...)` which we use to call `runInteraction(...)`
from the Rust side which means all of the database usage is correlated
with the request as expected. It also means any `log:info!(...)` done in
`run_interaction(...)` is correlated correctly. But there needs to be a
better story for when you want to log everywhere else.

I haven't explored tracking CPU usage on the Rust side.

I've left all of this out of this PR as I think it will be better to
tackle this as a dedicated follow-up. For example, I'm thinking about
instead creating a new `LoggingContext` with the `parent_context` set to
the calling context and try to avoid needing to call
`set_current_context(...)` on the Python side where possible (like
tracking CPU).



### Testing strategy

Added some tests that exercise some `async` Rust handlers for the
`/versions` endpoint:
```
SYNAPSE_TEST_LOG_LEVEL=INFO poetry run trial tests.rest.client.test_versions.VersionsTestCase
```

Real-world:

 1. `poetry run synapse_homeserver --config-path homeserver.yaml`
 1. `GET http://localhost:8008/_matrix/client/versions`
2026-07-07 13:07:35 -05:00
Erik JohnstonandClaude Fable 5 f8fefb09bd Re-expose the standalone format_event_* transforms for backwards compat (#19922)
The port of event serialization to Rust (#19837) removed
`format_event_raw`, `format_event_for_client_v1`,
`format_event_for_client_v2` and
`format_event_for_client_v2_without_room_id` from
`synapse.events.utils`, but there are modules in the wild that import
them from there.

Reimplement them as standalone pyfunctions in Rust, operating directly
on the Python dict so the original semantics are preserved exactly
(in-place mutation, returning the same dict, arbitrary non-JSON values
passing through, KeyError on a missing `unsigned` in the v1 format), and
re-export them from `synapse.events.utils`.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:47:03 +01:00
27c3b5394b Port the synchronous event serialization core to Rust (#19837)
### Summary

This moves the synchronous core of client event serialization out of
`synapse/events/utils.py` and into Rust
(`rust/src/events/serialize.rs`).

Event serialization is on the hot path for `/sync`, `/messages`, and
most client-facing endpoints. Previously it was a recursive pure-Python
routine (`_serialize_event` / `_inject_bundled_aggregations` /
`only_fields`) that interleaved
CPU-bound formatting with `async` DB/IO. This PR separates those two
concerns and moves the CPU-bound half to Rust:

- **Async prep stays in Python.**
`EventClientSerializer._prepare_serialization` does all DB/IO up front:
batch-fetching redaction events and running the registered module
`unsigned`-callback hooks, for the top-level events *and* every bundled
sub-event (edits and thread latest events, which are themselves
serialized). The admin/MSC4354 config is resolved once via
`_update_config`, rather than re-checked on every recursive call as the
old code did.
- **The synchronous core moves to Rust.** Given an event plus the
pre-fetched `redaction_map`, `unsigned_additions`, and
`bundle_aggregations`, the Rust code produces the client JSON entirely
in Rust — including the v1/v2 format transforms, `only_event_fields`
filtering, redaction handling, and recursive bundled aggregations.

### Details

- The Rust entry point is a single batch function, `serialize_events`,
taking a list of `(event, membership)` pairs. The three lookup maps are
shared across the whole batch, so they're read out of Python and
converted to Rust
structures **once** per batch rather than once per event.
`EventClientSerializer.serialize_event` (singular) is a thin wrapper
that calls it with a one-element list.
- `SerializeEventConfig` is now a Rust `pyclass`, and the old
`event_format` callable is replaced by the `EventFormat` enum (`Raw` /
`ClientV1` / `ClientV2` / `ClientV2WithoutRoomId`). Call sites in
`rest/admin/events.py` and
`rest/client/{notifications,room,sync}.py` are updated to pass the enum.
`make_config_for_admin` and MSC4354 enablement now go through
`SerializeEventConfig.for_admin()` / `with_msc4354()`.
- New accessors on `EventInternalMetadata` (`redacted_by`, `txn_id`,
`device_id`, `token_id`, `delay_id`, `soft_failed`,
`policy_server_spammy`) expose to Rust the fields the serializer reads.
- The `_split_field` unit tests move from `tests/events/test_utils.py`
to a Rust test in `serialize.rs`, since the implementation moved.

### Behaviour

This is intended to be a behaviour-preserving refactor — the Rust core
mirrors the previous Python output (field ordering, v1 key promotion,
redaction placement per room version, null-`redacts` handling,
transaction-ID gating).

Existing serialization, relations, and sync tests pass unchanged.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Eric Eastwood <madlittlemods@gmail.com>
Co-authored-by: Eric Eastwood <erice@element.io>
2026-07-07 11:59:27 +01:00
Hugh Nimmo-Smith 4104058e83 Make MSC4388 (sign-in with QR code) PUT requests idempotent (#19808) 2026-07-03 12:09:16 +00:00
Andrew Morgan 1639c30281 Fix the cargo-test and cargo-bench CI jobs not running (#19883) 2026-06-30 11:46:47 +00:00
Eric Eastwood 42138ad602 Split out deferred and tokio_runtime to their own Rust modules (#19868)
Spawning from https://github.com/element-hq/synapse/pull/19824 /
https://github.com/element-hq/synapse/pull/19846 and wanting to use
`create_deferred` in more than just the `http_client.rs`
2026-06-19 22:21:48 -05:00
Erik Johnston ac21bf08f3 Port Requester class to Rust. (#19828)
This is in prep for converting the event serialization to Rust.

This is a fairly mechanical port, except that we store the appservice ID
rather than the appservice object. This avoids us having to store a
`Py<..>` (or port the appservice object over).
2026-06-09 10:26:04 +01:00
Erik JohnstonandEric Eastwood b8cacf7537 Fix Event.__repr__ (#19817)
Follow on from #19701 

This (more or less) matches what we had before. Otherwise we just get a
default `<builtins.Event at 0x...>`

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2026-06-03 15:19:27 +01:00
Erik Johnston e8e5a42180 Fix parsing events that have large integers (#19819)
Follow on from https://github.com/element-hq/synapse/pull/19701.

Unfortunately serde has a bug when using `#[serde(flatten)]` with
`arbitrary-precision` feature when handling integers that fit in a i128
when doing `serde_json::from_value`. See
https://github.com/serde-rs/serde/issues/2230.

The `depythonize` hits the same issue. To fix this we make it so we only
parse events from strings and not values.
2026-06-03 14:52:05 +01:00
Erik Johnston 387dfabe3b Fix loading 'invalid' event from the database (#19816)
Follow on from #19701.

Some Synapse servers may have events in their database that don't pass
the canonical JSON checks. This is bad, but we still want to be able to
load them nonetheless.
2026-06-02 12:34:36 +01:00
Erik JohnstonandEric Eastwood 9e2a076144 Port Event class to Rust (#19701)
Ports the event class to Rust.

The main difference here are:
1. There is now a single event class
2. We now validate a lot more at event construction time than we
previously did (we basically checked nothing before). This required some
changes to the tests, including
https://github.com/matrix-org/sytest/pull/1423

Reviewable commit-by-commit.

### Overview of Event Rust structure

The format of the event struct in Rust is quite different than that in
Python.

The top-level looks like:

```rust
pub struct Event {
    /// The parsed event JSON.
    fields: FormattedEvent,

    /// The event ID. For format v1 this is read directly from the JSON;
    /// for v2+ it is computed from the canonical-JSON hash at
    /// construction time and cached here.
    event_id: Arc<str>,

    /// Synapse-internal per-event state that lives outside the federated
    /// JSON (e.g. outlier flag, soft-failure, stream positions).
    #[pyo3(get)]
    internal_metadata: EventInternalMetadata,

    /// The room version this event was parsed for.
    #[pyo3(get)]
    room_version: &'static RoomVersion,

    /// `None` for accepted events; otherwise a short reason set by auth
    /// when the event was rejected.
    rejected_reason: Option<Box<str>>,
}
```

which includes the actual parsed event in `FormattedEvent`, plus the
rest of the event metadata.

```rust
pub struct FormattedEvent<E = Arc<EventFormatEnum>> {
    #[serde(default)]
    pub signatures: Signatures,

    #[serde(default)]
    pub unsigned: Unsigned,

    #[serde(flatten)]
    pub specific_fields: E,

    #[serde(flatten)]
    pub common_fields: Arc<EventCommonFields>,
}
```

The struct is further split into the common fields, format specific
fields, plus the signatures and unsigned. We split out the signature and
unsigned fields as they are mutable, so when we clone the event we can
still share the common and specific fields and only copy signature and
unsigned.

The `specific_fields` are the fields that depend on the format version.
They can either be a specific format (e.g. `E = EventFormatV1`) or a
type-erased enum `EventFormatEnum` that is across all room versions:

```rust
pub enum EventFormatEnum {
    V1(EventFormatV1),
    V2V3(EventFormatV2V3),
    V4(EventFormatV4),
    VMSC4242(EventFormatVMSC4242),
}
```

For example:

```rust
/// Shared flat-list encoding of `auth_events` and `prev_events`, reused
/// by every format from v2/v3 onwards.
#[derive(Serialize, Deserialize)]
pub struct SimpleAuthPrevEvents {
    pub auth_events: Vec<String>,
    pub prev_events: Vec<String>,
}

/// Version-specific fields for room versions 3-10.
#[derive(Serialize, Deserialize)]
pub struct EventFormatV2V3 {
    pub room_id: Box<str>,
    #[serde(flatten)]
    pub auth_prev_events: SimpleAuthPrevEvents,
}
```


### Dev notes

As discussed in
[`#element-backend-internal:matrix.org`](https://matrix.to/#/!SGNQGPGUwtcPBUotTL:matrix.org/$3gTjDO440GbAz57cXcCawwiyFLiD0crrarvS1uhzKOY?via=jki.re&via=element.io&via=matrix.org)

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2026-06-02 11:05:38 +01:00
Erik Johnston ace8447037 Tidy up Rust RoomVersion structs (#19766)
This is in prep for using the room versions more from Rust.

Main changes:
- Change it so each room version is defined as a delta to the last one.
This is a cosmetic change that makes it easier to ensure the room
version definitions are correct (as they're defined as deltas from
previous versions).
- Move constants to `RoomVersion` constants, like `RoomVersion::V1`, for
convenience.
- Change visibility of various attributes.
2026-05-14 11:21:00 +01:00
Olivier 'reivilibre 1b0622fa99 Merge branch 'release-v1.153' into develop 2026-05-13 13:10:18 +01:00
Erik Johnston 5efeac44b2 Handle arbitrary sized integers in unsigned. (#19769)
Handle arbitrary sized integers in `unsigned` (and other Rust objects
that use `serde_json::Value`)
2026-05-13 11:28:06 +01:00
Erik Johnston c430c16df4 Port event content to Rust (#19725)
Based on #19708.

This is on the path to porting the entire event class to Rust, as
`event.content` will then return the new Rust class `JsonObject`.

This PR adds a pure Rust `JsonObject` class that is a `Mapping`
representing a json-style object. It uses `serde_json::Value` as its
in-memory representation and `pythonize` for conversion when a field is
looked up on the object.

I'm not thrilled with the name, but couldn't think of a better one.

This also adds `JsonObject` handling to the JSON serialisation functions
we use, as well as to the `freeze(..)` function.

Reviewable commit-by-commit.
2026-05-08 14:19:03 +01:00
Eric Eastwood 8dbbc4000b Commit stray Rust change that keeps popping up (rust/src/canonical_json.rs) (#19763)
(introduced in https://github.com/element-hq/synapse/pull/19739)

Seems like some automatic change from `poetry run ./scripts-dev/lint.sh`
2026-05-08 06:20:25 -05:00
Erik Johnston 23b8fcf85e Port Event.unsigned field to Rust (#19708)
Similar to #19706, let's port the `unsigned` field into a Rust class.

This does change things a bit in that we now define exactly what
unsigned fields that are allowed to be added to an event, and what
actually gets persisted. This should be a noop though, as we carefully
filter out what unsigned fields we allow in from federation, for example

As a side effect of this cleanup, I think this fixes handling
`unsigned.age` on events received over federation.
2026-05-06 18:51:42 +01:00
Erik Johnston 3e6bf10640 Port Event.signatures field to Rust (#19706)
This is another stepping stone in porting the event class fully to Rust.

The new `Signatures` class is relatively simple, as we actually don't
interact with it that much in the code. It does *not* implement
`Mapping` or `MutableMapping` as that takes quite a lot of effort that
we don't need, even though it would be more ergonomic.
2026-05-06 11:38:15 +01:00
Erik Johnston 76b4fdceed Add a canonical JSON impl (#19739)
This comes from
https://github.com/erikjohnston/rust-signed-json/blob/main/src/json.rs.
We need to be able to serialise canonical JSON in Rust to be able to
calculate event IDs once we port the event class to Rust.

We could instead make the above a properly published crate, but feels
easier to pull it into Synapse utils.
2026-04-28 17:46:03 +01:00
Kegan DougalandEric Eastwood 15c03b9689 MSC4242: State DAGs (CSAPI) (#19424)
This implements [MSC4242: State
DAGs](https://github.com/matrix-org/matrix-spec-proposals/pull/4242),
without support for federation.

A general overview:
 - It adds a new room version and new event type.
 - It adds a new field `calculated_auth_event_ids` to internal metadata.
- It stores the state DAG via new state DAG edges / forward extremities
tables.
 - It adds new auth rules as per the MSC.
- It uses the new `prev_state_events` field instead of
`prev_event_ids()` when doing state resolution.

Complement tests: https://github.com/matrix-org/complement/pull/841

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Eric Eastwood <erice@element.io>
2026-04-16 15:46:47 +00:00
Erik Johnston 2d015f78ea Convert EventInternalMetadata to use Arc<RwLock<_>> (#19669)
This moves the reference counting from PyO3 into standard Rust types,
allowing the class to be used natively from Rust without needing a
Python runtime.
2026-04-16 10:59:39 +01:00
Olivier 'reivilibre 52c05c5ca4 Introduce spam_checker_spammy internal event metadata. (#19453)
Follows: #19365

Part of: MSC4354 Sticky Events (experimental feature #19409)

This PR introduces a `spam_checker_spammy` flag, analogous to
`policy_server_spammy`, as an explicit flag
that an event was decided to be spammy by a spam-checker module.

The original Sticky Events PR (#18968) just reused
`policy_server_spammy`, but it didn't sit right with me
because we (at least appear to be experimenting with features that)
allow users to opt-in to seeing
`policy_server_spammy` events (presumably for moderation purposes).

Keeping these flags separate felt best, therefore.

As for why we need this flag: soon soft-failed status won't be
permanent, at least for sticky events.
The spam checker modules currently work by making events soft-failed.
We want to prevent spammy events from getting
reconsidered/un-soft-failed, so it seems like we need
a flag to track spam-checker spamminess *separately* from soft-failed.

Should be commit-by-commit friendly, but is also small.

---------

Signed-off-by: Olivier 'reivilibre <oliverw@matrix.org>
2026-04-15 16:53:23 +01:00
Quentin Gliech a778497acb Merge branch 'master' into develop 2026-04-07 15:43:26 +02:00
Quentin Gliech 09d83f3127 Fix KNOWN_ROOM_VERSIONS.__contains__ raising TypeError for non-string keys (#19649)
The Rust port of `KNOWN_ROOM_VERSIONS` (#19589) made `__contains__`
strict about key types, raising `TypeError` when called with `None`
instead of returning `False` like a Python dict would.
This broke `/sync` for rooms with a NULL `room_version` in the database.

```
  File "/home/synapse/src/synapse/handlers/sync.py", line 2628, in _get_room_changes_for_initial_sync
    if event.room_version_id not in KNOWN_ROOM_VERSIONS:
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: argument 'key': 'NoneType' object cannot be cast as 'str'
```
2026-04-07 12:12:01 +00:00
Noah MarkertandQuentin Gliech 8291a493c7 resolves #19403 Report the rust compiler version used in the prometheus metrics (#19643)
# What is done?
- resolves #19403
- Adds build-time Rust compiler detection and captures the rustc
--version value during the build.
- Exposes the captured compiler version from the Rust extension via a
new Python-callable function.
- Exports a new Prometheus metric for rustc version.

# How to test?
- compile `poetry install`
- add `enable_metrics: true` and 
```yaml
    resources:
    - compress: false
      names:
      - client
      - federation
      - metrics
```
to homeserver.yaml
- start synapse
- find the rustc version at `http://localhost:8008/_synapse/metrics`

### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))

---------

Co-authored-by: Quentin Gliech <quenting@element.io>
2026-04-03 11:21:23 +02:00
539f708f32 Remove redacted_because from internal unsigned. (#19581)
This is a simplification so that `unsigned` only includes "simple"
values, to make it easier to port to Rust.

Reviewable commit-by-commit

Summary:

1. **Add `recheck` column to `redactions` table**
   
A new boolean `recheck` column (default true) is added to the
`redactions` table. This captures whether a redaction needs its sender
domain checked at read time — required for room v3+ where redactions are
accepted speculatively and later validated. When persisting a new
redaction, `recheck` is set directly from
`event.internal_metadata.need_to_check_redaction()`.
     
It's fine if initially we recheck all redactions, as it only results in
a little more CPU overhead (as we always pull out the redaction event
regardless).
                                                      
2. **Backfill `recheck` via background update**
   
A background update (`redactions_recheck`) backfills the new column for
existing rows by reading `recheck_redaction` from each event's
`internal_metadata` JSON. This avoids loading full event objects by
reading `event_json` directly via a SQL JOIN.
                              
3. **Don't fetch confirmed redaction events from the DB**
                              
Previously, when loading events, Synapse recursively fetched all
redaction events regardless of whether they needed domain rechecking.
Now `_fetch_event_rows` reads the `recheck` column and splits redactions
into two lists:
        - `unconfirmed_redactions` — need fetching and domain validation
- `confirmed_redactions` — already validated, applied directly without
fetching the event
      
This avoids unnecessary DB reads for the common case of
already-confirmed redactions.
4. **Move `redacted_because` population to `EventClientSerializer`**
Previously, `redacted_because` (the full redaction event object) was
stored in `event.unsigned` at DB fetch time, coupling storage-layer code
to client serialization concerns. This is removed from
`_maybe_redact_event_row` and moved into
`EventClientSerializer.serialize_event`, which fetches the redaction
event on demand. The storage layer now only sets
`unsigned["redacted_by"]` (the redaction event ID).
5. **Always use `EventClientSerializer`**
   
The standalone `serialize_event` function was made private
(`_serialize_event`). All external callers — `rest/client/room.py`,
`rest/admin/events.py, appservice/api.py`, and `tests` — were updated to
use `EventClientSerializer.serialize_event` / `serialize_events`,
ensuring
  `redacted_because` is always populated correctly via the serializer.
6. **Batch-fetch redaction events in `serialize_events`**
   
`serialize_events` now collects all `redacted_by` IDs from the event
batch upfront and fetches them in a single `get_events` call, passing
the result as a `redaction_map` to each `serialize_event` call. This
reduces N individual DB round-trips to one when serializing a batch of
events that includes redacted events.

---------

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 09:18:08 +00:00
f545aa4f33 Port RoomVersion to Rust (#19589)
Principally so that we can share the same room version configuration
between Python and Rust.

For the most part, this is a direct port. Some special handling has had
to go into `KNOWN_ROOM_VERSIONS` so that it can be sensibly shared
between Python and Rust, since we do update it during config parsing.

---------

Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 09:17:31 +00:00
Andrew Ferrazzutti c0924fbbd8 MSC4140: put delay_id in unsigned data for sender (#19479)
Implements
https://github.com/matrix-org/matrix-spec-proposals/pull/4140/changes/49b200dcc11de286974925177b1e184cd905e6fa
2026-03-16 16:29:42 +00:00
Eric Eastwood 160d9788c0 Simplify Rust HTTP client response streaming and limiting (#19510)
*As suggested by @sandhose in
https://github.com/element-hq/synapse/pull/19498#discussion_r2865607737,*

Simplify Rust HTTP client response streaming and limiting


### Dev notes

Synapse's Rust HTTP client was introduced in
https://github.com/element-hq/synapse/pull/18357



### Pull Request Checklist

<!-- Please read
https://element-hq.github.io/synapse/latest/development/contributing_guide.html
before submitting your pull request -->

* [x] Pull request is based on the develop branch
* [x] Pull request includes a [changelog
file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog).
The entry should:
- Be a short description of your change which makes sense to users.
"Fixed a bug that prevented receiving messages from other servers."
instead of "Moved X method from `EventStore` to `EventWorkerStore`.".
  - Use markdown where necessary, mostly for `code blocks`.
  - End with either a period (.) or an exclamation mark (!).
  - Start with a capital letter.
- Feel free to credit yourself, by adding a sentence "Contributed by
@github_username." or "Contributed by [Your Name]." to the end of the
entry.
* [x] [Code
style](https://element-hq.github.io/synapse/latest/code_style.html) is
correct (run the
[linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters))
2026-03-03 15:24:25 +01:00
Eric Eastwood 979566ed8f Pre-allocate the buffer based on the expected Content-Length with the Rust HTTP client (#19498)
Spawning from
[looking](https://matrix.to/#/!cnVVNLKqgUzNTOFQkz:matrix.org/$XOVFm5mjCzzmhUaGc202zGdSq8eWgjr00MJqNSfzHiA?via=element.io&via=matrix.org&via=one.ems.host)
at some traces and seeing the Synapse Rust HTTP client taking way longer
than what the Synapse Pro Event Cache claims it was able to respond in
(added some [better
tracing](https://github.com/element-hq/synapse-pro-modules/pull/38) for
that). I don't think this specific change will have a meaningful impact
but just something I saw (pre-optimization).
2026-02-27 16:25:26 -06:00