Commit 4bf700e63e0 for php
commit 4bf700e63e080ad16f0c14363c9a876111592b7a
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Wed Sep 30 22:23:01 2026 +0200
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.
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 00000000000..258e76921c4
--- /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 00000000000..54a68a935ef
--- /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 95b85ccd80f..27da8e53ebb 100644
--- a/ext/standard/tests/serialize/serialization_objects_009.phpt
+++ b/ext/standard/tests/serialize/serialization_objects_009.phpt
@@ -13,11 +13,12 @@
--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 0138471aa63..e10b067c68d 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;
}