Skip to content

Unfreeze event dict after check_event_allowed module callbacks#19547

Open
MatMaul wants to merge 6 commits intoelement-hq:developfrom
tchapgouv:unfreeze-after-module-callbacks
Open

Unfreeze event dict after check_event_allowed module callbacks#19547
MatMaul wants to merge 6 commits intoelement-hq:developfrom
tchapgouv:unfreeze-after-module-callbacks

Conversation

@MatMaul
Copy link
Copy Markdown
Contributor

@MatMaul MatMaul commented Mar 11, 2026

Fix initially contributed in this PR: #18103

Fixes #18101

Pydantic validation was also failing if m.mentions is present (cf test).

Related bug: #18117

Discussed in #synapse-dev:matrix.org

Pull Request Checklist

@MatMaul MatMaul changed the title Unfreeze event after module callbacks Unfreeze event dict after check_event_allowed module callbacks Mar 11, 2026
# Unfreeze the event dict to avoid potential issues with frozen dicts further
# down the code. Pydantic for example is not happy with frozen dicts.
# cf https://github.com/element-hq/synapse/issues/18117
event._dict = unfreeze(event._dict)
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.

Initial implementation was adding an unfreeze method to EventBase. I changed that since we really don't want this to be called inside the modules.

@MatMaul MatMaul force-pushed the unfreeze-after-module-callbacks branch from c9b87a9 to 4c86b67 Compare March 11, 2026 13:26
@MatMaul MatMaul marked this pull request as ready for review March 11, 2026 13:37
@MatMaul MatMaul requested a review from a team as a code owner March 11, 2026 13:37
@@ -288,6 +289,9 @@ async def check_event_allowed(
# the hashes and signatures.
event.freeze()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should also update the freeze() docstring to callout the specific use cases that it's meant for and that you should remember to unfreeze before going back to Synapse code, etc

Comment on lines +326 to +329
# Unfreeze the event dict to avoid potential issues with frozen dicts further
# down the code. Pydantic for example is not happy with frozen dicts.
# cf https://github.com/element-hq/synapse/issues/18117
event._dict = unfreeze(event._dict)
Copy link
Copy Markdown
Contributor

@MadLittleMods MadLittleMods Mar 17, 2026

Choose a reason for hiding this comment

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

Perhaps we should create new freeze_event(...) and unfreeze_event(...) functions in this file that handles this logic. That way the pair can better be associated together as event._dict is a pretty arbitrary detail.

The location in this file vs being event.freeze() will also prevent people from accidentally mis-using it elsewhere (leaking into other parts of the code that it shouldn't).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even one step further, we could use a context manager pattern for this with freeze_event(event) as frozen_event: ... so we never forget to unfreeze

Comment thread changelog.d/19547.bugfix Outdated
@@ -0,0 +1 @@
Unfreeze event dict after check_event_allowed module callbacks. Frozen dicts can cause issues with Pydantic validation among other things. Contributed by @c-cal.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Currently, the PR description just says "Related to #18117" but I think this fixes the issue. Is that the case?

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.

Well it depends of how you look at it: we are fixing the problem for the module callback here, but we are not fixing the fact that a frozen event can be problematic in synapse code globally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hmmm, I see. I was jumping the gun since this is the only place we call event.freeze().

But we still have the USE_FROZEN_DICTS stuff which appears to do the same thing. At-least in this case, it's a self-inflicted configuration option and happens all the time, not just when a Synapse module is configured and some check_event_allowed callbacks are configured.

It's ironic that this config options says that it "prevents bugs" but introduces a whole new category of potential bugs.

Comment thread changelog.d/19547.bugfix Outdated
@@ -0,0 +1 @@
Unfreeze event dict after check_event_allowed module callbacks. Frozen dicts can cause issues with Pydantic validation among other things. Contributed by @c-cal.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Feel free to add your own handle here as well

Comment thread changelog.d/19547.bugfix Outdated
@@ -0,0 +1 @@
Unfreeze event dict after check_event_allowed module callbacks. Frozen dicts can cause issues with Pydantic validation among other things. Contributed by @c-cal.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps better 🤷 :

Suggested change
Unfreeze event dict after check_event_allowed module callbacks. Frozen dicts can cause issues with Pydantic validation among other things. Contributed by @c-cal.
Unfreeze event dict after module callbacks have run. Frozen dicts can cause issues with Pydantic validation and other type comparisons. Contributed by @c-cal.

event.freeze()

allow = True
event_dict = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
event_dict = None
replacement_event_dict = None


room_id = channel.json_body["room_id"]

event_id = self.create_and_send_event(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👍 Tested that this test fails when we comment out the unfreeze(...) fix

event._dict = unfreeze(event._dict)

return True, None
return allow, event_dict
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As an aside, I wonder if we have to be careful about someone passing back a frozen replacement_event_dict 🤔

Not something we necessarily have to concern ourselves with this PR but may be another problem. Perhaps we just need to tighten up the docstring details on what we expect passed back.

And hopefully, this doesn't naturally happen from someone taking the event, making a copy, mutating it, and passing it back.

content={
"body": "test",
"msgtype": "m.text",
"m.mentions": {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should call out that this is the secret sauce that tests xxx and is important because xxx

Comment thread tests/unittest.py
user: UserID,
soft_failed: bool = False,
prev_event_ids: list[str] | None = None,
content: dict[str, Any] | None = None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From the new usage, it looks like this was done in order to be able to define m.mentions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check_event_allowed callback from the module API cause a TypeError

2 participants