Needs work
Project:
Drupal core
Version:
main
Component:
cache system
Priority:
Major
Category:
Task
Assigned:
Unassigned
Reporter:
Created:
27 Aug 2015 at 13:50 UTC
Updated:
27 Jun 2025 at 19:00 UTC
Jump to comment: Most recent, Most recent file
Comments
Comment #2
wim leersComment #3
wim leers#2 originates from #2556889-42: [policy, no patch] Decide if SmartCache is still in scope for 8.0 and whether remaining risks require additional mitigation.
Fabianx already posted a review over at #2556889-53: [policy, no patch] Decide if SmartCache is still in scope for 8.0 and whether remaining risks require additional mitigation, which I need to address.
Comment #4
andypostare you sure that this approach will not abuse cache_tags table more?
Comment #6
wim leersAt least some of the failures are because
CacheabilityBubblingAccountProxyis missinguse DependencySerializationTrait. Which made me realize that this change actually forces everything using thecurrent_userservice to initialize the render and theme system (becausecurrent_user->renderer->theme.manager).So the proper solution is to not inject the
renderer. Otherwise even REST responses may get the theme system initialized.Comment #8
wim leersStupid me, I'm of course still getting a service injected. So I still need
DependencySerializationTrait.Comment #10
dawehner... stop at any given point at a sentence :)
Can we at least document why we don't bubble on uid?
Why do we need this change. Why do we opt out for that specific usecase?
Comment #11
wim leersComment #13
berdirFor starters, sounds like you want to exclude the anonymous user from this? ;)
Comment #14
effulgentsia commentedTagging same as #2557815-39: Automatically bubble the "user.node_grants:$op" cache context in node_query_node_access_alter(). Similar to that issue, I don't think this one should be promoted to Critical unless real core or contrib security vulnerabilities get discovered during RC that justify us doing so.
Comment #15
wim leersNow really on this again.
First, a rebased patch.
Comment #16
wim leers#13: done.
Comment #17
wim leersApparently
AccountInterfacewas changed in #2112679: getUsername() should return the username getDisplayName() for the formatted user name; it now has two additional required methods. Updated accordingly.Comment #18
wim leersFixing test failures caused by excessively added/bubbled cacheability metadata…
Sadly, to achieve that, we need to use the
current_user.non_bubblingservice in many places. For example, in all cache contexts and in all places where we do access checking.It seems that as a consequence, we'll end up with a majority of services consuming
current_user.non_bubblinginstead ofcurrent_user. Which means developers looking at core have a good chance of copy/pasting something that will in fact still be insecure…However, I can reduce the pain there by stopping to inject the current user into many services; many services don't actually need it. Doing that next.
Comment #19
dawehnerMh, that is a bit sad, well one alternative one could research is a bit more static code analysis, but that itself is of course orders of magnitude more complex.
Comment #20
wim leersRefactored a bunch of services to not pass the current user service to the access manager. That significantly cuts down on the problem described in #18.
Comment #21
wim leersMissed one spot.
Comment #22
wim leersFixed the failures in
CommentDefaultFormatterCacheTagsTestandToolbarCacheContextsTest.AFAICT
getTimeZone()should not bubble theusercache context, but thetimezonecache context.Comment #23
wim leersWhile working on #22, I noticed that the comment form now has more cache tags (see the changes in
CommentDefaultFormatterCacheTagsTest).This is because
filter_formats()(which is used to create the text format dropdown and the filter tips) uses the current user service to personalize it to the list of formats the current user can use. But this list is of course not specific to the user; it depends on the user's permissions. So it actually varies byuser.permissions, notuser. But, making this work correctly requires modifyingfilter_formats(), which 1) is a soft BC break at best, an actual BC break at worst, 2) is therefore in any case out of scope here.So, opening a separate issue for that.
This patch also fixes
CommentTranslationUITest's cacheability expectations.Comment #24
wim leersIssue+patch created: #2611924: filter_formats() returns different value based on access, lacks cacheability metadata.
Note that with that patch, we could revert the changes here to
CommentDefaultFormatterCacheTagsTest(in #22) andCommentTranslationUITest(in #23), because the cacheability metadata would become more accurate/more refined thanks to that.Comment #33
wim leersOne less fail and one less unnecessary
@current_userinjection.Comment #34
wim leersI noticed this is actually just like the three contextual link/local action/local task managers: having both the access manager and the current user services injected is pointless, since the access manager already has the current user service injected. So this one can be simplified also, which means again one less occurrence of
@current_user.non_bubbling.Comment #37
wim leersAgain one less fail.
Comment #38
wim leersAnd another one. Turns out HEAD is injecting
\Drupal::currentUser()into\Drupal::currentUser(), LOL.Comment #41
wim leersThe majority of the remaining failures are in REST tests. They're mainly caused by two bits of cacheability metadata being bubbled:
\Drupal\node\Entity\Node::getCurrentUserId(), which is called for the Node entity type'suidbase field, to set a default value; the mere act of instantiating a Node object does not mean it will also be rendered, so this should use the non-bubbling serviceAccountProxy::setAccount()(this is the class used for thecurrent_user.non_bubblingservice) doesand
drupal_get_user_timezone()calls\Drupal::currentUser()->getTimezone(). The latter then bubbles thetimezonecache context (since #22). But the mere act of ensuring the right timezone for the duration of this request does not mean that any date or time will actually be rendered. In other words: it does not make sense for\Drupal\Core\Session\CacheabilityBubblingAccountProxy::getTimeZone()to do any bubbling at all.This fixes 5 failures.
P.S.: debugging the second point has unveiled lots of bugs: we seem to call
setAccount()twice whenever a user logs in, once with aUserentity, once with aUserSession; ingetAccount()when$this->accountis not set and we default either to the initial account or the anonymous user session, we don't set the timezone; theAnonymousUserSession'stimezoneproperty is never set to a value, and so on. None of those seem to be causing any problems for now, but we should consider cleaning that up in future releases.Comment #42
wim leersOne less failure.
(This time:
filter_formats()again, called by\Drupal\filter\Plugin\DataType\FilterFormat::getSettableOptions(), called by\Drupal\Core\Validation\Plugin\Validation\Constraint\AllowedValuesConstraintValidator(), which uses the bubblingcurrent_userservice, and should really use the non-bubbling one. Form validation doesn't need render bubbling.)Comment #44
wim leers#41 caused us to go from 12 to 8 fails. But I said it fixed 5. It did. It also caused one regression, because one place (
NodeBlockFunctionalTest) had been updated to expect thetimezonecache context that no longer appears since #41.This fixes that.
Together with #42, this should be down to 6 failures.
Comment #45
wim leersJust like #42 fixed one validator to use the non-bubbling current user service, so should all validators be updated.
That, together with two more "save entity"-related things (in
User::postSave()andComment::preSave()) and one completely different thing that's completely independent from rendering (user_logout()), this should bring the number of failures down to zero.Comment #46
wim leersNext (and final) steps for the patch:
Then it's purely down to reviews. And specifically, review whether this is worth doing, because I think it's clear this is impossible to make disruption-free. Also read #18.
Comment #49
dawehnerIs there any reason why we no longer pass along the user in things like the local task manager? Going from explicit calling to implicit is IMHO not an improvement. Sadly its not described in any comment on this issue, but I can't really believe that its a technical necessarity, because in case it is, this will be a huge confusing API for people.
Comment #51
wim leersIt's described in #18:
The
access_manageralready gets thecurrent_userpassed in. So, any service that calls theaccess_managerdoesn't need to pass in thecurrent_user: that's pointless by definition, because theaccess_manageralready has it.So I don't see at all what's confusing about those changes; in fact, I find it much cleaner. In HEAD, it's confusing why you need to pass the
current_useragain to theaccess_manager.Did that make sense? If you disagree, can you explain why you find this less clear?
Comment #52
wim leersOne unit test had to be updated. Actually green now.
Comment #53
wim leersDiscussed with @dawehner in IRC. He felt very strongly that we should be explicit, and keep injecting the
current_userservice into the contextual link/local task/location action managers.So, reverting the changes in #20, #21 and #34.
Side benefit: this hugely reduces the size of the patch :)
Comment #54
wim leersI tried to do this, but failed miserably, because Symfony very much insists on making the inner/decorated service (
current_user.non_bubblingin our case) private, and even trying tricks using aliases, I am not able to figure out how to convince Symfony to not do that.Therefore, leaving the patch as-is for now.
Comment #55
wim leersAlso, the most relevant snippet from an IRC conversation:
Comment #56
wim leers#55 was around for >24 hours.
Moving to 8.1 per that IRC excerpt.
Comment #57
effulgentsia commentedI wonder if this is a better way of addressing the REST failures.
Here's a patch that goes back to #18 and just modifies
EarlyRenderingControllerWrapperSubscribera little. Interdiff is relative to #18.Comment #58
effulgentsia commented#57 doesn't appear to be queued for testbot. I wonder if changing the Version will fix that.
Comment #59
effulgentsia commentedMaybe I need to reupload with that version already set?
[Edit: yep, that seemed to do it]
Comment #61
effulgentsia commentedSome fixes extracted from #53.
Comment #63
effulgentsia commentedI'm curious why these changes are needed, so throwing a patch with that reverted at testbot.
Comment #65
effulgentsia commentedHah! #61 and #63 have the same failures, so the reversions in #63 seem ok.
Here's a fix for those last 2 failures.
Comment #66
effulgentsia commentedMeanwhile, seeing if we can revert some more.
Comment #68
effulgentsia commentedHere's a way to fix that.
Comment #69
effulgentsia commentedI like this approach more.
Comment #70
effulgentsia commentedCleaner version.
Comment #73
effulgentsia commentedComment #75
effulgentsia commentedHere's some docs additions, but not fixes for the 2 failures in #73. I'm done for the night. If someone (probably @Wim Leers) wants to fix those failures and figure out what else is needed to push this along, that would be great. I think the patch is at a state where we should triage it again for inclusion prior to RC4, because I think some of the concerns from #55 are now alleviated.
Comment #77
wim leersEntityAccessControlHandler's "current user" service is used byUserAccessControlHandlerto compare the user ID (for "edit/view own"-style permissions). Which means that we simply must use the non-bubbling service inEntityAccessControlHandler. Which is the right thing to do anyway, because:This should make the patch green again.
Comment #78
wim leers#77 AFAICT means that I should be able to revert all the
getStaticCacheKey()stuff. Let's see if I'm right.Comment #80
wim leersI'm afraid I've got lots of bad news, news that in my opinion shows that we really can't do this.
(And news that unfortunately demonstrates how unfortunate it is that we didn't have
(Refinable)CacheableDependencyInterfaceand the cache tags/contexts/max-age concepts since the very beginning of the Drupal 8 cycle.)I firmly believe that the
getStaticCacheKey()stuff added by @effulgentsia in #57 and later is the wrong direction. We'd be adding a public method without an interface. A method that should not even be necessary. Just to mask the bubbling in situations where bubbling should not happen in the first place.The clearest validation possible can be found right in the consequences (test failures) of #78: even if we remove the static caching from
filter_formats(), then we still pass in the bubblingcurrent_userservice to theaccess()method. By chance, it so happens that itsid()method is also called only for static caching. But if the specific entity access logic would callid(), then it would still bubble the cacheability metadata!So, it is absolutely and solely thanks to chance that #77 is green: if
FilterFormatAccessControlHandlercalledid(), then it would be red! So any contrib module adding more advanced entity access logic will then still cause the cacheability metadata to be bubbled (inappropriately so!), despite not doing anything wrong whatsoever!Similarly, all the reverts @effulgentsia made since #57, including the ones to the constraint validators and the
Commentclass (and to a lesser extent those in theaccess_manager's injected service)… they all just happen to only cause test failures for REST tests. So, sure, our tests are green. But that doesn't mean it is correct.This also explains why @effulgentsia's surgical change to
EarlyRenderingControllerWrapperSubscriberworks: because validation happens on POST requests… hence it's possible to revert the validation changes I made.Another example, that shows an even more painful/deeper example: the
Nodeclass'suidbase field, which hasThe default value callback customizes a
Nodeobject to vary by the current user. And we already have the infrastructure to solve this:RefinableCacheableDependencyInterface! But we don't use it here — it didn't exist yet at the time when Field API was designed.In other words: we cannot ever cache the "add node" form correctly, because we fail to see that the
Nodeobject it depends on actually varies by user.In conclusion, Drupal 8 has gotten much better about being aware about its dependencies. Without knowing about dependencies, it is impossible to correctly cache/invalidate things.
But… it's nowhere near done yet. We see problems like this at all layers, in all APIs: from the "default value callback" for a field definition, to the
current_userservice being injected not allowing us to conclude that the thing it is injected to must also vary by user, because thecurrent_userservice also allows you to vary something just by permission, by roles, by authenticated/anonymous.Far too many objects still conflate many concerns into a single thing.
Far too much logic still assumes it's okay to access global state to figure out the current user/permissions/roles/….
Consequently, the only way I see something like this patch being workable/understandable, is by doing something like my patch was doing: inject the non-bubbling service when in fact we don't want bubbling to happen at all! My patch avoids doing bubbling cacheability metadata onto a render context when the thing happening is completely unrelated to rendering. This patch does not. But that approach also is brittle/confusing. Hence #55+#56 concluding this should be moved to 8.1.
I updated all places that use
current_user.non_bubblingto have an explanation as to why they need it. They further support my explanation/analysis above.This is a bit dishonest; it doesn't mention at all this bubbling is specifically for rendering.
This is a far too complex explanation IMO. Logs are not cached. There's simply no point in caring about cacheability metadata *at all*.
This is only half the truth, see my comment for the full truth.
Comment #81
fabianx commented#80: I did not read all, but would it help to distinguish between having an active RenderContext vs. having it not active?
e.g. its perfectly fine for #access to also bubble the metadata, in case it is used as part of a deeper render array. It is redundant but not wrong.
etc.
Comment #83
effulgentsia commentedI still prefer the approach in #75. I think some of Wim's additions since then might be good, but I'd like to evaluate each one separately. Meanwhile, I think the 2 failures in #75 are actually the result of a bug in HEAD: #2614230: UserAccessControlHandler::checkAccess() fails to add a user cache context where needed.
Comment #84
effulgentsia commentedFor now, going back to #75, but Wim texted me an idea to move the isMethodSafe() check to inside CacheabilityBubblingAccountProxy, so this patch does that. Interdiff is relative to #75.
Comment #86
effulgentsia commentedI discussed this with @catch, @xjm, and @alexpott, and regrettably, we agree with Wim's earlier conclusion in #56 and again in #80 that this isn't viable for core in 8.0.
The following demonstrate some of the cases of false positives (i.e., a per-user cache context being bubbled where it shouldn't be and that therefore necessitates using the non-bubbling service or some other way of calling a per-user method without bubbling):
Where the user's ID is only used for static caching, but by code that doesn't know if the result actually varies per-user. For example, in this case, the access result might only vary by permission, but we're statically caching per user anyway, because with static caches, we don't need to worry about this being inefficient, and it's actually more efficient to just statically cache in this way. This choice of a highly granular static cache strategy doesn't affect what is rendered. If the id() method is called as part of calculating an access result, that might matter, but simply calling id() to statically cache does not. So this patch allows for getting a static cache key without triggering the bubbling, and that's fine for core, but this is a category of false positives that contrib will encounter as well once this goes in, and thereby need to change their code accordingly. The patch also contains a similar example of this category in filter_formats().
Yet another category of false positives that contrib will hit upon.
And another one.
And a problem with encountering false positives like this is that it can bring a high traffic site down due to unnecessary cache variation.
On the other hand, not having this in 8.0 does risk module developers forgetting to associate the 'user' context where they should, and causing information disclosure vulnerabilities on sites. However, that's a problem that results from a bug in the module, whereas if this patch were to go in, the potentially site crippling performance problems would be due to a bug in core.
Given these factors and that the last RC is mere moments away, we decided to postpone this to 8.1.
However, we think this safeguard might be a very high value contrib project between 8.0 and 8.1, either as part of tooling during development to help catch places where a module developer might have made a mistake, or on production for highly security sensitive sites.
Comment #94
dpiBarely touches user.module.
Comment #95
moshe weitzman commentedAnyone know of a contrib project like this? Or the opposite - catches situations where one tag starts appearing very often on your site and when invalidated, blows way more cache than anticipated.
Comment #96
berdirRedis module has an issue/patch for a report page that among other general stats shows the render cache items with the most variations and most frequently invalidated cache tags. It does have some issues and can get very slow with enough tags though, which is why I haven't committed it yet.
#2848872: Add status page and some statistics
Comment #105
prudloff commentedIf I understand #86 correctly, this was postponed until 8.1. So it can probably be reevaluated now.
Comment #106
prudloff commentedComment #109
xjmAdding triage credits as per #86.
It's definitely eligible to be considered now on that basis, although we'd want to evaluate whether the approach is still valid.