Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/DeepClone/DeepClone.php
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,12 @@ private static function reconstruct($prepared, $objectMeta, $numObjects, $proper

foreach ($objectMeta as $id => [$class]) {
if (':' === ($class[1] ?? null)) {
$objects[$id] = unserialize($class, null !== $allowedClasses ? ['allowed_classes' => $allowedClasses] : []);
// The result is used as an object below; a malformed payload can
// carry any serialize form (i:…, s:…, a:…), so reject anything
// that did not decode to an object rather than mistreating it.
if (!\is_object($objects[$id] = unserialize($class, null !== $allowedClasses ? ['allowed_classes' => $allowedClasses] : []))) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) object '.$id.' did not unserialize to an object, '.get_debug_type($objects[$id]).' given');
}
continue;
}
try {
Expand Down Expand Up @@ -953,6 +958,9 @@ private static function reconstruct($prepared, $objectMeta, $numObjects, $proper

return $objects[$prepared];
}
if (\PHP_INT_MIN === $prepared) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) "prepared" references unknown ref id out of range');
}
if (!isset($refs[-$prepared])) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) "prepared" references unknown ref id '.(-$prepared));
}
Expand Down Expand Up @@ -980,6 +988,9 @@ private static function resolveWithMask($value, $mask, $objects, &$refs)

return $objects[$value];
}
if (\PHP_INT_MIN === $value) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) malformed payload, ref id out of range');
}
if (!isset($refs[-$value])) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) malformed payload, unknown ref id '.(-$value));
}
Expand Down Expand Up @@ -1091,7 +1102,7 @@ private static function resolveNamedClosureScalar($value, $objects, $refs)
}
$obj = $objects[$obj];
} else {
if (!isset($refs[-$obj])) {
if (\PHP_INT_MIN === $obj || !isset($refs[-$obj])) {
throw new \ValueError('deepclone_from_array(): Argument #1 ($data) malformed payload, named-closure references unknown id '.$obj);
}
$obj = $refs[-$obj];
Expand Down
48 changes: 48 additions & 0 deletions tests/DeepClone/DeepCloneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,16 @@ public function testFromArrayRejectsClassesIntEntry()
deepclone_from_array(['classes' => [42], 'objectMeta' => 0, 'prepared' => 0]);
}

public function testFromArrayRejectsNonObjectUnserializeResult()
{
// A class-name string whose second byte is ':' is replayed through
// unserialize(); a scalar/array serialize form must be rejected rather
// than stored and later treated as an object.
$this->expectException(\ValueError::class);
$this->expectExceptionMessage('deepclone_from_array(): Argument #1 ($data) object 0 did not unserialize to an object, int given');
deepclone_from_array(['classes' => 'i:1234;', 'objectMeta' => 1, 'prepared' => 0]);
}

public function testFromArrayRejectsObjectMetaWrongType()
{
$this->expectException(\ValueError::class);
Expand Down Expand Up @@ -470,6 +480,44 @@ public function testFromArrayRejectsPreparedRefIdUnknown()
deepclone_from_array(['classes' => '', 'objectMeta' => 0, 'prepared' => -99]);
}

public function testFromArrayRejectsIntMinRefIdsWithoutWarning()
{
// Negating PHP_INT_MIN overflows to a float and emits a runtime warning
// before the value can be used as a ref-id array key. Every resolution
// path must reject it cleanly. A strict error handler turns any emitted
// diagnostic into a failure so the warning cannot regress unnoticed.
$cases = [
[
['classes' => 'stdClass', 'objectMeta' => 0, 'prepared' => \PHP_INT_MIN],
'"prepared" references unknown ref id out of range',
],
[
['classes' => 'stdClass', 'objectMeta' => 0, 'prepared' => [0 => \PHP_INT_MIN], 'mask' => [0 => true]],
'malformed payload, ref id out of range',
],
[
['classes' => 'stdClass', 'objectMeta' => 0, 'prepared' => [\PHP_INT_MIN, 'strlen'], 'mask' => 0],
'malformed payload, named-closure references unknown id -9223372036854775808',
],
];

set_error_handler(static function ($type, $message) {
throw new \RuntimeException('unexpected diagnostic: '.$message);
});
try {
foreach ($cases as [$payload, $expected]) {
try {
deepclone_from_array($payload);
$this->fail('Expected ValueError was not thrown');
} catch (\ValueError $e) {
$this->assertStringContainsString($expected, $e->getMessage());
}
}
} finally {
restore_error_handler();
}
}

public function testClosureGlobalFunctionWireFormat()
{
$d = deepclone_to_array(\Closure::fromCallable('strlen'));
Expand Down
Loading