Problem/Motivation
For a long time we've been used the patch/MR from #2879087: Use comment access handler instead of hardcoding permissions . There is a trickery there that creates a dummy user just to check some access (see below). This used to work in Drupal 10 with no issue. Suddenly, on 11.3, that dummy user becomes current user in a form that is rendered with a lazy builder. I don't know how even to debug it.
$dummy_authenticated_user = new UserSession([
'uid' => -1,
'roles' => [UserInterface::AUTHENTICATED_ROLE],
]);
$this->authenticatedCanPostComments = $field->access('create', $dummy_authenticated_user);
Discussing in Slack, @catch confirmed that this is possible in some circumstances and it's likely a core bug. Quoting @catch
If an entity is loaded after switching the account, and before switching it back, and fibers get suspended, that could happen.
The account switch happens inside AccessPolicyProcessor::processAccessPolicies()
Steps to reproduce
This can be difficult to reproduce, but the general idea is:
- Render array has two child elements that are placeholdered and use lazy builders
- One child element uses an access check or permission check against an account which is not the current user
- Another child element display content dependent on the current user
- When the array is rendered, the second element can erroneously display content for the wrong user
An example can be see in the test in MR 14937
Proposed resolution
The issue is that when there is an access or permissions check, permissions are calculated via AccessPolicyProcessor::processAccessPolicies(), and in there, the current user is switched to the account the permissions are being calculated for. In the process of doing so, if entities are loaded (such as roles), then fiber the processing is running can be suspended. This changes the execution stack outside of AccessPolicyProcessor::processAccessPolicies(), with the current user not having been switched back.
The ideal resolution would be to avoid switching the current user at all, but that needs #2628870: Access result caching per user(.permissions) does not check for correct user.
For now we can address this by running the account switch within a child fiber and resume that fiber any time it is suspended, so that the execution does not leave that fiber until the current user has been switched back.
Separately, as we track down global state mutation issues with Fibers in core, it might make sense to have some sort of opt out to fibers that devs/admins can use in the meantime? Such as:
- A
settingsproperty in settings.php indicating that fibers should not be suspended - A Drupal wrapper class for Fiber with something like
allowSuspend(bool $allow) - A service with a settable flag indicating that fibers should not be suspended
Remaining tasks
Create follow ups as necessary per proposed resolution.
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3576074
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- test
compare
- 3576074-current-user-is
changes, plain diff MR !14937
- 3576074-fiberstack-poc
changes, plain diff MR !14941
Comments
Comment #2
claudiu.cristeaComment #6
godotislateMR 14937 has a failing kernel test that I think shows what the issue is.
For a fix that can go in a patch release, we'd probably need to look at a fiber trap again or similar mechanism. Ideally, either the account isn't switched at all in AccessPolicyProcessor, or it can be switched back before all the calculations. But in the case of the first, #2628870-70: Access result caching per user(.permissions) does not check for correct user explains the difficult in providing a cache context for a provided user instead of the current user. For the second, the cache must take the correct user data into account, so that the account can't be switched back before then.
So instead, I looked at whether it's possible to provide a way to make current user, or the current language as in #3569172: Weird language negotiation behavior inside getLanguageSwitchLinks leading to incorrect languages being used to work contextually within the scope of the fiber hierarchy they run in. A POC of this is MR 14941, and the general idea is this:
Introduce a new class/service called FiberStack, which tracks the hierarchy of instantiated fibers. The name is actually quite bad, since it's not a stack at all and more of a tree, but gonna put a pin in that
Since this is new API, this solution wouldn't work for a patch release. It's also turned out to be more complex to implement than anticipated. Tests are still not passing, and I think a lot of that has to do with the fiber stack not being reconstituted correctly after a container rebuild or when services are serialized. I tried addressing some with static properties instead, which raises the question of whether the fiber stack should be a static class instead of a service, but there are still outstanding issues.
Anyway, I'm not too sure going further with this idea will be fruitful, but I'm also not too sure what alternatives there would be better to explore.
Comment #8
godotislateClosed MR 14941, because it should have been obviously silly to me and that not every current user change should automatically be limited to the scope of the current fiber. If a global scope mutation needs to be contained to the current fiber, that needs to be handled intentionally where the mutation is done.
Comment #9
godotislateAdded a fix to 14937.
There should definitely be investigation on how to avoid the account switch, but this addresses the bug for now.
Comment #10
godotislateComment #11
claudiu.cristeaI have tested the fix from MR 14937 on our project, were was initially reported and is the source of this ticket. Everything works now, the current user is not switched anymore. We've been also added a regression test for our specific case that failed before, but now is passing by patching our project with this fix. I have looked to the code and I can't find anything to complain about. Looking good.
I would RTBC it but...
Could we have a follow-up for this investigation? Or if one already exists, could you, please, link it here?
Also, the IS might need a very brief explanation on the fix.
Comment #12
godotislateIt is essentially #2628870: Access result caching per user(.permissions) does not check for correct user, with #2628870-70: Access result caching per user(.permissions) does not check for correct user: providing some possibilities/challenges:
Once that's done, then the account switching can be removed in AccessPolicyProcessor, either directly in that issue or in a follow up.
Updated the IS, and proposed some ideas for general workarounds for Fibers issues that maybe could go in to 11.4/12.0 while harder solutions are worked on.
Comment #13
claudiu.cristeaThank you for updates. Everything, looks good here. I hope this can catch the next patch release (11.3.4)
Comment #17
catchThis looks good. I wonder whether we want to add some kind of FiberTrap class with a static method that we can call from places like this (method could either take a callable or a fiber). We currently only have two places though so it's not quite rule of three yet.
It would theoretically have been possible to do something like this too: https://git.drupalcode.org/project/drupal/-/commit/df7ee1dbac085c3898ab8... - e.g. keep switching the user back and forth so that we can re-suspend the fiber, but this use case is sufficiently rare I don't think we'd get much benefit for a lot more complexity. It was bad enough implementing that once.
Committed/pushed to main, 11.x, and 11.3.x, thanks!
Comment #21
godotislateStubbed a follow up: #3576594: [PP-1] Avoid account switching when calculating permissions in AccessPolicyProcessor::processAccessPolicies()
Comment #24
godotislateComment #26
herved commentedWhile perf hunting on a project, Claude suggested this as a possible improvement to ::processAccessPolicies to avoid creating Fibers unnecessarily:
Since access check can run thousands of times on a single page load it seems to save a few ms, but it's not huge, so IDK if it's worth reporting a dedicated issue...
Edit: I eventually created it #3593008: AccessPolicyProcessor::processAccessPolicies() creates unnecessary Fibers when checking permissions for the current user