Commit 08f4b64e0ae for php

commit 08f4b64e0ae69e51ca95b54717dc74405ba5f0f9
Author: David Carlier <devnexen@gmail.com>
Date:   Thu Oct 22 20:24:43 2026 +0100

    Fix GH-24373: serialize() context reused by destructors during teardown

    The context was destroyed while still registered. Unregister it first.

    Close GH-24452

diff --git a/NEWS b/NEWS
index a35e4ce8132..22760f8d7d1 100644
--- a/NEWS
+++ b/NEWS
@@ -133,6 +133,8 @@ PHP                                                                        NEWS
     (Jorg Sowa)
   . Don't share the unserialize() context with enum autoloaders.
     (Nicolas Grekas)
+  . Fixed bug GH-24373 (serialize() called from a destructor during var_hash
+    destruction reuses the outer var_hash). (David Carlier)

 - Streams:
   . Fixed bug GH-24508 (HTTP wrapper leaks the dechunk filter when header
diff --git a/ext/standard/tests/serialize/gh24373.phpt b/ext/standard/tests/serialize/gh24373.phpt
new file mode 100644
index 00000000000..14da98ade0d
--- /dev/null
+++ b/ext/standard/tests/serialize/gh24373.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-24373 (serialize() called from a destructor during var_hash destruction reuses the outer var_hash)
+--CREDITS--
+janhelke
+--FILE--
+<?php
+class D {
+    public function __serialize(): array { return []; }
+    public function __destruct() {
+        $o = new stdClass();
+        $GLOBALS['inner'] = serialize([$o, $o]);
+    }
+}
+class Outer {
+    public function __serialize(): array { return ["d" => new D()]; }
+}
+serialize([new Outer(), new stdClass()]);
+echo $GLOBALS['inner'], PHP_EOL;
+
+class D2 {
+    public function __serialize(): array { return []; }
+    public function __destruct() {
+        $a = [];
+        for ($i = 0; $i < 2000; $i++) { $o = new stdClass(); $a[] = $o; $a[] = $o; }
+        serialize($a);
+    }
+}
+class Outer2 {
+    public function __serialize(): array { return ["d" => new D2()]; }
+}
+$shared = [];
+for ($i = 0; $i < 200; $i++) { $shared[] = new stdClass(); }
+$list = [new Outer2()];
+foreach ($shared as $o) { $list[] = $o; $list[] = $o; }
+serialize($list);
+echo "survived", PHP_EOL;
+?>
+--EXPECT--
+a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}
+survived
diff --git a/ext/standard/tests/serialize/gh24373_unserialize.phpt b/ext/standard/tests/serialize/gh24373_unserialize.phpt
new file mode 100644
index 00000000000..91bd6ee6bca
--- /dev/null
+++ b/ext/standard/tests/serialize/gh24373_unserialize.phpt
@@ -0,0 +1,14 @@
+--TEST--
+GH-24373 (unserialize() called from a destructor during var_hash destruction reuses the outer var_hash)
+--FILE--
+<?php
+class D {
+    public function __destruct() {
+        $GLOBALS['r'] = unserialize('a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}');
+    }
+}
+unserialize('a:2:{i:0;O:1:"D":0:{}i:0;i:1;}');
+var_dump($GLOBALS['r'][0] === $GLOBALS['r'][1]);
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/standard/var.c b/ext/standard/var.c
index a9ef4950af2..8906d914d5d 100644
--- a/ext/standard/var.c
+++ b/ext/standard/var.c
@@ -1425,12 +1425,13 @@ PHPAPI void php_var_serialize_set_incomplete_classes_only(php_serialize_data_t d

 PHPAPI void php_var_serialize_destroy(php_serialize_data_t d) {
 	/* fprintf(stderr, "SERIALIZE_DESTROY   == lock: %u, level: %u\n", BG(serialize_lock), BG(serialize).level); */
-	if (BG(serialize_lock) || BG(serialize).level == 1) {
+	if (BG(serialize_lock)) {
 		zend_hash_destroy(&d->ht);
 		efree(d);
-	}
-	if (!BG(serialize_lock) && !--BG(serialize).level) {
+	} else if (!--BG(serialize).level) {
 		BG(serialize).data = NULL;
+		zend_hash_destroy(&d->ht);
+		efree(d);
 	}
 }

diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re
index 43968dd40c9..e5cc1acd758 100644
--- a/ext/standard/var_unserializer.re
+++ b/ext/standard/var_unserializer.re
@@ -89,12 +89,13 @@ PHPAPI php_unserialize_data_t php_var_unserialize_init(void) {

 PHPAPI void php_var_unserialize_destroy(php_unserialize_data_t d) {
 	/* fprintf(stderr, "UNSERIALIZE_DESTROY == lock: %u, level: %u\n", BG(serialize_lock), BG(unserialize).level); */
-	if (BG(serialize_lock) || BG(unserialize).level == 1) {
+	if (BG(serialize_lock)) {
 		var_destroy(&d);
 		efree(d);
-	}
-	if (!BG(serialize_lock) && !--BG(unserialize).level) {
+	} else if (!--BG(unserialize).level) {
 		BG(unserialize).data = NULL;
+		var_destroy(&d);
+		efree(d);
 	}
 }