8000 bug #28414 [VarExporter] fix exporting final serializable classes ext… · sroze/symfony@3b931fe · GitHub
[go: up one dir, main page]

Skip to content

Commit 3b931fe

Browse files
bug symfony#28414 [VarExporter] fix exporting final serializable classes extending internal ones (nicolas-grekas)
This PR was merged into the 4.2-dev branch. Discussion ---------- [VarExporter] fix exporting final serializable classes extending internal ones | Q | A | ------------- | --- | Branch? | master | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Another edge case, discovered while reading doctrine/instantiator#39 Commits ------- a5bf9b0 [VarExporter] fix exporting final serializable classes extending internal ones
2 parents 1a46605 + a5bf9b0 commit 3b931fe

File tree

6 files changed

+81
-25
lines changed

6 files changed

+81
-25
lines changed

src/Symfony/Component/VarExporter/Internal/Exporter.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
7676
$sleep = null;
7777
$arrayValue = (array) $value;
7878

79-
if (!isset(Registry::$prototypes[$class])) {
79+
if (!isset(Registry::$reflectors[$class])) {
8080
// Might throw Exception("Serialization of '...' is not allowed")
8181
Registry::getClassReflector($class);
8282
serialize(Registry::$prototypes[$class]);
@@ -87,14 +87,16 @@ public static function prepare($values, $objectsPool, &$refsPool, &$objectsCount
8787
$reflector = Registry::$reflectors[$class];
8888
$proto = Registry::$prototypes[$class];
8989

90-
if ($value instanceof \ArrayIterator || $value instanceof \ArrayObject) {
90+
if (($value instanceof \ArrayIterator || $value instanceof \ArrayObject) && null !== $proto) {
9191
// ArrayIterator and ArrayObject need special care because their "flags"
9292
// option changes the behavior of the (array) casting operator.
93-
$proto = Registry::$cloneable[$class] ? clone Registry::$prototypes[$class] : $reflector->newInstanceWithoutConstructor();
9493
$properties = self::getArrayObjectProperties($value, $arrayValue, $proto);
95-
} elseif ($value instanceof \SplObjectStorage) {
96-
// By implementing Serializable, SplObjectStorage breaks internal references,
97-
// let's deal with it on our own.
94+
95+
// populates Registry::$prototypes[$class] with a new instance
96+
Registry::getClassReflector($class, Registry::$instantiableWithoutConstructor[$class], Registry::$cloneable[$class]);
97+
} elseif ($value instanceof \SplObjectStorage && Registry::$cloneable[$class] && null !== $proto) {
98+
// By implementing Serializable, SplObjectStorage breaks
99+
// internal references; let's deal with it on our own.
98100
foreach (clone $value as $v) {
99101
$properties[] = $v;
100102
$properties[] = $value[$v];
@@ -284,12 +286,15 @@ private static function exportRegistry(Registry $value, string $indent, string $
284286
continue;
285287
}
286288
if (!Registry::$instantiableWithoutConstructor[$class]) {
287-
if (is_subclass_of($class, 'Throwable')) {
288-
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
289-
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}';
289+
if (is_subclass_of($class, 'Serializable')) {
290+
$serializables[$k] = 'C:'.\strlen($class).':"'.$class.'":0:{}';
290291
} else {
291292
$serializables[$k] = 'O:'.\strlen($class).':"'.$class.'":0:{}';
292293
}
294+
if (is_subclass_of($class, 'Throwable')) {
295+
$eol = is_subclass_of($class, 'Error') ? "\0Error\0" : "\0Exception\0";
296+
$serializables[$k] = substr_replace($serializables[$k], '1:{s:'.(5 + \strlen($eol)).':"'.$eol.'trace";a:0:{}}', -4);
297+
}
293298
continue;
294299
}
295300
$code .= $subIndent.(1 !== $k - $j ? $k.' => ' : '');

src/Symfony/Component/VarExporter/Internal/Registry.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ public static function unserialize($objects, $serializables)
3737

3838
try {
3939
foreach ($serializables as $k => $v) {
40-
$objects[$k] = unserialize($v);
40+
if (false === $objects[$k] = unserialize($v)) {
41+
throw new \Exception(error_get_last()['message'] ?? 'unserialize(): unknown error');
42+
}
4143
}
4244
} finally {
4345
ini_set('unserialize_callback_func', $unserializeCallback);
@@ -67,18 +69,16 @@ public static function getClassReflector($class, $instantiableWithoutConstructor
6769
if (self::$instantiableWithoutConstructor[$class] = $instantiableWithoutConstructor || !$reflector->isFinal()) {
6870
$proto = $reflector->newInstanceWithoutConstructor();
6971
} else {
70-
$r = $reflector;
71-
do {
72-
if ($r->isInternal()) {
73-
if (false === $proto = @unserialize('O:'.\strlen($class).':"'.$class.'":0:{}')) {
74-
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
75-
}
76-
break;
77-
}
78-
} while ($r = $r->getParentClass());
79-
80-
if (self::$instantiableWithoutConstructor[$class] = !$r) {
72+
try {
8173
$proto = $reflector->newInstanceWithoutConstructor();
74+
self::$instantiableWithoutConstructor[$class] = true;
75+
} catch (\ReflectionException $e) {
76+
$proto = $reflector->implementsInterface('Serializable') ? 'C:' : 'O:';
77+
if ('C:' === $proto && !$reflector->getMethod('unserialize')->isInternal()) {
78+
$proto = null;
79+
} elseif (false === $proto = @unserialize($proto.\strlen($class).':"'.$class.'":0:{}')) {
80+
throw new \Exception(sprintf("Serialization of '%s' is not allowed", $class));
81+
}
8282
}
8383
}
8484

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
4+
$o = \Symfony\Component\VarExporter\Internal\Registry::unserialize([], [
5+
'C:54:"Symfony\\Component\\VarExporter\\Tests\\FinalArrayIterator":49:{a:2:{i:0;i:123;i:1;s:21:"x:i:0;a:0:{};m:a:0:{}";}}',
6+
]),
7+
null,
8+
[],
9+
$o[0],
10+
[]
11+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
return \Symfony\Component\VarExporter\Internal\Hydrator::hydrate(
4+
$o = [
5+
(\Symfony\Component\VarExporter\Internal\Registry::$factories[\Symfony\Component\VarExporter\Tests\FinalStdClass::class] ?? \Symfony\Component\VarExporter\Internal\Registry::f(\Symfony\Component\VarExporter\Tests\FinalStdClass::class))(),
6+
],
7+
null,
8+
[],
9+
$o[0],
10+
[]
11+
);

src/Symfony/Component/VarExporter/Tests/VarExporterTest.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ public function provideFailingSerialization()
6868
}
6969

7070
/**
71-
* @dataProvider provideMarshall
71+
* @dataProvider provideExport
7272
*/
73-
public function testMarshall(string $testName, $value, bool $staticValueExpected = false)
73+
public function testExport(string $testName, $value, bool $staticValueExpected = false)
7474
{
7575
$dumpedValue = $this->getDump($value);
7676
$isStaticValue = true;
@@ -98,7 +98,7 @@ public function testMarshall(string $testName, $value, bool $staticValueExpected
9898
}
9999
}
100100

101-
public function provideMarshall()
101+
public function provideExport()
102102
{
103103
yield array('multiline-string', array("\0\0\r\nA" => "B\rC\n\n"), true);
104104

@@ -175,6 +175,10 @@ public function provideMarshall()
175175
$rl->setValue($value, 123);
176176

177177
yield array('final-error', $value);
178+
179+
yield array('final-array-iterator', new FinalArrayIterator());
180+
181+
yield array('final-stdclass', new FinalStdClass());
178182
}
179183
}
180184

@@ -278,3 +282,28 @@ public function __construct(bool $throw = true)
278282
}
279283
}
280284
}
285+
286+
final class FinalArrayIterator extends \ArrayIterator
287+
{
288+
public function serialize()
289+
{
290+
return serialize(array(123, parent::serialize()));
291+
}
292+
293+
public function unserialize($data)
294+
{
295+
if ('' === $data) {
296+
throw new \InvalidArgumentException('Serialized data is empty.');
297+
}
298+
list(, $data) = unserialize($data);
299+
parent::unserialize($data);
300+
}
301+
}
302+
303+
final class FinalStdClass extends \stdClass
304+
{
305+
public function __clone()
306+
{
307+
throw new \BadMethodCallException('Should not be called.');
308+
}
309+
}

src/Symfony/Component/VarExporter/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "symfony/var-exporter",
33
"type": "library",
44
"description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
5-
"keywords": ["export", "serialize"],
5+
"keywords": ["export", "serialize", "instantiate", "hydrate", "construct", "clone"],
66
"homepage": "https://symfony.com",
77
"license": "MIT",
88
"authors": [

0 commit comments

Comments
 (0)
0