[Cache] Handle unserialize() failures gracefully

This commit is contained in:
Nicolas Grekas 2016-08-08 15:21:59 +02:00
parent 0416b8ab02
commit 47db638fa1
8 changed files with 144 additions and 23 deletions

View File

@ -350,6 +350,33 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface
}
}
/**
* Like the native unserialize() function but throws an exception if anything goes wrong.
*
* @param string $value
*
* @return mixed
*
* @throws \Exception
*/
protected static function unserialize($value)
{
if ('b:0;' === $value) {
return false;
}
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
if (false !== $value = unserialize($value)) {
return $value;
}
throw new \DomainException('Failed to unserialize cached value');
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
private function getId($key)
{
CacheItem::validateKey($key);
@ -361,13 +388,26 @@ abstract class AbstractAdapter implements AdapterInterface, LoggerAwareInterface
{
$f = $this->createCacheItem;
foreach ($items as $id => $value) {
yield $keys[$id] => $f($keys[$id], $value, true);
unset($keys[$id]);
try {
foreach ($items as $id => $value) {
$key = $keys[$id];
unset($keys[$id]);
yield $key => $f($key, $value, true);
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to fetch requested items', array('keys' => array_values($keys), 'exception' => $e));
}
foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
/**
* @internal
*/
public static function handleUnserializeCallback($class)
{
throw new \DomainException('Class not found: '.$class);
}
}

View File

@ -49,7 +49,11 @@ class ApcuAdapter extends AbstractAdapter
*/
protected function doFetch(array $ids)
{
return apcu_fetch($ids);
try {
return apcu_fetch($ids);
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
}
}
/**

View File

@ -55,12 +55,22 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface
*/
public function getItem($key)
{
if (!$isHit = $this->hasItem($key)) {
$isHit = $this->hasItem($key);
try {
if (!$isHit) {
$value = null;
} elseif (!$this->storeSerialized) {
$value = $this->values[$key];
} elseif ('b:0;' === $value = $this->values[$key]) {
$value = false;
} elseif (false === $value = unserialize($value)) {
$value = null;
$isHit = false;
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e));
$value = null;
} elseif ($this->storeSerialized) {
$value = unserialize($this->values[$key]);
} else {
$value = $this->values[$key];
$isHit = false;
}
$f = $this->createCacheItem;
@ -181,16 +191,30 @@ class ArrayAdapter implements AdapterInterface, LoggerAwareInterface
{
$f = $this->createCacheItem;
foreach ($keys as $key) {
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
foreach ($keys as $i => $key) {
try {
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] >= $now || !$this->deleteItem($key))) {
$value = null;
} elseif (!$this->storeSerialized) {
$value = $this->values[$key];
} elseif ('b:0;' === $value = $this->values[$key]) {
$value = false;
} elseif (false === $value = unserialize($value)) {
$value = null;
$isHit = false;
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to unserialize key "{key}"', array('key' => $key, 'exception' => $e));
$value = null;
} elseif ($this->storeSerialized) {
$value = unserialize($this->values[$key]);
} else {
$value = $this->values[$key];
$isHit = false;
}
unset($keys[$i]);
yield $key => $f($key, $value, $isHit);
}
foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
}

View File

@ -32,7 +32,25 @@ class DoctrineAdapter extends AbstractAdapter
*/
protected function doFetch(array $ids)
{
return $this->provider->fetchMultiple($ids);
$unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
try {
return $this->provider->fetchMultiple($ids);
} catch (\Error $e) {
$trace = $e->getTrace();
if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
switch ($trace[0]['function']) {
case 'unserialize':
case 'apcu_fetch':
case 'apc_fetch':
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
}
}
throw $e;
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**

View File

@ -60,7 +60,7 @@ class FilesystemAdapter extends AbstractAdapter
foreach ($ids as $id) {
$file = $this->getFile($id);
if (!$h = @fopen($file, 'rb')) {
if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
continue;
}
if ($now >= (int) $expiresAt = fgets($h)) {
@ -73,7 +73,7 @@ class FilesystemAdapter extends AbstractAdapter
$value = stream_get_contents($h);
fclose($h);
if ($i === $id) {
$values[$id] = unserialize($value);
$values[$id] = parent::unserialize($value);
}
}
}

View File

@ -134,19 +134,15 @@ class RedisAdapter extends AbstractAdapter
*/
protected function doFetch(array $ids)
{
$result = array();
if ($ids) {
$values = $this->redis->mGet($ids);
$index = 0;
foreach ($ids as $id) {
if ($value = $values[$index++]) {
$result[$id] = unserialize($value);
yield $id => parent::unserialize($value);
}
}
}
return $result;
}
/**

View File

@ -46,4 +46,42 @@ abstract class AdapterTestCase extends CachePoolTest
$item = $cache->getItem('key.dlt');
$this->assertFalse($item->isHit());
}
public function testNotUnserializable()
{
if (isset($this->skippedTests[__FUNCTION__])) {
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
return;
}
$cache = $this->createCachePool();
$item = $cache->getItem('foo');
$cache->save($item->set(new NotUnserializable()));
$item = $cache->getItem('foo');
$this->assertFalse($item->isHit());
foreach ($cache->getItems(array('foo')) as $item) {
}
$cache->save($item->set(new NotUnserializable()));
foreach ($cache->getItems(array('foo')) as $item) {
}
$this->assertFalse($item->isHit());
}
}
class NotUnserializable implements \Serializable
{
public function serialize()
{
return serialize(123);
}
public function unserialize($ser)
{
throw new \Exception(__CLASS__);
}
}

View File

@ -22,6 +22,7 @@ class DoctrineAdapterTest extends AdapterTestCase
protected $skippedTests = array(
'testDeferredSaveWithoutCommit' => 'Assumes a shared cache which ArrayCache is not.',
'testSaveWithoutExpire' => 'Assumes a shared cache which ArrayCache is not.',
'testNotUnserializable' => 'ArrayCache does not use serialize/unserialize',
);
public function createCachePool($defaultLifetime = 0)