[php-src] master: Merge branch 'PHP-8.4' into PHP-8.5

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-08-28T09:15:57-04:00

Commit: https://github.com/php/php-src/commit/3f5baab5a99c37c81f5578e551814346daa4c1f6
Raw diff: https://github.com/php/php-src/commit/3f5baab5a99c37c81f5578e551814346daa4c1f6.diff

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Keep compiled RuleBasedBreakIterator rules alive for the iterator

Changed paths:
  A  ext/intl/tests/rbbiter_compiled_rules_lifetime.phpt
  M  NEWS
  M  ext/intl/breakiterator/breakiterator_class.cpp
  M  ext/intl/breakiterator/breakiterator_class.h
  M  ext/intl/breakiterator/rulebasedbreakiterator_methods.cpp


Diff:

diff --git a/NEWS b/NEWS
index dbce8fde3a0d..a5d2cfe2c037 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,8 @@ PHP                                                                        NEWS
   . Fixed grapheme_str_split() treating UBRK_DONE as a byte index. (iliaal)
   . Fixed a leak in Locale::getKeywords() when a keyword value cannot be
     read. (iliaal)
+  . Fixed a use-after-free when IntlRuleBasedBreakIterator is constructed
+    from compiled rules. (iliaal)
 
 - Opcache:
   . Fixed opcache.protect_memory race under ZTS. (realFlowControl)
diff --git a/ext/intl/breakiterator/breakiterator_class.cpp b/ext/intl/breakiterator/breakiterator_class.cpp
index 4976d4ff675b..e078ff691274 100644
--- a/ext/intl/breakiterator/breakiterator_class.cpp
+++ b/ext/intl/breakiterator/breakiterator_class.cpp
@@ -109,6 +109,9 @@ static zend_object *BreakIterator_clone_obj(zend_object *object)
 		} else {
 			bio_new->biter = new_biter;
 			ZVAL_COPY(&bio_new->text, &bio_orig->text);
+			if (bio_orig->compiled_rules) {
+				bio_new->compiled_rules = zend_string_copy(bio_orig->compiled_rules);
+			}
 		}
 	} else {
 		zend_throw_error(NULL, "Cannot clone uninitialized BreakIterator");
@@ -163,6 +166,7 @@ static void breakiterator_object_init(BreakIterator_object *bio)
 {
 	intl_error_init(BREAKITER_ERROR_P(bio));
 	bio->biter = NULL;
+	bio->compiled_rules = NULL;
 	ZVAL_UNDEF(&bio->text);
 }
 /* }}} */
@@ -177,6 +181,10 @@ static void BreakIterator_objects_free(zend_object *object)
 		delete bio->biter;
 		bio->biter = NULL;
 	}
+	if (bio->compiled_rules) {
+		zend_string_release(bio->compiled_rules);
+		bio->compiled_rules = NULL;
+	}
 	intl_error_reset(BREAKITER_ERROR_P(bio));
 
 	zend_object_std_dtor(&bio->zo);
diff --git a/ext/intl/breakiterator/breakiterator_class.h b/ext/intl/breakiterator/breakiterator_class.h
index 0852d86e2a82..8061acc0ddac 100644
--- a/ext/intl/breakiterator/breakiterator_class.h
+++ b/ext/intl/breakiterator/breakiterator_class.h
@@ -38,6 +38,8 @@ typedef struct {
 	// current text
 	zval text;
 
+	zend_string *compiled_rules;
+
 	zend_object	zo;
 } BreakIterator_object;
 
diff --git a/ext/intl/breakiterator/rulebasedbreakiterator_methods.cpp b/ext/intl/breakiterator/rulebasedbreakiterator_methods.cpp
index a7c322b1d816..2d17e93e46da 100644
--- a/ext/intl/breakiterator/rulebasedbreakiterator_methods.cpp
+++ b/ext/intl/breakiterator/rulebasedbreakiterator_methods.cpp
@@ -87,6 +87,9 @@ U_CFUNC PHP_METHOD(IntlRuleBasedBreakIterator, __construct)
 	}
 
 	breakiterator_object_create(object, rbbi, false);
+	if (compiled) {
+		Z_INTL_BREAKITERATOR_P(object)->compiled_rules = zend_string_copy(rules);
+	}
 }
 
 U_CFUNC PHP_METHOD(IntlRuleBasedBreakIterator, getRules)
diff --git a/ext/intl/tests/rbbiter_compiled_rules_lifetime.phpt b/ext/intl/tests/rbbiter_compiled_rules_lifetime.phpt
new file mode 100644
index 000000000000..d0eb5fc2e755
--- /dev/null
+++ b/ext/intl/tests/rbbiter_compiled_rules_lifetime.phpt
@@ -0,0 +1,77 @@
+--TEST--
+IntlRuleBasedBreakIterator compiled rules outlive the source string
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (version_compare(INTL_ICU_VERSION, '68.1') < 0) die('skip for ICU >= 68.1'); ?>
+--FILE--
+<?php
+
+$rules = <<<RULES
+\$LN = [[:letter:] [:number:]];
+\$S = [.;,:];
+
+!!forward;
+\$LN+ {1};
+\$S+ {42};
+!!reverse;
+\$LN+ {1};
+\$S+ {42};
+!!safe_forward;
+!!safe_reverse;
+RULES;
+
+$src = new IntlRuleBasedBreakIterator($rules);
+$len = strlen($src->getBinaryRules());
+
+$it = new IntlRuleBasedBreakIterator($src->getBinaryRules(), true);
+unset($src);
+
+/* ICU aliases the buffer it was built from, so the freed rules have to be
+   reclaimed and overwritten for the iterator below to read stale bytes. */
+$ballast = [];
+for ($i = 0; $i < 16; $i++) {
+    $ballast[] = str_repeat("\xCC", $len);
+}
+
+$it->setText('ab,cd');
+echo $it->first(), "\n";
+while (true) {
+    $n = $it->next();
+    if ($n === IntlBreakIterator::DONE) {
+        break;
+    }
+    echo $n, "\n";
+}
+
+$clone = clone $it;
+unset($it);
+$ballast[] = str_repeat("\xDD", $len);
+$clone->setText('xy');
+echo $clone->first(), "\n";
+echo $clone->next(), "\n";
+
+$src = new IntlRuleBasedBreakIterator($rules);
+$it = new IntlRuleBasedBreakIterator($src->getBinaryRules(), true);
+unset($src);
+for ($i = 0; $i < 16; $i++) {
+    $ballast[] = str_repeat("\xEE", $len);
+}
+$it->setText('ab,cd');
+$parts = $it->getPartsIterator();
+unset($it);
+foreach ($parts as $p) {
+    echo $p, "\n";
+}
+
+?>
+--EXPECT--
+0
+2
+3
+5
+0
+2
+ab
+,
+cd
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.