Commit 7d6523170ea for php.net

commit 7d6523170eaf5e0dcc8296eb8fe175232cd11ffb
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Mon Aug 24 12:48:52 2026 -0400

    [intl] fix leak when iterating IntlBreakIterator parts iterators

    getPartsIterator() leaked because wrapping_obj was a counted
    self-reference, so the iterator never reached destruction and the
    current element was retained. wrapping_obj stays UNDEF; current and
    the backing BreakIterator are released from the iterator dtor, not
    the IntlIterator object dtor, so iterating a temporary parts
    iterator does not dangle. The string enumeration iterator still
    self-references because move_forward/rewind need the owner.

    Closes GH-23464

diff --git a/NEWS b/NEWS
index 935b3123489..c9c96543120 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-19320 (FPM UID and GID overflow). (Pratik Bhujel)

 - Intl:
+  . Fixed a memory leak when iterating IntlBreakIterator::getPartsIterator()
+    results. (iliaal)
   . Fixed a double-free when IntlGregorianCalendar construction fails after
     the ICU constructor adopts the TimeZone. (iliaal)
   . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions
diff --git a/ext/intl/breakiterator/breakiterator_iterators.cpp b/ext/intl/breakiterator/breakiterator_iterators.cpp
index 6817f52ffb0..be98a1ea3e0 100644
--- a/ext/intl/breakiterator/breakiterator_iterators.cpp
+++ b/ext/intl/breakiterator/breakiterator_iterators.cpp
@@ -242,7 +242,7 @@ void IntlIterator_from_BreakIterator_parts(zval *break_iter_zv,
 	ii->iterator->index = 0;

 	((zoi_with_current*)ii->iterator)->destroy_it = _breakiterator_parts_destroy_it;
-	ZVAL_OBJ_COPY(&((zoi_with_current*)ii->iterator)->wrapping_obj, Z_OBJ_P(object));
+	ZVAL_UNDEF(&((zoi_with_current*)ii->iterator)->wrapping_obj);
 	ZVAL_UNDEF(&((zoi_with_current*)ii->iterator)->current);

 	((zoi_break_iter_parts*)ii->iterator)->bio = Z_INTL_BREAKITERATOR_P(break_iter_zv);
diff --git a/ext/intl/common/common_enum.cpp b/ext/intl/common/common_enum.cpp
index 58ebeabcb40..79853f7f6ef 100644
--- a/ext/intl/common/common_enum.cpp
+++ b/ext/intl/common/common_enum.cpp
@@ -35,6 +35,8 @@ zend_object_handlers IntlIterator_handlers;
 void zoi_with_current_dtor(zend_object_iterator *iter)
 {
 	zoi_with_current *zoiwc = (zoi_with_current*)iter;
+	iter->funcs->invalidate_current(iter);
+	zoiwc->destroy_it(iter);
 	zval_ptr_dtor(&zoiwc->wrapping_obj);
 	ZVAL_UNDEF(&zoiwc->wrapping_obj);
 }
@@ -147,7 +149,6 @@ static void IntlIterator_objects_dtor(zend_object *object)
 {
 	IntlIterator_object	*ii = php_intl_iterator_fetch_object(object);
 	if (ii->iterator) {
-		((zoi_with_current*)ii->iterator)->destroy_it(ii->iterator);
 		OBJ_RELEASE(&ii->iterator->std);
 		ii->iterator = NULL;
 	}
diff --git a/ext/intl/tests/breakiter_parts_iterator_current_leak.phpt b/ext/intl/tests/breakiter_parts_iterator_current_leak.phpt
new file mode 100644
index 00000000000..a006d7f2f79
--- /dev/null
+++ b/ext/intl/tests/breakiter_parts_iterator_current_leak.phpt
@@ -0,0 +1,31 @@
+--TEST--
+IntlPartsIterator must not leak, and a temporary one must not dangle
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+function parts(): IntlPartsIterator {
+    $bi = IntlBreakIterator::createWordInstance('en');
+    $bi->setText('hello world');
+    return $bi->getPartsIterator();
+}
+
+foreach (parts() as $part) {
+    echo "[$part]\n";
+}
+
+$bi = IntlBreakIterator::createWordInstance('en');
+$bi->setText('hello world foo bar baz');
+$m0 = memory_get_usage();
+for ($i = 0; $i < 20000; $i++) {
+    foreach ($bi->getPartsIterator() as $v) {
+        break;
+    }
+}
+var_dump(memory_get_usage() - $m0 < 1024 * 1024);
+?>
+--EXPECT--
+[hello]
+[ ]
+[world]
+bool(true)