-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Description
Symfony version(s) affected
5.x, 6.x
Description
On an open source PHP application I work on, we sometimes have issues reported by administrators related to unexpected filesystem saturation. The impacted cache namespace is supposed to contains only a dozen of items, but the directory contains 250k files located at its root. These files are temporary files generated when a cache entry is updated, and are supposed to be removed automatically.
The problem is that the target file is exists, contains stale information, but is not writable.
First, let's resume what is done by the Filesystem apadter when a cache entry is written:
- a temporary file is created at the root of the cache directory;
- content is written inside this file;
- this file is renamed to its target path, using the PHP
rename()
function.
Related code:
symfony/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
Lines 88 to 114 in d69ab75
private function write(string $file, string $data, int $expiresAt = null): bool | |
{ | |
set_error_handler(__CLASS__.'::throwError'); | |
try { | |
$tmp = $this->directory.$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6))); | |
try { | |
$h = fopen($tmp, 'x'); | |
} catch (\ErrorException $e) { | |
if (!str_contains($e->getMessage(), 'File exists')) { | |
throw $e; | |
} | |
$tmp = $this->directory.$this->tmpSuffix = str_replace('/', '-', base64_encode(random_bytes(6))); | |
$h = fopen($tmp, 'x'); | |
} | |
fwrite($h, $data); | |
fclose($h); | |
if (null !== $expiresAt) { | |
touch($tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds | |
} | |
return rename($tmp, $file); | |
} finally { | |
restore_error_handler(); | |
} | |
} |
Now, let's see what the rename()
function do:
- the file is copied to its target path;
- the ownership of the target file is updated;
- the mod of the target file is updated;
- the source file is deleted.
If any of the 3 first steps fails, the unlink is not done.
Related code: https://github.com/php/php-src/blob/221b4fe246e62c5d59f617ee4cbe6fd4c614fb5a/main/streams/plain_wrapper.c#L1272-L1371
How to reproduce
- Create a cache entry with a short lifetime (1s).
- Make the corresponding file read-only on the filesystem.
- Update the cache entry.
-> The temporary file created on the cache directory root is not deleted.
Possible Solution
On Symfony 4.4, a cleaning operation was always done after the rename, see #39059. I think it could be simple to put back this cleaning operation, to prevent filesystem saturation.
Additional Context
No response