From 5809b35b29bb46f26037e30e136c2f375650c475 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 12 Jan 2023 23:08:30 +0100 Subject: [PATCH 1/2] Introduce a way to (de)serialize subclasses of un(de)serializable classes if the appropriate methods are present This introduces the class flag ZEND_ACC_SUBCLASS_SERIALIZABLE, which allows classes which are not serializable / deserializable to become that if a subclass implements the appropriate methods. Setting this flag through the stubs can be done using @subclass-serializable. Introducing this through behaviour a new flag allows for opt-in behaviour, which is backwards compatible. Fixes GH-8996. --- Zend/zend_compile.h | 9 +++++++-- Zend/zend_inheritance.c | 11 ++++++++++- build/gen_stub.php | 22 +++++++++++++++++++++- ext/dom/php_dom.stub.php | 15 ++++++++++++--- ext/dom/php_dom_arginfo.h | 8 ++++---- ext/dom/tests/not_serializable.phpt | 8 ++++---- ext/intl/formatter/formatter.stub.php | 5 ++++- ext/intl/formatter/formatter_arginfo.h | 4 ++-- ext/intl/tests/bug74063.phpt | 2 +- ext/simplexml/simplexml.stub.php | 5 ++++- ext/simplexml/simplexml_arginfo.h | 4 ++-- ext/standard/var.c | 9 +++++++-- ext/standard/var_unserializer.re | 9 +++++++-- 13 files changed, 85 insertions(+), 26 deletions(-) diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 765e54fb56ee8..cc5a2bd547bd7 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -239,7 +239,7 @@ typedef struct _zend_oparray_context { /* or IS_CONSTANT_VISITED_MARK | | | */ #define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */ /* | | | */ -/* Class Flags (unused: 30,31) | | | */ +/* Class Flags (unused: 31) | | | */ /* =========== | | | */ /* | | | */ /* Special class types | | | */ @@ -305,7 +305,12 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ -/* Function Flags (unused: 29-30) | | | */ +/* Subclass can be serialized or unserialized even if | | | */ +/* the parent cannot be, on the condition that the | | | */ +/* subclass implements the appropriate methods. | | | */ +#define ZEND_ACC_SUBCLASS_SERIALIZABLE (1 << 30) /* X | | | */ +/* | | | */ +/* Function Flags (unused: 28-30) | | | */ /* ============== | | | */ /* | | | */ /* deprecation flag | | | */ diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index d1c66b310a6b4..43d626314fba2 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -1672,7 +1672,16 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par ce->ce_flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS; } } - ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_READONLY_PROPS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES); + ce->ce_flags |= parent_ce->ce_flags & (ZEND_HAS_STATIC_IN_METHODS | ZEND_ACC_HAS_TYPE_HINTS | ZEND_ACC_HAS_READONLY_PROPS | ZEND_ACC_USE_GUARDS | ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES | ZEND_ACC_SUBCLASS_SERIALIZABLE); + + if ((parent_ce->ce_flags & (ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_SUBCLASS_SERIALIZABLE)) == (ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_SUBCLASS_SERIALIZABLE)) { + if (ce->__serialize || ce->__unserialize + || ce->serialize || ce->unserialize + || zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_SLEEP)) + || zend_hash_find_known_hash(&ce->function_table, ZSTR_KNOWN(ZEND_STR_WAKEUP))) { + ce->ce_flags &= ~(ZEND_ACC_NOT_SERIALIZABLE | ZEND_ACC_SUBCLASS_SERIALIZABLE); + } + } } /* }}} */ diff --git a/build/gen_stub.php b/build/gen_stub.php index 84d7944a786a7..5906cb95ab0cc 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -23,7 +23,8 @@ const PHP_81_VERSION_ID = 80100; const PHP_82_VERSION_ID = 80200; const PHP_83_VERSION_ID = 80300; -const ALL_PHP_VERSION_IDS = [PHP_70_VERSION_ID, PHP_80_VERSION_ID, PHP_81_VERSION_ID, PHP_82_VERSION_ID, PHP_83_VERSION_ID]; +const PHP_84_VERSION_ID = 80400; +const ALL_PHP_VERSION_IDS = [PHP_70_VERSION_ID, PHP_80_VERSION_ID, PHP_81_VERSION_ID, PHP_82_VERSION_ID, PHP_83_VERSION_ID, PHP_84_VERSION_ID]; /** * @return FileInfo[] @@ -1845,6 +1846,7 @@ protected function getFlagsByPhpVersion(): array PHP_81_VERSION_ID => [$flags], PHP_82_VERSION_ID => [$flags], PHP_83_VERSION_ID => [$flags], + PHP_84_VERSION_ID => [$flags], ]; } @@ -2528,6 +2530,7 @@ class ClassInfo { /** @var AttributeInfo[] */ public array $attributes; public bool $isNotSerializable; + public bool $isSubclassSerializable; /** @var Name[] */ public array $extends; /** @var Name[] */ @@ -2563,6 +2566,7 @@ public function __construct( bool $isStrictProperties, array $attributes, bool $isNotSerializable, + bool $isSubclassSerializable, array $extends, array $implements, array $constInfos, @@ -2582,6 +2586,7 @@ public function __construct( $this->isStrictProperties = $isStrictProperties; $this->attributes = $attributes; $this->isNotSerializable = $isNotSerializable; + $this->isSubclassSerializable = $isSubclassSerializable; $this->extends = $extends; $this->implements = $implements; $this->constInfos = $constInfos; @@ -2795,12 +2800,19 @@ private function getFlagsByPhpVersion(): array $php83Flags = $php82Flags; + $php84Flags = $php83Flags; + + if ($this->isSubclassSerializable) { + $php84Flags[] = "ZEND_ACC_SUBCLASS_SERIALIZABLE"; + } + return [ PHP_70_VERSION_ID => $php70Flags, PHP_80_VERSION_ID => $php80Flags, PHP_81_VERSION_ID => $php81Flags, PHP_82_VERSION_ID => $php82Flags, PHP_83_VERSION_ID => $php83Flags, + PHP_84_VERSION_ID => $php84Flags, ]; } @@ -3712,6 +3724,7 @@ function parseClass( $isDeprecated = false; $isStrictProperties = false; $isNotSerializable = false; + $isSubclassSerializable = false; $allowsDynamicProperties = false; $attributes = []; @@ -3728,6 +3741,8 @@ function parseClass( $isNotSerializable = true; } else if ($tag->name === 'undocumentable') { $isUndocumentable = true; + } else if ($tag->name == 'subclass-serializable') { + $isSubclassSerializable = true; } } } @@ -3745,6 +3760,10 @@ function parseClass( throw new Exception("A class may not have '@strict-properties' and '#[\\AllowDynamicProperties]' at the same time."); } + if (!$isNotSerializable && $isSubclassSerializable) { + throw new Exception("@subclass-serializable without @not-serializable is a no-op"); + } + $extends = []; $implements = []; @@ -3783,6 +3802,7 @@ function parseClass( $isStrictProperties, $attributes, $isNotSerializable, + $isSubclassSerializable, $extends, $implements, $consts, diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php index fb8faaab10f57..93b2afb7e8aa1 100644 --- a/ext/dom/php_dom.stub.php +++ b/ext/dom/php_dom.stub.php @@ -294,7 +294,10 @@ public function after(...$nodes): void; public function replaceWith(...$nodes): void; } -/** @not-serializable */ +/** + * @not-serializable + * @subclass-serializable + */ class DOMNode { public const int DOCUMENT_POSITION_DISCONNECTED = 0x01; @@ -415,7 +418,10 @@ public function getRootNode(?array $options = null): DOMNode {} public function compareDocumentPosition(DOMNode $other): int {} } -/** @not-serializable */ +/** + * @not-serializable + * @subclass-serializable + */ class DOMNameSpaceNode { /** @readonly */ @@ -977,7 +983,10 @@ public function __construct(string $name, string $value = "") {} } #ifdef LIBXML_XPATH_ENABLED -/** @not-serializable */ +/** + * @not-serializable + * @subclass-serializable + */ class DOMXPath { /** @readonly */ diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h index 9ed5aa443cb54..64641d25ea845 100644 --- a/ext/dom/php_dom_arginfo.h +++ b/ext/dom/php_dom_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 9142d10743d4ec2f5e587c67bd111155c6149efd */ + * Stub hash: 5b1f6242a564eb790ec28541055fd25ee56dfeba */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_dom_import_simplexml, 0, 1, DOMElement, 0) ZEND_ARG_TYPE_INFO(0, node, IS_OBJECT, 0) @@ -1093,7 +1093,7 @@ static zend_class_entry *register_class_DOMNode(void) INIT_CLASS_ENTRY(ce, "DOMNode", class_DOMNode_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_SUBCLASS_SERIALIZABLE; zval const_DOCUMENT_POSITION_DISCONNECTED_value; ZVAL_LONG(&const_DOCUMENT_POSITION_DISCONNECTED_value, 0x1); @@ -1257,7 +1257,7 @@ static zend_class_entry *register_class_DOMNameSpaceNode(void) INIT_CLASS_ENTRY(ce, "DOMNameSpaceNode", class_DOMNameSpaceNode_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_SUBCLASS_SERIALIZABLE; zval property_nodeName_default_value; ZVAL_UNDEF(&property_nodeName_default_value); @@ -1835,7 +1835,7 @@ static zend_class_entry *register_class_DOMXPath(void) INIT_CLASS_ENTRY(ce, "DOMXPath", class_DOMXPath_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_SUBCLASS_SERIALIZABLE; zval property_document_default_value; ZVAL_UNDEF(&property_document_default_value); diff --git a/ext/dom/tests/not_serializable.phpt b/ext/dom/tests/not_serializable.phpt index 9869a8c87e35a..fb6ca6ab4bdfb 100644 --- a/ext/dom/tests/not_serializable.phpt +++ b/ext/dom/tests/not_serializable.phpt @@ -36,7 +36,7 @@ try { ?> --EXPECT-- -Serialization of 'DOMDocument' is not allowed -Serialization of 'DOMElement' is not allowed -Serialization of 'DOMXPath' is not allowed -Serialization of 'DOMNameSpaceNode' is not allowed +Serialization of 'DOMDocument' is not allowed, unless you extend the class and provide a serialisation method +Serialization of 'DOMElement' is not allowed, unless you extend the class and provide a serialisation method +Serialization of 'DOMXPath' is not allowed, unless you extend the class and provide a serialisation method +Serialization of 'DOMNameSpaceNode' is not allowed, unless you extend the class and provide a serialisation method diff --git a/ext/intl/formatter/formatter.stub.php b/ext/intl/formatter/formatter.stub.php index 84ae31b518380..698c8310b5053 100644 --- a/ext/intl/formatter/formatter.stub.php +++ b/ext/intl/formatter/formatter.stub.php @@ -2,7 +2,10 @@ /** @generate-class-entries */ -/** @not-serializable */ +/** + * @not-serializable + * @subclass-serializable + */ class NumberFormatter { /* UNumberFormatStyle constants */ diff --git a/ext/intl/formatter/formatter_arginfo.h b/ext/intl/formatter/formatter_arginfo.h index 27851b180514f..782e309cd1c6b 100644 --- a/ext/intl/formatter/formatter_arginfo.h +++ b/ext/intl/formatter/formatter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 3b050eaf6f2f54e3726b04a17d40b06ad610a724 */ + * Stub hash: 2c83a6c7f0aeaaaee5327e6a5f4112a673034b2f */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NumberFormatter___construct, 0, 0, 2) ZEND_ARG_TYPE_INFO(0, locale, IS_STRING, 0) @@ -126,7 +126,7 @@ static zend_class_entry *register_class_NumberFormatter(void) INIT_CLASS_ENTRY(ce, "NumberFormatter", class_NumberFormatter_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_SUBCLASS_SERIALIZABLE; zval const_PATTERN_DECIMAL_value; ZVAL_LONG(&const_PATTERN_DECIMAL_value, UNUM_PATTERN_DECIMAL); diff --git a/ext/intl/tests/bug74063.phpt b/ext/intl/tests/bug74063.phpt index b563dea40117b..58f693a599e3f 100644 --- a/ext/intl/tests/bug74063.phpt +++ b/ext/intl/tests/bug74063.phpt @@ -12,4 +12,4 @@ try { } ?> --EXPECT-- -Serialization of 'NumberFormatter' is not allowed +Serialization of 'NumberFormatter' is not allowed, unless you extend the class and provide a serialisation method diff --git a/ext/simplexml/simplexml.stub.php b/ext/simplexml/simplexml.stub.php index 4735573521587..8c8e98b247a73 100644 --- a/ext/simplexml/simplexml.stub.php +++ b/ext/simplexml/simplexml.stub.php @@ -8,7 +8,10 @@ function simplexml_load_string(string $data, ?string $class_name = SimpleXMLElem function simplexml_import_dom(SimpleXMLElement|DOMNode $node, ?string $class_name = SimpleXMLElement::class): ?SimpleXMLElement {} -/** @not-serializable */ +/** + * @not-serializable + * @subclass-serializable + */ class SimpleXMLElement implements Stringable, Countable, RecursiveIterator { /** @tentative-return-type */ diff --git a/ext/simplexml/simplexml_arginfo.h b/ext/simplexml/simplexml_arginfo.h index f6100eb243e31..0c79044764d95 100644 --- a/ext/simplexml/simplexml_arginfo.h +++ b/ext/simplexml/simplexml_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 06c88dc2fb5582a6d21c11aee6ac0a0538e70cbc */ + * Stub hash: 373d138420453f0b7a7e6fdd711b869229e97c5a */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_simplexml_load_file, 0, 1, SimpleXMLElement, MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, filename, IS_STRING, 0) @@ -170,7 +170,7 @@ static zend_class_entry *register_class_SimpleXMLElement(zend_class_entry *class INIT_CLASS_ENTRY(ce, "SimpleXMLElement", class_SimpleXMLElement_methods); class_entry = zend_register_internal_class_ex(&ce, NULL); - class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + class_entry->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_SUBCLASS_SERIALIZABLE; zend_class_implements(class_entry, 3, class_entry_Stringable, class_entry_Countable, class_entry_RecursiveIterator); return class_entry; diff --git a/ext/standard/var.c b/ext/standard/var.c index 4f04ff6c0deb3..8334a5eb3bdd0 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -1054,8 +1054,13 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_ uint32_t count; if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { - zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed", - ZSTR_VAL(ce->name)); + if (ce->ce_flags & ZEND_ACC_SUBCLASS_SERIALIZABLE) { + zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed, unless you extend the class and provide a serialisation method", + ZSTR_VAL(ce->name)); + } else { + zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed", + ZSTR_VAL(ce->name)); + } return; } diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index a050fb5f74a70..10c165f0c19d6 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -1277,8 +1277,13 @@ object ":" uiv ":" ["] { *p = YYCURSOR; if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { - zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", - ZSTR_VAL(ce->name)); + if (ce->ce_flags & ZEND_ACC_SUBCLASS_SERIALIZABLE) { + zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed, unless you extend the class and provide a unserialisation method", + ZSTR_VAL(ce->name)); + } else { + zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", + ZSTR_VAL(ce->name)); + } zend_string_release_ex(class_name, 0); return 0; } From 18258cee45d8350d11cc83ed917b9dfedbf04e2f Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Thu, 12 Jan 2023 23:12:31 +0100 Subject: [PATCH 2/2] Add a regression test for GH-8996 Co-authored-by: wazelin --- ext/dom/tests/gh8996.phpt | Bin 0 -> 1573 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ext/dom/tests/gh8996.phpt diff --git a/ext/dom/tests/gh8996.phpt b/ext/dom/tests/gh8996.phpt new file mode 100644 index 0000000000000000000000000000000000000000..995d195b04569362ff7c096f27db945712bffb61 GIT binary patch literal 1573 zcmcIkZBN@U5Z>qfijx&7i9{F$X#=;E4{I4nK+#$xq;*3kIofE6Q^ihzRsHX~yf*Dt z32mB{5Apf#p2yGm&UGil@x*nF&lm3T$;k(>2czqo*hf&91R@N63LV4}=)1kRh407v z9mjCp;r(QIGrk<%j49NQBQl*|UJYr}J6p^bhT(-mDHzu|9zz_&(IEEH2&D#m(I|Zd zgjL&&r2(`SNstMRV7^4*Kxol5N^|-c1|B@6(ktjZJg5+HVLKc|l8D#BsmzdPG4(u9 zZokR_r?o+e4DYY6Y^PfU$~Ii z{++sJBdSkgG@DvsvvuX%p|!PJp?RIqo5#Rva_IA^bk}PtTkcyYUr^~d`8{H}f$sK; zHi<&pSuw&p>lY1wJN%T-393UNXLjfC$XS!9X2cV{xTbPq^F><_D3GxR5r*+I`g?T6 zkX{PB#%VPVK+h4%#t_n{Q4nVV4I)virAn9x^*r|3n#o6E(!wq0)&Jts6lRM}s~#j* zvy2fxJmO8YwnSmP!