[Cache] Handle unserialize() failures gracefully
This commit is contained in:
parent
0416b8ab02
commit
47db638fa1
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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__);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user