Commit a2486445bdb for php

commit a2486445bdb08851aff6c3dd48ab30a97db569de
Author: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com>
Date:   Tue Oct 27 16:28:34 2026 +0100

    Destructor of regular arrays must be ZVAL_PTR_DTOR or NULL (#24727)

diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 4c5d74b49e9..ba5768f0f5d 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -47,6 +47,10 @@ PHP 8.7 INTERNALS UPGRADE NOTES
 - The INIT_CLASS_ENTRY_INIT_METHODS() macro, which was previously used as
   part of handling disabled classes, was removed following the removal of
   the `disable_classes` INI setting in PHP 8.5.
+- Using zend_array_destroy() on HashTables with a custom pDestructor is not
+  allowed anymore. User-facing HashTables must have pDestructor = ZVAL_PTR_DTOR
+  or NULL, and must be freed with zend_array_destroy(). Other HashTables can be
+  freed with zend_hash_destroy() + pefree().

 ========================
 2. Build system changes
diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c
index 2c2003038a6..0b2ed1c8cbd 100644
--- a/Zend/zend_hash.c
+++ b/Zend/zend_hash.c
@@ -2054,6 +2054,9 @@ ZEND_API void ZEND_FASTCALL zend_array_destroy(HashTable *ht)
 	if (ht->nNumUsed) {
 		/* In some rare cases destructors of regular arrays may be changed */
 		if (UNEXPECTED(ht->pDestructor != ZVAL_PTR_DTOR)) {
+			if (UNEXPECTED(ht->pDestructor != NULL)) {
+				abort();
+			}
 			zend_hash_destroy(ht);
 			goto free_ht;
 		}
diff --git a/ext/dom/private_data.c b/ext/dom/private_data.c
index dd624a2c2e9..55d1fb5b43a 100644
--- a/ext/dom/private_data.c
+++ b/ext/dom/private_data.c
@@ -42,6 +42,14 @@ php_dom_libxml_ns_mapper *php_dom_ns_mapper_from_private(php_dom_private_data *p
 	return private_data == NULL ? NULL : &private_data->ns_mapper;
 }

+static void php_dom_libxml_ns_mapper_prefix_map_dtor(zval *zv)
+{
+	HashTable *ht = Z_ARRVAL_P(zv);
+
+	zend_hash_destroy(ht);
+	efree(ht);
+}
+
 php_dom_private_data *php_dom_private_data_create(void)
 {
 	php_dom_private_data *private_data = emalloc(sizeof(*private_data));
@@ -49,7 +57,7 @@ php_dom_private_data *php_dom_private_data_create(void)
 	private_data->header.ns_hook = php_dom_libxml_private_data_ns_hook;
 	private_data->ns_mapper.html_ns = NULL;
 	private_data->ns_mapper.prefixless_xmlns_ns = NULL;
-	zend_hash_init(&private_data->ns_mapper.uri_to_prefix_map, 0, NULL, ZVAL_PTR_DTOR, false);
+	zend_hash_init(&private_data->ns_mapper.uri_to_prefix_map, 0, NULL, php_dom_libxml_ns_mapper_prefix_map_dtor, false);
 	private_data->template_fragments = NULL;
 	return private_data;
 }
diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c
index 263b4561a67..986aac3d819 100644
--- a/ext/mbstring/mbstring.c
+++ b/ext/mbstring/mbstring.c
@@ -3570,8 +3570,7 @@ PHP_FUNCTION(mb_list_encodings)
 		/* Initialize shared array of supported encoding names
 		 * This is done so that we can check if `mb_list_encodings()` is being
 		 * passed to other mbstring functions using a cheap pointer equality check */
-		HashTable *array = emalloc(sizeof(HashTable));
-		zend_hash_init(array, 80, NULL, zval_ptr_dtor_str, false);
+		HashTable *array = zend_new_array(80);
 		for (const mbfl_encoding **encodings = mbfl_get_supported_encodings(); *encodings; encodings++) {
 			zval tmp;
 			ZVAL_STRING(&tmp, (*encodings)->name);