[DeepClone] Fix round-tripping short-lived Random\Randomizer instances on PHP 8.2#632
Merged
Merged
Conversation
58dffc4 to
91a9867
Compare
…s on PHP 8.2 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 when the Randomizer is released before the returned state, the slot dangles: array reads copy it verbatim (gettype() reports "unknown type") and serializing the retained state crashes the engine. php-src fixed this for 8.3 by duplicating the table (c5fa7696e64), but 8.2 was already security-only and still ships the dangling behavior. deepclone_to_array() retains exactly such a state in its payload, so round-tripping a Randomizer that does not outlive the payload segfaulted on PHP 8.2, which is how the test suite has been crashing the 8.2 CI jobs. Pass the state through a serialize() round trip while the source object is still alive: the serializer is the one userland-reachable code path that resolves IS_INDIRECT slots. Randomizer is the only affected class: of the internal classes whose __serialize() retains the un-duplicated property table (Randomizer, HashContext, the Random engines), it is the only one declaring a property, and only declared properties create IS_INDIRECT slots. Everything else therefore pays nothing, and the version guard makes the branch dead code on PHP 8.3 and up.
91a9867 to
ae1778e
Compare
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.
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:
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, 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 copyIS_INDIRECTslots verbatim instead of resolving them (gettype()reportsunknown 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 aserialize()/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\Randomizeris 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 createIS_INDIRECTslots (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