[PHP-CVS] [php-src] master: Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, and tidyNode (#21694)

[email protected] (Ilia Alshanetsky via GitHub) Thu, 30 Jul 2026 16:47:56 +0000
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-07-30T12:47:52-04:00

Commit: https://github.com/php/php-src/commit/4782ec55aae78dd52e9e4af801c2144fdd5bf3e5
Raw diff: https://github.com/php/php-src/commit/4782ec55aae78dd52e9e4af801c2144fdd5bf3e5.diff

Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, and tidyNode (#21694)

These classes wrap native C handles (libxml2 writer/reader, SNMP
session, libTidy document/node) that cannot survive serialization.
Unserializing produces a broken object with NULL internal pointers, and
tidyNode segfaults on use.

bug72479.phpt exercised a UAF via unserialize() of SNMP; NOT_SERIALIZABLE
rejects the class outright, closing that vector by construction.

Fixes GH-21682
Closes GH-21694

Changed paths:
  A  ext/snmp/tests/gh21682.phpt
  A  ext/tidy/tests/gh21682.phpt
  A  ext/xmlreader/tests/gh21682.phpt
  A  ext/xmlwriter/tests/gh21682.phpt
  M  ext/snmp/snmp.stub.php
  M  ext/snmp/snmp_arginfo.h
  M  ext/snmp/tests/bug72479.phpt
  M  ext/tidy/tidy.stub.php
  M  ext/tidy/tidy_arginfo.h
  M  ext/xmlreader/php_xmlreader.stub.php
  M  ext/xmlreader/php_xmlreader_arginfo.h
  M  ext/xmlwriter/php_xmlwriter.stub.php
  M  ext/xmlwriter/php_xmlwriter_arginfo.h


Diff:

diff --git a/ext/snmp/snmp.stub.php b/ext/snmp/snmp.stub.php
index 0a303aea77ff..0283f7ff7620 100644
--- a/ext/snmp/snmp.stub.php
+++ b/ext/snmp/snmp.stub.php
@@ -181,6 +181,7 @@ function snmp_get_valueretrieval(): int {}
 
 function snmp_read_mib(string $filename): bool {}
 
+/** @not-serializable */
 class SNMP
 {
     /** @cvalue SNMP_VERSION_1 */
diff --git a/ext/snmp/snmp_arginfo.h b/ext/snmp/snmp_arginfo.h
index 1ee821f0538d..07caa70fa9eb 100644
--- a/ext/snmp/snmp_arginfo.h
+++ b/ext/snmp/snmp_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit snmp.stub.php instead.
- * Stub hash: e2451ac3ea0fa5eb1158e8b7252e61c6794d514f */
+ * Stub hash: 20039fa88cb9f8a861bf3bf3e2e5e291e50c6a12 */
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_snmpget, 0, 3, IS_MIXED, 0)
 	ZEND_ARG_TYPE_INFO(0, hostname, IS_STRING, 0)
@@ -266,7 +266,7 @@ static zend_class_entry *register_class_SNMP(void)
 	zend_class_entry ce, *class_entry;
 
 	INIT_CLASS_ENTRY(ce, "SNMP", class_SNMP_methods);
-	class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE);
 
 	zval const_VERSION_1_value;
 	ZVAL_LONG(&const_VERSION_1_value, SNMP_VERSION_1);
diff --git a/ext/snmp/tests/bug72479.phpt b/ext/snmp/tests/bug72479.phpt
index 8127bbc94559..72c61905fc8f 100644
--- a/ext/snmp/tests/bug72479.phpt
+++ b/ext/snmp/tests/bug72479.phpt
@@ -10,28 +10,12 @@ require_once(__DIR__.'/skipif.inc');
 <?php
 $arr = [1, [1, 2, 3, 4, 5], 3, 4, 5];
 $poc = 'a:3:{i:1;N;i:2;O:4:"snmp":1:{s:11:"quick_print";'.serialize($arr).'}i:1;R:7;}';
-$out = unserialize($poc);
-gc_collect_cycles();
-$fakezval = ptr2str(1122334455);
-$fakezval .= ptr2str(0);
-$fakezval .= "\x00\x00\x00\x00";
-$fakezval .= "\x01";
-$fakezval .= "\x00";
-$fakezval .= "\x00\x00";
-for ($i = 0; $i < 5; $i++) {
-    $v[$i] = $fakezval.$i;
-}
-var_dump($out[1]);
-
-function ptr2str($ptr)
-{
-    $out = '';
-    for ($i = 0; $i < 8; $i++) {
-        $out .= chr($ptr & 0xff);
-        $ptr >>= 8;
-    }
-    return $out;
+try {
+    $out = unserialize($poc);
+    var_dump($out);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
 }
 ?>
 --EXPECT--
-int(1)
+Exception: Unserialization of 'SNMP' is not allowed
diff --git a/ext/snmp/tests/gh21682.phpt b/ext/snmp/tests/gh21682.phpt
new file mode 100644
index 000000000000..36b455035935
--- /dev/null
+++ b/ext/snmp/tests/gh21682.phpt
@@ -0,0 +1,17 @@
+--TEST--
+GH-21682 (SNMP should not be serializable)
+--EXTENSIONS--
+snmp
+--FILE--
+<?php
+$s = new SNMP(SNMP::VERSION_1, "localhost", "public");
+try {
+    serialize($s);
+    echo "ERROR: should have thrown\n";
+} catch (\Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+$s->close();
+?>
+--EXPECT--
+Exception: Serialization of 'SNMP' is not allowed
diff --git a/ext/tidy/tests/gh21682.phpt b/ext/tidy/tests/gh21682.phpt
new file mode 100644
index 000000000000..33ec0045961c
--- /dev/null
+++ b/ext/tidy/tests/gh21682.phpt
@@ -0,0 +1,26 @@
+--TEST--
+GH-21682 (tidy and tidyNode should not be serializable)
+--EXTENSIONS--
+tidy
+--FILE--
+<?php
+$t = new tidy();
+try {
+    serialize($t);
+    echo "ERROR: should have thrown\n";
+} catch (\Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+$t->parseString("<html><body>test</body></html>");
+$node = $t->body();
+try {
+    serialize($node);
+    echo "ERROR: should have thrown\n";
+} catch (\Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+?>
+--EXPECT--
+Exception: Serialization of 'tidy' is not allowed
+Exception: Serialization of 'tidyNode' is not allowed
diff --git a/ext/tidy/tidy.stub.php b/ext/tidy/tidy.stub.php
index add98c505b11..b3f142947fe2 100644
--- a/ext/tidy/tidy.stub.php
+++ b/ext/tidy/tidy.stub.php
@@ -861,6 +861,7 @@ function tidy_get_head(tidy $tidy): ?tidyNode {}
 
 function tidy_get_body(tidy $tidy): ?tidyNode {}
 
+/** @not-serializable */
 class tidy
 {
     public ?string $errorBuffer = null;
@@ -973,6 +974,7 @@ public function html(): ?tidyNode {}
     public function body(): ?tidyNode {}
 }
 
+/** @not-serializable */
 final class tidyNode
 {
     public readonly string $value;
diff --git a/ext/tidy/tidy_arginfo.h b/ext/tidy/tidy_arginfo.h
index 22336502bfd5..cded60957021 100644
--- a/ext/tidy/tidy_arginfo.h
+++ b/ext/tidy/tidy_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit tidy.stub.php instead.
- * Stub hash: 0e6561410a63658f76011c1ddcecdd1e68757f0a */
+ * Stub hash: 7a1ba6bc8ec95e846ec89060b30f54d2c32486ef */
 
 ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_tidy_parse_string, 0, 1, tidy, MAY_BE_FALSE)
 	ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0)
@@ -472,7 +472,7 @@ static zend_class_entry *register_class_tidy(void)
 	zend_class_entry ce, *class_entry;
 
 	INIT_CLASS_ENTRY(ce, "tidy", class_tidy_methods);
-	class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE);
 
 	zval property_errorBuffer_default_value;
 	ZVAL_NULL(&property_errorBuffer_default_value);
@@ -492,7 +492,7 @@ static zend_class_entry *register_class_tidyNode(void)
 	zend_class_entry ce, *class_entry;
 
 	INIT_CLASS_ENTRY(ce, "tidyNode", class_tidyNode_methods);
-	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NOT_SERIALIZABLE);
 
 	zval property_value_default_value;
 	ZVAL_UNDEF(&property_value_default_value);
diff --git a/ext/xmlreader/php_xmlreader.stub.php b/ext/xmlreader/php_xmlreader.stub.php
index d31903706604..d2ce48c43f35 100644
--- a/ext/xmlreader/php_xmlreader.stub.php
+++ b/ext/xmlreader/php_xmlreader.stub.php
@@ -2,6 +2,7 @@
 
 /** @generate-class-entries */
 
+/** @not-serializable */
 class XMLReader
 {
     /* Constants for NodeType - cannot define common types to share with dom as there are differences in these types */
diff --git a/ext/xmlreader/php_xmlreader_arginfo.h b/ext/xmlreader/php_xmlreader_arginfo.h
index f0950020c8a5..8283a72f7d30 100644
--- a/ext/xmlreader/php_xmlreader_arginfo.h
+++ b/ext/xmlreader/php_xmlreader_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit php_xmlreader.stub.php instead.
- * Stub hash: 80288a0f40eabc7802a928963386616ea31e448d */
+ * Stub hash: 11cf6e4c523d9ebbe2775d5bd127be303402336f */
 
 ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_XMLReader_close, 0, 0, IS_TRUE, 0)
 ZEND_END_ARG_INFO()
@@ -176,7 +176,7 @@ static zend_class_entry *register_class_XMLReader(void)
 	zend_class_entry ce, *class_entry;
 
 	INIT_CLASS_ENTRY(ce, "XMLReader", class_XMLReader_methods);
-	class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE);
 
 	zval const_NONE_value;
 	ZVAL_LONG(&const_NONE_value, XML_READER_TYPE_NONE);
diff --git a/ext/xmlreader/tests/gh21682.phpt b/ext/xmlreader/tests/gh21682.phpt
new file mode 100644
index 000000000000..95b8dd222d3b
--- /dev/null
+++ b/ext/xmlreader/tests/gh21682.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-21682 (XMLReader should not be serializable)
+--EXTENSIONS--
+xmlreader
+--FILE--
+<?php
+$r = new XMLReader();
+try {
+    serialize($r);
+    echo "ERROR: should have thrown\n";
+} catch (\Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+?>
+--EXPECT--
+Exception: Serialization of 'XMLReader' is not allowed
diff --git a/ext/xmlwriter/php_xmlwriter.stub.php b/ext/xmlwriter/php_xmlwriter.stub.php
index 44b509aa1dd7..3d002d5c87c1 100644
--- a/ext/xmlwriter/php_xmlwriter.stub.php
+++ b/ext/xmlwriter/php_xmlwriter.stub.php
@@ -86,6 +86,7 @@ function xmlwriter_output_memory(XMLWriter $writer, bool $flush = true): string
 
 function xmlwriter_flush(XMLWriter $writer, bool $empty = true): string|int {}
 
+/** @not-serializable */
 class XMLWriter
 {
     /**
diff --git a/ext/xmlwriter/php_xmlwriter_arginfo.h b/ext/xmlwriter/php_xmlwriter_arginfo.h
index 8170077bdab0..33e85c3e9ec5 100644
--- a/ext/xmlwriter/php_xmlwriter_arginfo.h
+++ b/ext/xmlwriter/php_xmlwriter_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit php_xmlwriter.stub.php instead.
- * Stub hash: fcc388de55bd6d21530d16f6a9ab5f0eb307c1ff */
+ * Stub hash: 27ddfb10eeeda8acfa744751fdcdd030a207f899 */
 
 ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_xmlwriter_open_uri, 0, 1, XMLWriter, MAY_BE_FALSE)
 	ZEND_ARG_TYPE_INFO(0, uri, IS_STRING, 0)
@@ -484,7 +484,7 @@ static zend_class_entry *register_class_XMLWriter(void)
 	zend_class_entry ce, *class_entry;
 
 	INIT_CLASS_ENTRY(ce, "XMLWriter", class_XMLWriter_methods);
-	class_entry = zend_register_internal_class_with_flags(&ce, NULL, 0);
+	class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_NOT_SERIALIZABLE);
 
 	return class_entry;
 }
diff --git a/ext/xmlwriter/tests/gh21682.phpt b/ext/xmlwriter/tests/gh21682.phpt
new file mode 100644
index 000000000000..8437301a7cff
--- /dev/null
+++ b/ext/xmlwriter/tests/gh21682.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-21682 (XMLWriter should not be serializable)
+--EXTENSIONS--
+xmlwriter
+--FILE--
+<?php
+$w = new XMLWriter();
+try {
+    serialize($w);
+    echo "ERROR: should have thrown\n";
+} catch (\Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+?>
+--EXPECT--
+Exception: Serialization of 'XMLWriter' is not allowed