Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions tests/v12_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@ func TestMSC4289PrivilegedRoomCreators(t *testing.T) {
)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably v12 is special because historically Synapse would use the domain in the room ID as a last-ditch server to join via?

-- @kegsay, #843 (comment)

I wouldn't expect this to work in other room versions (unless you backdate events to rollback state).

But for v12, the room creator has ultimate power to do whatever and so should be able to send a m.room.mebership to join again.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the reason for empty rooms being unjoinable that you can end up with two independent branches if users from different servers join the empty room? You have no way to know if other users rejoined the same empty room on their own servers. It was never about permissions, so I don't see how this is any different in v12 than previous room versions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tulir Unclear. Could that just be a different variant of a fork? That sounds like a plausible technical reason but I've never heard it before. Any prior art/spec about this?

Feels like something that needs to be clarified.

@tulir tulir Jul 13, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it has ever been explicitly specified. Might have always been an implicit constraint of servers being expected to use the make/send_join handshake for rooms they're not participating in, which you obviously can't do if there are no servers participating in the room. Though looking at the spec, even that isn't actually specified, https://spec.matrix.org/v1.19/server-server-api/#joining-rooms just says "When the homeserver does not yet know about the room it cannot do this directly." (this = just send a m.room.member event). You could interpret that to mean rooms you've left can be rejoined directly as long as the homeserver hasn't deleted the data from the db, but that'd be bad in terms of causing forks.

However, if we were to go with allowing joining empty rooms, I don't see any technical reason why it would have to be limited to room creators. Being a creator doesn't allow bypassing join rules, so you can't join invite-only rooms that way. In public rooms, anyone could send a join event like that, not only creators. The only reason to limit it would be to avoid forks, but if that's the justification, it should probably be limited to the primary creator and not include additional_creators.

Definitely need some spec clarifications, I think it'd be best to just define empty rooms are unjoinable by declaring that servers must use the make/send_join handshake unless they're actually participating in the room (at least 1 local member joined) and explicitly specifying that the handshake requirement leads to empty rooms being permanently unjoinable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if we were to go with allowing joining empty rooms, I don't see any technical reason why it would have to be limited to room creators. Being a creator doesn't allow bypassing join rules, so you can't join invite-only rooms that way.

hmmm, I guess a point against this is literally in MSC4289,

The room creator has always been able to leave an invite-only room and then rejoin it without an invite.

-- Security considerations in MSC4289

But the MSC doesn't mention join rules at all or how this is surmised.


In public rooms, anyone could send a join event like that, not only creators. The only reason to limit it would be to avoid forks, but if that's the justification, it should probably be limited to the primary creator and not include additional_creators.

I'm not sure we really need to prevent forks. Kinda feels the same as the normal disappearing in a submarine for months type of thing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the MSC doesn't mention join rules at all or how this is surmised.

I think that part just means backdating/referencing old events, since auth rules don't allow it even in v12. Has to be considered from a security perspective, but not something that can be used as an actual feature.

I'm not sure we really need to prevent forks

I guess if we don't care about that at all, then we could just allow anyone to join empty public rooms (that their server still has in the local database)

IMO we should just forbid joining empty rooms entirely, but if other people think allowing it is better, we can write the spec that way too. I'll make a spec issue and/or PR

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec issue is matrix-org/matrix-spec#2415, might make a PR for the forbid joining empty rooms option later

t.Run("Room creator can re-join room after everyone left", func(t *testing.T) {
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"room_version": roomVersion12,
})
alice.MustLeaveRoom(t, roomID)
alice.MustJoinRoom(t, roomID, nil)
})

t.Run("Additional room creator can re-join room after everyone left", func(t *testing.T) {
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"room_version": roomVersion12,
"creation_content": map[string]any{
"additional_creators": []string{bob.UserID},
},
})
alice.MustLeaveRoom(t, roomID)
bob.MustLeaveRoom(t, roomID)
bob.MustJoinRoom(t, roomID, nil)
})

t.Run("PL event is missing creator in users map", func(t *testing.T) {
roomID := alice.MustCreateRoom(t, map[string]interface{}{
"room_version": roomVersion12,
Expand Down
Loading