diff --git a/ext/dom/node.c b/ext/dom/node.c
index bcf4ee487d38d..a5bc7d00ffa12 100644
--- a/ext/dom/node.c
+++ b/ext/dom/node.c
@@ -1786,4 +1786,25 @@ PHP_METHOD(DOMNode, getLineNo)
}
/* }}} */
+/**
+ * We want to block the serialization and unserialization of DOM classes.
+ * However, using @not-serializable makes the child classes also not serializable, even if the user implements the methods.
+ * So instead, we implement the methods wherein we throw exceptions.
+ * The reason we choose these methods is because:
+ * - If the user implements __serialize / __unserialize, the respective throwing methods are not called.
+ * - If the user implements __sleep / __wakeup, then it's also not a problem because they will not enter the throwing methods.
+ */
+
+PHP_METHOD(DOMNode, __sleep)
+{
+ zend_throw_exception_ex(NULL, 0, "Serialization of '%s' is not allowed", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name));
+ RETURN_THROWS();
+}
+
+PHP_METHOD(DOMNode, __wakeup)
+{
+ zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name));
+ RETURN_THROWS();
+}
+
#endif
diff --git a/ext/dom/php_dom.stub.php b/ext/dom/php_dom.stub.php
index bbf7bad0f1491..3fb2909a6f922 100644
--- a/ext/dom/php_dom.stub.php
+++ b/ext/dom/php_dom.stub.php
@@ -56,7 +56,6 @@ public function after(...$nodes): void;
public function replaceWith(...$nodes): void;
}
-/** @not-serializable */
class DOMNode
{
/** @readonly */
@@ -104,6 +103,10 @@ class DOMNode
public string $textContent;
+ public function __sleep(): array {}
+
+ public function __wakeup(): void {}
+
/** @return DOMNode|false */
public function appendChild(DOMNode $node) {}
@@ -156,7 +159,6 @@ public function removeChild(DOMNode $child) {}
public function replaceChild(DOMNode $node, DOMNode $child) {}
}
-/** @not-serializable */
class DOMNameSpaceNode
{
/** @readonly */
@@ -182,6 +184,12 @@ class DOMNameSpaceNode
/** @readonly */
public ?DOMNode $parentNode;
+
+ /** @implementation-alias DOMNode::__sleep */
+ public function __sleep(): array {}
+
+ /** @implementation-alias DOMNode::__wakeup */
+ public function __wakeup(): void {}
}
class DOMImplementation
diff --git a/ext/dom/php_dom_arginfo.h b/ext/dom/php_dom_arginfo.h
index 1be65cb75d16b..edcc883aba57e 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: 20a0ff883af3bbf073d9c8bc8246646ffafe7818 */
+ * Stub hash: 203760d1cf0e063ffd9abe743a0e24a97985767e */
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)
@@ -28,6 +28,11 @@ ZEND_END_ARG_INFO()
#define arginfo_class_DOMChildNode_replaceWith arginfo_class_DOMParentNode_append
+ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_DOMNode___sleep, 0, 0, IS_ARRAY, 0)
+ZEND_END_ARG_INFO()
+
+#define arginfo_class_DOMNode___wakeup arginfo_class_DOMChildNode_remove
+
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_DOMNode_appendChild, 0, 0, 1)
ZEND_ARG_OBJ_INFO(0, node, DOMNode, 0)
ZEND_END_ARG_INFO()
@@ -100,6 +105,10 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_DOMNode_replaceChild, 0, 0, 2)
ZEND_ARG_OBJ_INFO(0, child, DOMNode, 0)
ZEND_END_ARG_INFO()
+#define arginfo_class_DOMNameSpaceNode___sleep arginfo_class_DOMNode___sleep
+
+#define arginfo_class_DOMNameSpaceNode___wakeup arginfo_class_DOMChildNode_remove
+
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_DOMImplementation_getFeature, 0, 2, IS_NEVER, 0)
ZEND_ARG_TYPE_INFO(0, feature, IS_STRING, 0)
ZEND_ARG_TYPE_INFO(0, version, IS_STRING, 0)
@@ -491,6 +500,8 @@ ZEND_END_ARG_INFO()
ZEND_FUNCTION(dom_import_simplexml);
ZEND_METHOD(DOMCdataSection, __construct);
ZEND_METHOD(DOMComment, __construct);
+ZEND_METHOD(DOMNode, __sleep);
+ZEND_METHOD(DOMNode, __wakeup);
ZEND_METHOD(DOMNode, appendChild);
ZEND_METHOD(DOMNode, C14N);
ZEND_METHOD(DOMNode, C14NFile);
@@ -672,6 +683,8 @@ static const zend_function_entry class_DOMChildNode_methods[] = {
static const zend_function_entry class_DOMNode_methods[] = {
+ ZEND_ME(DOMNode, __sleep, arginfo_class_DOMNode___sleep, ZEND_ACC_PUBLIC)
+ ZEND_ME(DOMNode, __wakeup, arginfo_class_DOMNode___wakeup, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, appendChild, arginfo_class_DOMNode_appendChild, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, C14N, arginfo_class_DOMNode_C14N, ZEND_ACC_PUBLIC)
ZEND_ME(DOMNode, C14NFile, arginfo_class_DOMNode_C14NFile, ZEND_ACC_PUBLIC)
@@ -694,6 +707,8 @@ static const zend_function_entry class_DOMNode_methods[] = {
static const zend_function_entry class_DOMNameSpaceNode_methods[] = {
+ ZEND_MALIAS(DOMNode, __sleep, __sleep, arginfo_class_DOMNameSpaceNode___sleep, ZEND_ACC_PUBLIC)
+ ZEND_MALIAS(DOMNode, __wakeup, __wakeup, arginfo_class_DOMNameSpaceNode___wakeup, ZEND_ACC_PUBLIC)
ZEND_FE_END
};
@@ -989,7 +1004,6 @@ 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;
zval property_nodeName_default_value;
ZVAL_UNDEF(&property_nodeName_default_value);
@@ -1104,7 +1118,6 @@ 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;
zval property_nodeName_default_value;
ZVAL_UNDEF(&property_nodeName_default_value);
diff --git a/ext/dom/tests/gh8996.phpt b/ext/dom/tests/gh8996.phpt
new file mode 100644
index 0000000000000..4175f9acce160
--- /dev/null
+++ b/ext/dom/tests/gh8996.phpt
@@ -0,0 +1,84 @@
+--TEST--
+GH-8996: DOMNode serialization on PHP ^8.1
+--EXTENSIONS--
+dom
+--FILE--
+xmlData = $this->saveXML();
+ return ['xmlData'];
+ }
+
+ public function __wakeup(): void
+ {
+ $this->loadXML($this->xmlData);
+ }
+}
+
+$dom = new SerializableDomDocumentSleepWakeup('1.0', 'UTF-8');
+$dom->loadXML('value');
+
+$serialized = serialize($dom);
+var_dump($serialized);
+$unserialized = unserialize($serialized);
+
+echo "Serialized:\n-----------\n$serialized\n-----------\nRestored:\n-----------\n{$unserialized->saveXml()}";
+
+echo "=== __serialize and __unserialize ===\n";
+
+class SerializableDomDocumentSerializeUnserialize extends DOMDocument
+{
+ public function __serialize(): array
+ {
+ return ['xmlData' => $this->saveXML()];
+ }
+
+ public function __unserialize(array $data): void
+ {
+ $this->loadXML($data['xmlData']);
+ }
+}
+
+$dom = new SerializableDomDocumentSerializeUnserialize('1.0', 'UTF-8');
+$dom->loadXML('value');
+
+$serialized = serialize($dom);
+$unserialized = unserialize($serialized);
+
+echo "Serialized:\n-----------\n$serialized\n-----------\nRestored:\n-----------\n{$unserialized->saveXml()}";
+
+?>
+--EXPECTF--
+=== __sleep and __wakeup ===
+string(144) "O:34:"SerializableDomDocumentSleepWakeup":1:{s:43:"%0SerializableDomDocumentSleepWakeup%0xmlData";s:39:"
+value
+";}"
+Serialized:
+-----------
+O:34:"SerializableDomDocumentSleepWakeup":1:{s:43:"%0SerializableDomDocumentSleepWakeup%0xmlData";s:39:"
+value
+";}
+-----------
+Restored:
+-----------
+
+value
+=== __serialize and __unserialize ===
+Serialized:
+-----------
+O:43:"SerializableDomDocumentSerializeUnserialize":1:{s:7:"xmlData";s:39:"
+value
+";}
+-----------
+Restored:
+-----------
+
+value
diff --git a/ext/dom/tests/not_unserializable.phpt b/ext/dom/tests/not_unserializable.phpt
new file mode 100644
index 0000000000000..30ae64feb5701
--- /dev/null
+++ b/ext/dom/tests/not_unserializable.phpt
@@ -0,0 +1,29 @@
+--TEST--
+DOM classes are not unserializable
+--EXTENSIONS--
+dom
+--FILE--
+getMessage(), "\n";
+ }
+}
+
+?>
+--EXPECT--
+Unserialization of 'DOMXPath' is not allowed
+Unserialization of 'DOMDocument' is not allowed
+Unserialization of 'DOMNode' is not allowed
+Unserialization of 'DOMNameSpaceNode' is not allowed