From 7bbc161a745706af65813c60f33c4c520b666966 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 30 Sep 2026 17:24:09 +0200 Subject: [PATCH] standard: Create incomplete objects again from the `C` format 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. --- .../serialize/incomplete_class_c_format.phpt | 36 +++++++++++ ...mplete_class_c_format_allowed_classes.phpt | 63 +++++++++++++++++++ .../serialize/serialization_objects_009.phpt | 7 ++- ext/standard/var_unserializer.re | 6 +- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 ext/standard/tests/serialize/incomplete_class_c_format.phpt create mode 100644 ext/standard/tests/serialize/incomplete_class_c_format_allowed_classes.phpt 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-- + +--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-- + ['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; }