deepclone_to_array: resolve INDIRECT slots in retained __serialize() states#23
Merged
Merged
Conversation
…states Before PHP 8.3, Random\Randomizer::__serialize() returns the object's raw property table with only the table itself addref'd; its "engine" entry is an IS_INDIRECT slot pointing into the object (fixed for 8.3 by php-src commit c5fa7696e64, never backported to the by-then security-only 8.2). deepclone_to_array() walks such a state with dc_copy_array(), which passed IS_INDIRECT slots to dc_copy_value() unresolved, so the payload retained pointers into the source object. Once a Randomizer that did not outlive its payload was released, deepclone_from_array() dereferenced freed memory and crashed. Resolve indirects (and skip resolved UNDEF slots) at the start of the walk, exactly like the native serializer and the existing build_scoped_props loop already do.
9e9f2ee to
b4cb516
Compare
nicolas-grekas
added a commit
to symfony/polyfill
that referenced
this pull request
Jun 10, 2026
… instances on PHP 8.2 (nicolas-grekas) This PR was merged into the 1.x branch. Discussion ---------- [DeepClone] Fix round-tripping short-lived Random\Randomizer instances on PHP 8.2 | Q | A | ------------- | --- | Branch? | 1.x | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT This is what has been segfaulting the PHP 8.2 CI jobs since the Randomizer round-trip tests landed in #629 (and what #630 inherits). Minimal reproducer, no polyfill involved, on PHP 8.2.31: ```php $state = (new Random\Randomizer(new Random\Engine\Mt19937(7)))->__serialize(); serialize($state); // SIGSEGV ``` Before PHP 8.3, `Random\Randomizer::__serialize()` returns the object's raw property table with only the table itself addref'd. Its `engine` entry is an `IS_INDIRECT` slot pointing into the object, so once the Randomizer is released the retained state dangles. php-src fixed this for 8.3 by duplicating the table (php/php-src@c5fa7696e64), but 8.2 was already security-only and the fix was never backported; 8.2.31 still crashes. `deepclone_to_array()` retains exactly such a state in its payload, so round-tripping a Randomizer that does not outlive the payload segfaulted on 8.2. There is no copy-based userland fix: plain array reads copy `IS_INDIRECT` slots verbatim instead of resolving them (`gettype()` reports `unknown type`), which is also how the contamination is detected. The serializer is the one userland-reachable code path that resolves these slots, so when a `__serialize()` result is contaminated (checked while the source object is still alive, and only on PHP < 8.3), it is passed through a `serialize()`/`unserialize()` round trip to materialize real values. No new tests: the #629 Randomizer tests are the regression tests; they segfault on PHP 8.2 before this change and pass after (suite verified green on 8.1, 8.2, 8.3 and 8.5). The nested engine becomes a fresh copy rather than a shared instance on the affected versions, matching the already-documented identity behavior of nested `__serialize()` states. Companion extension fix (the with-extensions 8.2 CI legs run the tagged ext via PIE and stay red until it ships in a release): symfony/php-ext-deepclone#23 Perf note: `Random\Randomizer` is special-cased instead of scanned for, after auditing PHP 8.2 core for the pattern: of the internal classes whose `__serialize()` retains the un-duplicated property table (Randomizer, `HashContext`, the Random engines), only Randomizer declares a property, and only declared properties create `IS_INDIRECT` slots (GMP uses the legacy serialize handler, consumed while the object is alive; the date classes already duplicate). Measured on 8.2: no overhead for any other class; Randomizer itself pays one serialize round trip (~190 us for a Mt19937 state), the price of not crashing. On PHP 8.3+ the version guard makes the branch dead code. The trade-off versus scanning every internal state: a hypothetical third-party extension class combining the same pattern with declared properties would keep crashing, as it does today. Companion extension fix (the with-extensions 8.2 CI legs run the tagged ext via PIE, fixed in v0.7.2): symfony/php-ext-deepclone#23 Commits ------- ae1778e [DeepClone] Fix round-tripping short-lived Random\Randomizer instances on PHP 8.2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before PHP 8.3,
Random\Randomizer::__serialize()returns the object's raw property table with only the table itself addref'd; itsengineentry is anIS_INDIRECTslot pointing into the object. php-src fixed it for 8.3 by duplicating the table (php/php-src@c5fa7696e64), but the fix was never backported to the by-then security-only 8.2, which still ships the dangling behavior in its final release.deepclone_to_array()walks such a state withdc_copy_array(), which passedIS_INDIRECTslots todc_copy_value()unresolved, so the payload retained pointers into the source object. Once a Randomizer that did not outlive its payload was released,deepclone_from_array()dereferenced freed memory and crashed:This is what segfaults the symfony/polyfill 1.x CI on the PHP 8.2 jobs that install this extension via PIE (the polyfill-only jobs are fixed by the companion PR below); the released v0.7.1 is affected, so those CI legs stay red until this ships in a tagged release.
The fix resolves indirects (and skips resolved UNDEF slots) at the start of the
dc_copy_array()walk, exactly like the native serializer and the extension's existingbuild_scoped_propsloop already do. Verified against a stock PHP 8.2 build: the newdeepclone_randomizer.phptsegfaults before the fix and passes after; the full suite is green on 8.2 and 8.6, and the polyfill test suite passes against the patched extension.Companion polyfill fix: symfony/polyfill#632