Skip to content

[Cache] Temporary files not cleaned from filesystem #52092

@cedric-anne

Description

@cedric-anne

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:

  1. a temporary file is created at the root of the cache directory;
  2. content is written inside this file;
  3. this file is renamed to its target path, using the PHP rename() function.

Related code:

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:

  1. the file is copied to its target path;
  2. the ownership of the target file is updated;
  3. the mod of the target file is updated;
  4. 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

  1. Create a cache entry with a short lifetime (1s).
  2. Make the corresponding file read-only on the filesystem.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions