Commit 947c556ba2b for php

commit 947c556ba2bc7f400d57494ab54f76e8daf69f11
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Tue Oct 27 16:24:23 2026 +0100

    Assert that destroyed hash tables are not in GC buffer (#24739)

diff --git a/Zend/tests/gh24739.phpt b/Zend/tests/gh24739.phpt
new file mode 100644
index 00000000000..a0504f8ac61
--- /dev/null
+++ b/Zend/tests/gh24739.phpt
@@ -0,0 +1,23 @@
+--TEST--
+array_splice() resets type info before destroying hash table
+--ENV--
+a=1
+--FILE--
+<?php
+
+$a = [getenv('a'), getenv('a'), getenv('a')];
+$b = $a;
+unset($b); // add to GC buffer
+
+array_splice($a, 1, 1);
+
+var_dump($a);
+
+?>
+--EXPECT--
+array(2) {
+  [0]=>
+  string(1) "1"
+  [1]=>
+  string(1) "1"
+}
diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c
index dc4c7a6c91d..2c2003038a6 100644
--- a/Zend/zend_hash.c
+++ b/Zend/zend_hash.c
@@ -1962,6 +1962,9 @@ ZEND_API void ZEND_FASTCALL zend_hash_destroy(HashTable *ht)
 {
 	IS_CONSISTENT(ht);
 	HT_ASSERT(ht, GC_REFCOUNT(ht) <= 1);
+	ZEND_ASSERT(((GC_TYPE(ht) == IS_ARRAY && !(GC_TYPE_INFO(ht) & GC_INFO_MASK))
+			|| GC_TYPE(ht) == IS_NULL /* destroyed by GC */)
+			&& "HashTables exposed to userland should be destroyed with zend_array_destroy()");

 	if (ht->nNumUsed) {
 		if (HT_IS_PACKED(ht)) {
diff --git a/ext/standard/array.c b/ext/standard/array.c
index 8cc40a71ab9..50b45a8f304 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -3456,7 +3456,11 @@ static void php_splice(HashTable *in_hash, zend_long offset, zend_long length, H
 		return;
 	}

+	uint32_t type_info = GC_TYPE_INFO(in_hash);
+	GC_TYPE_INFO(in_hash) = GC_ARRAY;
+
 	zend_hash_destroy(in_hash);
+	GC_TYPE_INFO(in_hash) = type_info;

 	HT_FLAGS(in_hash)          = HT_FLAGS(&out_hash);
 	in_hash->nTableSize        = out_hash.nTableSize;
@@ -3740,8 +3744,11 @@ PHP_FUNCTION(array_unshift)

 	/* replace HashTable data */
 	Z_ARRVAL_P(stack)->pDestructor = NULL;
+	uint32_t type_info = GC_TYPE_INFO(Z_ARRVAL_P(stack));
+	GC_TYPE_INFO(Z_ARRVAL_P(stack)) = GC_ARRAY;
 	zend_hash_destroy(Z_ARRVAL_P(stack));

+	GC_TYPE_INFO(Z_ARRVAL_P(stack)) = type_info;
 	HT_FLAGS(Z_ARRVAL_P(stack))          = HT_FLAGS(&new_hash);
 	Z_ARRVAL_P(stack)->nTableSize        = new_hash.nTableSize;
 	Z_ARRVAL_P(stack)->nTableMask        = new_hash.nTableMask;