It can occur that while ensuring a directory exists and has a .htaccess file that this .htaccess is created between !file_exists() and file_put_contents() because of a concurrent request in following code in core/lib/Drupal/Component/PhpStorage/FileStorage.php
protected function ensureDirectory($directory, $mode = 0777) {
if ($this->createDirectory($directory, $mode)) {
$htaccess_path = $directory . '/.htaccess';
if (!file_exists($htaccess_path) && file_put_contents($htaccess_path, static::htaccessLines())) {
@chmod($htaccess_path, 0444);
}
}
}
Silence the warning ...
if (!file_exists($htaccess_path) && @file_put_contents($htaccess_path, static::htaccessLines())) {
Comments
Comment #2
jummonk commentedComment #3
vuilComment #4
vuilComment #5
alexpottThanks for fixing this bug. Race conditions are hard to test and recreate. At the very least we need a comment explaining why something is silenced.
Comment #6
cilefen commentedComment #8
joshua.roberson commentedThe old patch doesn't work for Drupal 8.8.0 and also the warning is coming from a new file.
core/lib/Drupal/Component/FileSecurity/FileSecurity.php
This is the warning I get. I've created a new patch for it.
Warning: file_put_contents(.../twig/5df8f5904ff7b_field--node--title.html.t_U47XKAxSeaTanWRelNR6hRFQD//.htaccess): failed to open stream: Permission denied in Drupal\Component\FileSecurity\FileSecurity::writeFile() (line 158 of .../core/lib/Drupal/Component/FileSecurity/FileSecurity.php)Comment #9
joshua.roberson commentedComment #10
alexpottI actually encountered this randomly on a project. What turned out to fix it was the patch attached. We add the directory separator in \Drupal\Component\FileSecurity\FileSecurity::writeFile(). I think having two slashes breaks the
file_exists($file_path)check.Comment #11
vijaycs85nice finding. would it be possible to have a test?
Comment #12
alexpott@vijaycs85 I don't really know how to... it seems something based on environment and timing but once I made the change in #10 the random error went away.
Comment #13
alexpottI think we have comprehensive coverage of the class in \Drupal\Tests\Component\FileSecurity\FileSecurityTest and we know that the change in #10 is correct and that the double directory slash in
is wrong.
Comment #14
ruuds commentedI've applied the patch of #10, but it doesn't solve the problem for me. The warnings occur after clearing the drupal caches, also when writing the twig php template files:
Warning: file_put_contents(sites/website.nl/files/php/twig/5e9ea69050773_region--messages.html.twi_1zZlP40KCScZ9hppQnJELmqDQ/.htaccess): failed to open stream: Permission denied in Drupal\Component\FileSecurity\FileSecurity::writeFile() (line 158 of /home/website/domains/website.nl/web/core/lib/Drupal/Component/FileSecurity/FileSecurity.php)Warning: file_put_contents(sites/website.nl/files/php/twig/5e9ea69050773_region--messages.html.twi_1zZlP40KCScZ9hppQnJELmqDQ/.htaccess): failed to open stream: Permission denied in Drupal\Component\FileSecurity\FileSecurity::writeFile() (line 158 of /home/website/domains/website.nl/web/core/lib/Drupal/Component/FileSecurity/FileSecurity.php)I think silencing using @ would be an appropriate solution; Silencing file_put_contents is also done in other places (See MTimeProtectedFastFileStorage and FileStorage for example. Also, file_put_contents returns false when it fails, so we don't really need an extra php-warning I think.
Patch #8 solved the problem for me, altrough patch #10 also applies for correctness of the path to the .htaccess file.
Comment #15
alexpott@ruuds thanks for testing #10. Interesting that we've got different types of fails leading to the same error. I wonder if taking an exclusive lock works here...
Comment #16
ruuds commented@alexpott your patch of #15 also seems to solve the issue for me. I will monitor this the coming days.
Comment #17
alexpott@Ruuds it looks like the patch in #15 is a non-starter :( - LOCK_EX is not supported by vfs - which we use in kernel tests etc... and I'm not sure it is supported by the public / private stream wrappers either. Ho hum.
Comment #18
alexpottHere's an alternative approach that also uses an exclusive lock to create the file. It also needs silencing but I feel this reduces calls and is the most optimal solution in performance and multi-process sensitive code.
Note we also have another version of this to keep up-to-date.
Comment #19
alexpottWell that's not right. I'm not coping at all with $force :(
Comment #20
alexpottHmmm okay - let's go back to silencing the @file_put_contents().... but once we do that we can remove the directory checks. Since having them introduces a bigger window where concurrent requests can end up with this problem.
Comment #22
kim.pepperChanges look good to me. RTBC +1
Comment #23
ruuds commentedThanks for your effort @alexpott. The patch works great for me.
Comment #24
catchWhy are these changes necessary?
Is it worth adding some reasoning for why we don't bother checking if the file exists etc. here - just in case someone in a couple of years tries to add those back?
Comment #25
ruuds commented1. This is made because FileSecurity.php is duplicated from core. See the comment at the top of https://github.com/drupal/core-vendor-hardening/blob/8.8.x/FileSecurity.php
2. Maybe we can also reference this issue in the comment?
Comment #27
alexpottRe #24.1 and .2
Here's the whole method with the patch applied
Re #24.1 So we don't need to add the the / in the writeHtAccesss and writeWebConfig because we're adding it in writeFile already. In fact I found when the error was happening for me this was the cause. I suspect because the system has to do more work to resolve
path/to//.htaccessthanpath/to/.htaccessRe #24.2 I think the new comment covers this. We're already checking for file existence. The checks we're removing are about directory existing but now we're silencing errors from @file_put_contents checking the directory existence is pointless and unnecessary. Not sure that adding a comment to that purpose really adds to this. We added the
file_exists($directory) && is_writable($directory)checks in #2176141: Add a return value to file_save_htaccess() with no real explanation too :( (my fault).Comment #28
vuilComment #29
xjmI think it'd be good to get @catch's signoff here specifically, based on the previous discussion.
Comment #30
xjmThe patch also seems to not work on 9.1.x; should we provide a D9 version?
Comment #31
kim.pepper@xjm Do you mean the patch doesn't apply?
It seemed to apply ok for me. I re-rolled #20 just to make sure.
Comment #32
kim.pepperUpdating the issue version. This should be 9.1.x
Comment #33
vuilComment #34
catchOK so for #24.1 it's not so much that the change is necessary but that we're removing something unnecessary (which may be the cause of the bug in some cases).
#24.2 yeah I'm mostly concerned if someone tries to 'optimize' it at some point and re-introduces the bug, but that's probably too much history for a comment.
Comment #35
firfin commentedI think @catch's reply to 24.2 makes sense. As this is really hard ( problem is not consistently reproducable) to write a test for it should be marked in some way.
What else needs to be done before it can be committed?
Comment #36
ruuds commentedI'm running the patch for two months now without any strange new issues.
Maybe change the comment to something like this: "Try to write the file. This can fail if concurrent requests are both trying to write a the same time. It wil also fail when $directory doesn’t exist or isn’t writable. We won’t explicitly test for these conditions because it introduces two extra windows wherein concurrency issues can occur."
I think my written English is not good enough to determine if this 'marks' the whole issue...
Comment #37
alexpottImproved the comment along the lines of #36. I guess the extra docs can't hurt. It's a bit in the weeds but if it prevents us making the same mistake in the future that's great.
Leaving at rtbc since this is only a comment change.
Comment #41
catchCommitted/pushed to 9.1.x and cherry-picked back to 8.9.x, thanks!