[php-src] PHP-8.6: standard: Create incomplete objects again from the `C` format (#24020)
| From: | Nicolas Grekas via GitHub | Date: | Wed, 30 Sep 2026 20:23:04 +0000 |
| Subject: | [php-src] PHP-8.6: standard: Create incomplete objects again from the `C` format (#24020) | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-141820@lists.php.net to get a copy of this message | ||
Author: Nicolas Grekas (nicolas-grekas)
Committer: GitHub (web-flow)
Pusher: kocsismate
Date: 2026-09-30T22:23:01+02:00
Commit: https://github.com/php/php-src/commit/4bf700e63e080ad16f0c14363c9a876111592b7a
Raw diff: https://github.com/php/php-src/commit/4bf700e63e080ad16f0c14363c9a876111592b7a.diff
standard: Create incomplete objects again from the
C format (#24020)
Since GH-22046 was fixed, unserialize() fails as a whole when a C payload names a class
that is missing or not in allowed_classes, because the __PHP_Incomplete_Class substituted for it has
no unserializer either.
The incomplete class has no internal state, so create an empty one as before and keep failing only
for real classes that are not Serializable.
Changed paths:
A ext/standard/tests/serialize/incomplete_class_c_format.phpt
A ext/standard/tests/serialize/incomplete_class_c_format_allowed_classes.phpt
M ext/standard/tests/serialize/serialization_objects_009.phpt
M ext/standard/var_unserializer.re
Diff:
diff --git a/ext/standard/tests/serialize/incomplete_class_c_format.phpt
b/ext/standard/tests/serialize/incomplete_class_c_format.phpt
new file mode 100644
index 000000000000..258e76921c4a
--- /dev/null
+++ b/ext/standard/tests/serialize/incomplete_class_c_format.phpt
@@ -0,0 +1,36 @@
+--TEST--
+unserialize() turns C: objects of missing classes into incomplete objects
+--FILE--
+<?php
+
+var_dump(unserialize('a:2:{i:0;C:7:"Missing":4:{abcd}i:1;s:4:"tail";}'));
+
+function callback(string $class): void
+{
+ echo __FUNCTION__, "($class)\n";
+}
+
+ini_set('unserialize_callback_func', 'callback');
+
+var_dump(unserialize('C:7:"Missing":4:{abcd}'));
+?>
+--EXPECTF--
+Warning: Class __PHP_Incomplete_Class has no unserializer in %s on line %d
+array(2) {
+ [0]=>
+ object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(7) "Missing"
+ }
+ [1]=>
+ string(4) "tail"
+}
+callback(Missing)
+
+Warning: unserialize(): Function callback() hasn't defined the class it was called for in %s
on line %d
+
+Warning: Class __PHP_Incomplete_Class has no unserializer in %s on line %d
+object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(7) "Missing"
+}
diff --git a/ext/standard/tests/serialize/incomplete_class_c_format_allowed_classes.phpt
b/ext/standard/tests/serialize/incomplete_class_c_format_allowed_classes.phpt
new file mode 100644
index 000000000000..54a68a935ef1
--- /dev/null
+++ b/ext/standard/tests/serialize/incomplete_class_c_format_allowed_classes.phpt
@@ -0,0 +1,63 @@
+--TEST--
+unserialize() with allowed_classes turns C: objects of classes that are not allowed into incomplete
objects
+--FILE--
+<?php
+
+final class Kept
+{
+ public $a = 1;
+}
+
+final class Blocked implements Serializable
+{
+ public function serialize(): string
+ {
+ return '';
+ }
+
+ public function unserialize(string $data): void
+ {
+ echo __METHOD__, "\n";
+ }
+
+ public function __serialize(): array
+ {
+ return [];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ echo __METHOD__, "\n";
+ }
+}
+
+$payload =
'a:4:{i:0;O:4:"Kept":1:{s:1:"a";i:2;}i:1;C:7:"Blocked":4:{abcd}i:2;C:7:"Missing":4:{abcd}i:3;r:4;}';
+
+var_dump(unserialize($payload, ['allowed_classes' => ['Kept']]));
+?>
+--EXPECTF--
+Warning: Class __PHP_Incomplete_Class has no unserializer in %s on line %d
+
+Warning: Class __PHP_Incomplete_Class has no unserializer in %s on line %d
+array(4) {
+ [0]=>
+ object(Kept)#%d (1) {
+ ["a"]=>
+ int(2)
+ }
+ [1]=>
+ object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(7) "Blocked"
+ }
+ [2]=>
+ object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(7) "Missing"
+ }
+ [3]=>
+ object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(7) "Blocked"
+ }
+}
diff --git a/ext/standard/tests/serialize/serialization_objects_009.phpt
b/ext/standard/tests/serialize/serialization_objects_009.phpt
index 95b85ccd80f7..27da8e53ebb4 100644
--- a/ext/standard/tests/serialize/serialization_objects_009.phpt
+++ b/ext/standard/tests/serialize/serialization_objects_009.phpt
@@ -13,11 +13,12 @@ echo "Done";
--EXPECTF--
Warning: Class __PHP_Incomplete_Class has no unserializer in %s on line %d
-Warning: unserialize(): Error at offset 11 of 18 bytes in %s on line %d
-
Warning: Class C has no unserializer in %s on line %d
Warning: unserialize(): Error at offset 11 of 18 bytes in %s on line %d
-bool(false)
+object(__PHP_Incomplete_Class)#%d (1) {
+ ["__PHP_Incomplete_Class_Name"]=>
+ string(1) "C"
+}
bool(false)
Done
diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index 0138471aa63e..e10b067c68d8 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -769,7 +769,11 @@ static inline int object_custom(UNSERIALIZE_PARAMETER, zend_class_entry *ce)
if (ce->unserialize == NULL) {
zend_error(E_WARNING, "Class %s has no unserializer", ZSTR_VAL(ce->name));
- return 0;
+ /* __PHP_Incomplete_Class has no internal state, an empty instance is safe. */
+ if (ce != PHP_IC_ENTRY) {
+ return 0;
+ }
+ object_init_ex(rval, ce);
} else if (ce->unserialize(rval, ce, (const unsigned char*)*p, datalen, (zend_unserialize_data
*)var_hash) != SUCCESS) {
return 0;
}