Commit 8d12555b authored by catch's avatar catch

Issue #2527478 by znerol, googletorp: Resolve infinite stampede in mtime protected PHP storage

parent 15b2b5ed
...@@ -102,4 +102,10 @@ public function listAll() { ...@@ -102,4 +102,10 @@ public function listAll() {
return $names; return $names;
} }
/**
* {@inheritdoc}
*/
public function garbageCollection() {
}
} }
...@@ -266,4 +266,10 @@ public function listAll() { ...@@ -266,4 +266,10 @@ public function listAll() {
return $names; return $names;
} }
/**
* {@inheritdoc}
*/
public function garbageCollection() {
}
} }
...@@ -63,7 +63,7 @@ public function __construct(array $configuration) { ...@@ -63,7 +63,7 @@ public function __construct(array $configuration) {
} }
/** /**
* Implements Drupal\Component\PhpStorage\PhpStorageInterface::save(). * {@inheritdoc}
*/ */
public function save($name, $data) { public function save($name, $data) {
$this->ensureDirectory($this->directory); $this->ensureDirectory($this->directory);
...@@ -78,44 +78,32 @@ public function save($name, $data) { ...@@ -78,44 +78,32 @@ public function save($name, $data) {
// permission. // permission.
chmod($temporary_path, 0444); chmod($temporary_path, 0444);
// Prepare a directory dedicated for just this file. Ensure it has a current // Determine the exact modification time of the file.
// mtime so that when the file (hashed on that mtime) is moved into it, the $mtime = $this->getUncachedMTime($temporary_path);
// mtime remains the same (unless the clock ticks to the next second during
// the rename, in which case we'll try again). // Move the temporary file into the proper directory. Note that POSIX
// compliant systems as well as modern Windows perform the rename operation
// atomically, i.e. there is no point at which another process attempting to
// access the new path will find it missing.
$directory = $this->getContainingDirectoryFullPath($name); $directory = $this->getContainingDirectoryFullPath($name);
if (file_exists($directory)) {
$this->unlink($directory);
}
$this->ensureDirectory($directory); $this->ensureDirectory($directory);
$full_path = $this->getFullPath($name, $directory, $mtime);
$result = rename($temporary_path, $full_path);
// Move the file to its final place. The mtime of a directory is the time of // Finally reset the modification time of the directory to match the one of
// the last file create or delete in the directory. So the moving will // the newly created file. In order to prevent the creation of a file if the
// update the directory mtime. However, this update will very likely not // directory does not exist, ensure that the path terminates with a
// show up, because it has a coarse, one second granularity and typical // directory separator.
// moves takes significantly less than that. In the unlucky case the clock //
// ticks during the move, we need to keep trying until the mtime we hashed // Recall that when subsequently loading the file, the hash is calculated
// on and the updated mtime match. // based on the file name, the containing mtime, and a the secret string.
$previous_mtime = 0; // Hence updating the mtime here is comparable to pointing a symbolic link
$i = 0; // at a new target, i.e., the newly created file.
while (($mtime = $this->getUncachedMTime($directory)) && ($mtime != $previous_mtime)) { if ($result) {
$previous_mtime = $mtime; $result &= touch($directory . '/', $mtime);
// Reset the file back in the temporary location if this is not the first
// iteration.
if ($i > 0) {
$this->unlink($temporary_path);
$temporary_path = $this->tempnam($this->directory, '.');
rename($full_path, $temporary_path);
// Make sure to not loop infinitely on a hopelessly slow filesystem.
if ($i > 10) {
$this->unlink($temporary_path);
return FALSE;
}
}
$full_path = $this->getFullPath($name, $directory, $mtime);
rename($temporary_path, $full_path);
$i++;
} }
return TRUE;
return (bool) $result;
} }
/** /**
...@@ -161,6 +149,44 @@ public function delete($name) { ...@@ -161,6 +149,44 @@ public function delete($name) {
return FALSE; return FALSE;
} }
/**
* {@inheritdoc}
*/
public function garbageCollection() {
$flags = \FilesystemIterator::CURRENT_AS_FILEINFO;
$flags += \FilesystemIterator::SKIP_DOTS;
foreach ($this->listAll() as $name) {
$directory = $this->getContainingDirectoryFullPath($name);
try {
$dir_iterator = new \FilesystemIterator($directory, $flags);
}
catch (\UnexpectedValueException $e) {
// FilesystemIterator throws an UnexpectedValueException if the
// specified path is not a directory, or if it is not accessible.
continue;
}
$directory_unlink = TRUE;
$directory_mtime = filemtime($directory);
foreach ($dir_iterator as $fileinfo) {
if ($directory_mtime > $fileinfo->getMTime()) {
// Ensure the folder is writable.
@chmod($directory, 0777);
@unlink($fileinfo->getPathName());
}
else {
// The directory still contains valid files.
$directory_unlink = FALSE;
}
}
if ($directory_unlink) {
$this->unlink($name);
}
}
}
/** /**
* Gets the full path of the containing directory where the file is or should * Gets the full path of the containing directory where the file is or should
* be stored. * be stored.
...@@ -208,4 +234,5 @@ protected function tempnam($directory, $prefix) { ...@@ -208,4 +234,5 @@ protected function tempnam($directory, $prefix) {
} while (file_exists($path)); } while (file_exists($path));
return $path; return $path;
} }
} }
...@@ -99,4 +99,11 @@ public function getFullPath($name); ...@@ -99,4 +99,11 @@ public function getFullPath($name);
*/ */
public function listAll(); public function listAll();
/**
* Performs garbage collection on the storage.
*
* The storage may choose to delete expired or invalidated items.
*/
public function garbageCollection();
} }
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory; use Drupal\Core\KeyValueStore\KeyValueDatabaseExpirableFactory;
use Drupal\Core\PageCache\RequestPolicyInterface; use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Core\PhpStorage\PhpStorageFactory;
use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Menu\MenuTreeParameters;
...@@ -1283,6 +1284,10 @@ function system_cron() { ...@@ -1283,6 +1284,10 @@ function system_cron() {
->condition('expire', 0, '<>') ->condition('expire', 0, '<>')
->condition('expire', REQUEST_TIME, '<') ->condition('expire', REQUEST_TIME, '<')
->execute(); ->execute();
// Clean up PHP storage.
PhpStorageFactory::get('container')->garbageCollection();
PhpStorageFactory::get('service_container')->garbageCollection();
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment