[php-src] PHP-8.4: [intl] fix leak when iterating IntlBreakIterator parts iterators

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-26T16:13:09-04:00

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

[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

Changed paths:
  A  ext/intl/tests/breakiter_parts_iterator_current_leak.phpt
  M  NEWS
  M  ext/intl/breakiterator/breakiterator_iterators.cpp
  M  ext/intl/common/common_enum.cpp


Diff:

diff --git a/NEWS b/NEWS
index 935b31234898..c9c96543120d 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 6817f52ffb02..be98a1ea3e04 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 58ebeabcb406..79853f7f6ef5 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 000000000000..a006d7f2f792
--- /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)
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.