Commit cc8abaf9f10 for php.net
commit cc8abaf9f109a83c741b9fe5297f1c094c775811
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Jul 15 16:20:48 2026 -0400
Fix use-after-free serializing an array grown by an element's hook (#22714)
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.
diff --git a/ext/standard/tests/serialize/serialize_array_ref_reentrancy.phpt b/ext/standard/tests/serialize/serialize_array_ref_reentrancy.phpt
new file mode 100644
index 00000000000..490fa4f6c3b
--- /dev/null
+++ b/ext/standard/tests/serialize/serialize_array_ref_reentrancy.phpt
@@ -0,0 +1,21 @@
+--TEST--
+serialize(): a by-reference __serialize() that grows the array being walked must not free it
+--FILE--
+<?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];
+var_dump(serialize($top));
+?>
+--EXPECT--
+string(59) "a:1:{i:0;a:2:{i:0;O:1:"G":1:{s:1:"d";i:1;}i:1;s:4:"tail";}}"
diff --git a/ext/standard/var.c b/ext/standard/var.c
index 3137a270f66..d79b8941634 100644
--- a/ext/standard/var.c
+++ b/ext/standard/var.c
@@ -1303,13 +1303,17 @@ static void php_var_serialize_intern(smart_str *buf, zval *struc, php_serialize_
zend_release_properties(myht);
return;
}
- case IS_ARRAY:
+ case IS_ARRAY: {
smart_str_appendl(buf, "a:", 2);
myht = Z_ARRVAL_P(struc);
+ bool rcn = !is_root && (in_rcn_array || GC_REFCOUNT(myht) > 1);
+ GC_TRY_ADDREF(myht);
php_var_serialize_nested_data(
buf, struc, myht, zend_array_count(myht), /* incomplete_class */ false, var_hash,
- !is_root && (in_rcn_array || GC_REFCOUNT(myht) > 1));
+ rcn);
+ GC_TRY_DTOR_NO_REF(myht);
return;
+ }
case IS_REFERENCE:
struc = Z_REFVAL_P(struc);
goto again;