Commit 4782ec55aae for php.net

commit 4782ec55aae78dd52e9e4af801c2144fdd5bf3e5
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Jul 30 12:47:52 2026 -0400

    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

diff --git a/ext/snmp/snmp.stub.php b/ext/snmp/snmp.stub.php
index 0a303aea77f..0283f7ff762 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 1ee821f0538..07caa70fa9e 100644
Binary files a/ext/snmp/snmp_arginfo.h and b/ext/snmp/snmp_arginfo.h differ
diff --git a/ext/snmp/tests/bug72479.phpt b/ext/snmp/tests/bug72479.phpt
index 8127bbc9455..72c61905fc8 100644
--- a/ext/snmp/tests/bug72479.phpt
+++ b/ext/snmp/tests/bug72479.phpt
@@ -10,28 +10,12 @@
 <?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 00000000000..36b45503593
--- /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 00000000000..33ec0045961
--- /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 add98c505b1..b3f142947fe 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 22336502bfd..cded6095702 100644
Binary files a/ext/tidy/tidy_arginfo.h and b/ext/tidy/tidy_arginfo.h differ
diff --git a/ext/xmlreader/php_xmlreader.stub.php b/ext/xmlreader/php_xmlreader.stub.php
index d3190370660..d2ce48c43f3 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 f0950020c8a..8283a72f7d3 100644
Binary files a/ext/xmlreader/php_xmlreader_arginfo.h and b/ext/xmlreader/php_xmlreader_arginfo.h differ
diff --git a/ext/xmlreader/tests/gh21682.phpt b/ext/xmlreader/tests/gh21682.phpt
new file mode 100644
index 00000000000..95b8dd222d3
--- /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 44b509aa1dd..3d002d5c87c 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 8170077bdab..33e85c3e9ec 100644
Binary files a/ext/xmlwriter/php_xmlwriter_arginfo.h and b/ext/xmlwriter/php_xmlwriter_arginfo.h differ
diff --git a/ext/xmlwriter/tests/gh21682.phpt b/ext/xmlwriter/tests/gh21682.phpt
new file mode 100644
index 00000000000..8437301a7cf
--- /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