Skip to content

Fix GH-21682: Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, tidyNode - #21694

Merged
iliaal merged 1 commit into
php:masterfrom
iliaal:fix/gh-21682-ziparchive-not-serializable
Jul 30, 2026
Merged

iliaal merged 1 commit into
php:masterfrom
iliaal:fix/gh-21682-ziparchive-not-serializable

Conversation

@iliaal

@iliaal iliaal commented Apr 9, 2026 •

Copy link
Copy Markdown
Member

Part of #21682

Five classes wrap native C handles but allow serialization. Unserializing them produces objects with NULL internal pointers.

Segfault:

  • tidyNode (ext/tidy): crashes on hasChildren(), dangling TidyNode pointer

Silent data loss:

  • tidy (ext/tidy): body() returns NULL, cleanRepair() no-ops
  • SNMP (ext/snmp): methods run against a dead session without error

Throws on use:

  • XMLWriter (ext/xmlwriter): methods throw "Invalid or uninitialized XMLWriter object"
  • XMLReader (ext/xmlreader): read() throws "Data must be loaded before reading"

Adds @not-serializable to all five so serialize() throws instead of producing broken objects.

The UAF exploit test bug72479.phpt instantiated SNMP via unserialize(). With NOT_SERIALIZABLE, unserialize() rejects the class and closes the UAF vector.

ZipArchive moved to a follow-up PR after review feedback.

@ndossche

ndossche commented Apr 9, 2026

Copy link
Copy Markdown
Member

One thing to keep in mind is that it may introduce issues analogous to #8996
Perhaps the not-serializable flag is not the right way to do it, I was never really convinced that also blocking children is the right thing to do.

@iliaal

iliaal commented Apr 9, 2026 •

Copy link
Copy Markdown
Member Author

Serializing these classes today: tidyNode segfaults on hasChildren(), ZipArchive/tidy silently lose state, XMLWriter/XMLReader throw on every method call. None produce a usable object.

Re child classes and GH-8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state. DOMDocument has saveXML()/loadXML(), which makes SerializableDomDocument viable. These classes have no equivalent. The underlying handles (xmlTextWriterPtr, TidyDoc, libzip archive, SNMP session) are opaque. A child adding __serialize would track its own state and rebuild from scratch on wakeup, not serialize the parent. The GH-8996 pattern doesn't apply here in practice.

Happy to switch to the DOM-style custom handler if you'd prefer that over NOT_SERIALIZABLE, I do think though it introduces complexity for little meaningful value though.

@ndossche ndossche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, see my nitpick remark.

Comment thread ext/zip/tests/bug72434.phpt Outdated
@@ -1,29 +1,17 @@
--TEST--
Bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
--EXTENSIONS--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be moved to ext/zip/tests

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough, moved as asked.

@iliaal
iliaal requested a review from ndossche April 9, 2026 20:50

@DanielEScherzer DanielEScherzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re child classes and #8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state

ZipArchive will have a way to capture this soon with #21497, see https://github.com/tstarling/php-src/blob/de15d91b44fe56d7e02c4ada8e6e7c0ea13ef146/ext/zip/tests/ZipArchive_closeString_basic.phpt, so allowing subclasses to be serialized might be useful - can you split out the ZipArchive changes? In the absence of a maintainer for ext/zip we should at least give more consideration after #21497 before deciding to prevent subclass serialization

nicolas-grekas added a commit to symfony/polyfill that referenced this pull request Apr 10, 2026
…ND_TRIPPABLE

These classes wrap native C handles and silently lose state through
property restoration. Matches php/php-src#21694 which adds
NOT_SERIALIZABLE to them; the explicit list protects older PHP
versions where the flag isn't set yet.
@iliaal

iliaal commented Apr 10, 2026

Copy link
Copy Markdown
Member Author

Re child classes and #8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state

ZipArchive will have a way to capture this soon with #21497, see https://github.com/tstarling/php-src/blob/de15d91b44fe56d7e02c4ada8e6e7c0ea13ef146/ext/zip/tests/ZipArchive_closeString_basic.phpt, so allowing subclasses to be serialized might be useful - can you split out the ZipArchive changes? In the absence of a maintainer for ext/zip we should at least give more consideration after #21497 before deciding to prevent subclass serialization

Granted, closeString() makes subclass serialization workable. My reservation is narrower: the common case is the archive bytes, and openString()/closeString() already round-trip those without a wrapper. A subclass only helps when it carries extra state alongside the archive, which is a narrow niche. That said, I take the point that blocking it now forecloses a path that might matter later.

I'll split the ZipArchive change into its own PR so the other changes can go in. We can revisit Zip once #21497 merges and we see what subclasses actually need.

@iliaal
iliaal force-pushed the fix/gh-21682-ziparchive-not-serializable branch from 602c0b0 to 119e9dd Compare April 10, 2026 10:57
@iliaal
iliaal requested a review from DanielEScherzer April 10, 2026 11:00
@DanielEScherzer
DanielEScherzer dismissed their stale review April 10, 2026 15:31

