[php-src] master: Fix use-after-free serializing an array grown by an element's hook (#22714)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-07-15T16:20:48-04:00

Commit: https://github.com/php/php-src/commit/cc8abaf9f109a83c741b9fe5297f1c094c775811
Raw diff: https://github.com/php/php-src/commit/cc8abaf9f109a83c741b9fe5297f1c094c775811.diff

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.

Changed paths:
  A  ext/standard/tests/serialize/serialize_array_ref_reentrancy.phpt
  M  ext/standard/var.c


Diff:

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 000000000000..490fa4f6c3b0
--- /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 3137a270f661..d79b8941634d 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;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.