Skip to content

Fix use-after-free serializing an array grown by an element's hook#22714

Merged
iliaal merged 1 commit into
php:masterfrom
iliaal:fix-serialize-array-ref-uaf
Jul 15, 2026
Merged

Fix use-after-free serializing an array grown by an element's hook#22714
iliaal merged 1 commit into
php:masterfrom
iliaal:fix-serialize-array-ref-uaf

Conversation

@iliaal

@iliaal iliaal commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

The IS_ARRAY case of php_var_serialize_intern() walks the array's HashTable without holding a reference while php_var_serialize_nested_data() recurses into user hooks, so a __serialize() that grows the same array through a by-reference alias reallocs the backing store mid-walk and the iterator reads freed memory. The object case, var_dump, and var_export already hold a ref across the walk; this adds the same GC_ADDREF/GC_DTOR_NO_REF for arrays, so the append separates a copy instead of reallocating in place. __sleep() and Serializable::serialize() reach the same walk, so one test covers all three.

Reproducer (invalid read in php_var_serialize_nested_data under ASan/valgrind; a normal build usually reads the freed memory and passes, so the test is an ASan canary):

<?php
class G {
    public $ref;
    public function __serialize(): array {
        for ($i = 0; $i < 128; $i++) $this->ref[] = 'x' . $i;
        return ['d' => 1];
    }
}
$g = new G();
$inner = [$g, 'tail'];
$g->ref = &$inner;
$top = [&$inner];
serialize($top);

Comment thread ext/standard/var.c
Comment thread ext/standard/var.c Outdated
smart_str_appendl(buf, "a:", 2);
myht = Z_ARRVAL_P(struc);
bool rcn = !is_root && (in_rcn_array || GC_REFCOUNT(myht) > 1);
if (!(GC_FLAGS(myht) & GC_IMMUTABLE)) {

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.

GC_TRY_ADDREF

Comment thread ext/standard/var.c Outdated
buf, struc, myht, zend_array_count(myht), /* incomplete_class */ 0, var_hash,
!is_root && (in_rcn_array || GC_REFCOUNT(myht) > 1));
rcn);
if (!(GC_FLAGS(myht) & GC_IMMUTABLE)) {

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.

GC_TRY_DTOR_NO_REF

@iliaal iliaal force-pushed the fix-serialize-array-ref-uaf branch from f2cd4b0 to 8ae20f7 Compare July 15, 2026 10:58
@iliaal iliaal requested review from LamentXU123 and ndossche July 15, 2026 11:00
@ndossche

Copy link
Copy Markdown
Member

I would only merge this in master though

@iliaal

iliaal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

I would only merge this in master though

Any reason for master only? Change seem fairly safe to me and does address UAF.

@ndossche

Copy link
Copy Markdown
Member

I would only merge this in master though

Any reason for master only? Change seem fairly safe to me and does address UAF.

Generally, for maliciously-crafted code we only do these changes on master to avoid breakage.
For me personally, I'm worried about any refcount interactions in the serialization code. Since the serializer relies on refcounts in some places, we had some breakage of such code in the past.
If the code sits in master for a while and does not break anything, we can consider backporting it.

@iliaal

iliaal commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

I would only merge this in master though

Any reason for master only? Change seem fairly safe to me and does address UAF.

Generally, for maliciously-crafted code we only do these changes on master to avoid breakage. For me personally, I'm worried about any refcount interactions in the serialization code. Since the serializer relies on refcounts in some places, we had some breakage of such code in the past. If the code sits in master for a while and does not break anything, we can consider backporting it.

Fair enough I'll update the PR to master

The IS_ARRAY case of php_var_serialize_intern() walked the array's
HashTable without holding a reference across
php_var_serialize_nested_data(), which recurses into user hooks
(__serialize, __sleep, Serializable::serialize). A hook that grows the
same array through a by-reference alias reallocs the backing store
mid-walk, so the iterator reads freed memory. Hold a ref across the walk,
as the object path and var_dump/var_export already do, so the append
separates a copy instead of reallocating in place.
@iliaal iliaal force-pushed the fix-serialize-array-ref-uaf branch from 8ae20f7 to 01107d8 Compare July 15, 2026 18:48
@iliaal iliaal changed the base branch from PHP-8.4 to master July 15, 2026 18:48
@iliaal iliaal merged commit cc8abaf into php:master Jul 15, 2026
23 of 24 checks passed
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.

3 participants