PR title needs to be updated, but otherwise no remaining objections about ZipArchive

@iliaal iliaal changed the title Fix GH-21682: Add NOT_SERIALIZABLE to ZipArchive, XMLWriter, XMLReader, SNMP, tidy, tidyNode Fix GH-21682: Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, tidyNode Jun 13, 2026
@iliaal

iliaal commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

@DanielEScherzer Updated title, mind giving this a 2nd look plz

@DanielEScherzer

Copy link
Copy Markdown
Member

@DanielEScherzer Updated title, mind giving this a 2nd look plz

No remaining objections from me, but I'm not sure if we w to do this generally, it is kind of a breaking change since it also restricts subclasses. Leaving it up to the individual extension maintainers to approve as usual

@Girgias

Girgias commented Jun 14, 2026

Copy link
Copy Markdown
Member

It might make sense to see if we can make some of those classes final.

@iluuu1994 iluuu1994 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, thanks!

Comment thread ext/snmp/tests/gh21682.phpt Outdated
serialize($s);
echo "ERROR: should have thrown\n";
} catch (\Exception $e) {
echo $e->getMessage() . "\n";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
echo $e->getMessage() . "\n";
echo $e::class, ": ", $e->getMessage(), PHP_EOL;

This style is usually used nowadays to also assert the exception type, particularly since the introduction of the Throwable policy: https://github.com/php/policies/blob/main/coding-standards-and-naming.rst#throwables

Applies to all tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied across all five tests; the expected output now asserts the Exception type too.

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 phpGH-21682
Closes phpGH-21694
@iliaal
iliaal force-pushed the fix/gh-21682-ziparchive-not-serializable branch from 119e9dd to dcb3523 Compare June 30, 2026 12:49
@iliaal
iliaal requested a review from devnexen as a code owner June 30, 2026 12:49
@iliaal
iliaal requested a review from TimWolla June 30, 2026 13:31
@iliaal
iliaal merged commit 4782ec5 into php:master Jul 30, 2026
18 checks passed
nicolas-grekas added a commit to symfony/polyfill that referenced this pull request Aug 7, 2026
…(nicolas-grekas)

This PR was merged into the 1.x branch.

Discussion
----------

Align tests with PHP 8.6 changes to ext/tidy and ext/intl

Fixes the two failures of the `PHPUnit Tests (8.6, apc, apcu, ... intl-73.2 ...)` job, which is red on `1.x` itself, plus an unrelated flaky test that failed in the same run.

### `DeepCloneTest::testTidyNodeRoundTrip`

php-src [`4782ec55aae`](php/php-src@4782ec55aae) (*Add NOT_SERIALIZABLE to XMLWriter, XMLReader, SNMP, tidy, and tidyNode*, php/php-src#21694, master only) marks `tidyNode` ``@not`-serializable` — it wraps a libTidy handle and "segfaults on use" once unserialized.

`ext/deepclone` honours `ZEND_ACC_NOT_SERIALIZABLE`, so on 8.6 both engines now refuse the node: the extension via that flag, the polyfill because `unserialize('O:8:"tidyNode":0:{}')` throws. Same exception, same message — only the test was stale.

Per the design principle that the `deepclone` polyfill mirrors the newest native state rather than each intermediate version, `tidyNode` joins `NOT_ROUND_TRIPPABLE`. The extension is more lenient below 8.6 (`tidyNode` is `final` and bare-instantiable there, so it passes the probe), so the test keeps the round-trip expectation for that combination.

### `NormalizerTest::testNormalizeWithInvalidForm`

php-src [`94e8c54ef27`](php/php-src@94e8c54ef27) (*ext/intl: Fix various error messages*, php/php-src#22828, master only) dropped the doubled article introduced back when intl warnings were promoted to exceptions. Both the polyfill and its test hardcoded `must be a a valid`, so the message is now conditional on `PHP_VERSION_ID >= 80600`.

### `Php73Test::testHardwareTimeAsArrayNanos`

Unrelated, and the reason the `(7.3, apc, apcu, ...)` job failed in the same run: the test asserted `$hrtime2[0] - $hrtime[0] === 0`, which fails whenever the two `hrtime()` calls straddle a whole second. It now measures total elapsed nanoseconds and checks the sub-second component stays in range.

Commits
-------

159e290 Align tests with PHP 8.6 changes to ext/tidy and ext/intl
nicolas-grekas added a commit to symfony/php-ext-deepclone that referenced this pull request Sep 23, 2026
This PR was merged into the main branch.

Discussion
----------

Add support for PHP 8.6

PHP 8.6 is at RC2, so this tests it like the other versions instead of as an informational nightly, on macOS too, and builds its Windows DLLs so that the next release ships them (php-windows-builder takes the official QA builds until 8.6.0 is out).

PHP 8.6 also made `tidyNode` not serializable (php/php-src#21694), which `deepclone_to_array()` honours like for any class: its round-trip test now runs before 8.6 only, and a new one checks the rejection from 8.6 on.

Commits
-------

852cada Add support for PHP 8.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants