Commit 02f71b68132 for php.net

commit 02f71b68132cde1f97343e8fa5210699659e89d2
Author: Weilin Du <weilindu@php.net>
Date:   Mon Jun 22 13:19:09 2026 +0800

    ext/intl: Fix double construction leaks (#22386)

    Calling Collator::__construct() or Spoofchecker::__construct() on an already
    constructed object replaces the stored ICU handle, which leaves the previous
    handle unreachable and prevents it from being released during object
    destruction.

    Reject repeated construction with an Error for both classes so the existing ICU
    handle remains owned by the object. Add PHPT coverage for the double
    construction path.

    Closes #22386

diff --git a/NEWS b/NEWS
index 1b6809def6c..f880b5ca33c 100644
--- a/NEWS
+++ b/NEWS
@@ -54,6 +54,8 @@ PHP                                                                        NEWS
     and later. (Graham Campbell)
   . Fixed Locale::lookup() and locale_lookup() to return NULL instead of the
     fallback locale when a language tag cannot be canonicalized. (Weilin Du)
+  . Fixed memory leaks when calling Collator::__construct() or
+    Spoofchecker::__construct() twice. (Weilin Du)

 - mysqli:
   . Fix stmt->query leak in mysqli_execute_query() validation errors.
diff --git a/ext/intl/collator/collator_create.c b/ext/intl/collator/collator_create.c
index 88dacc1c1db..ca57d5431e0 100644
--- a/ext/intl/collator/collator_create.c
+++ b/ext/intl/collator/collator_create.c
@@ -42,6 +42,10 @@ static int collator_ctor(INTERNAL_FUNCTION_PARAMETERS, zend_error_handling *erro

 	INTL_CHECK_LOCALE_LEN_OR_FAILURE(locale_len);
 	COLLATOR_METHOD_FETCH_OBJECT;
+	if (co->ucoll) {
+		zend_throw_error(NULL, "Collator object is already constructed");
+		return FAILURE;
+	}

 	if(locale_len == 0) {
 		locale = (char *)intl_locale_get_default();
diff --git a/ext/intl/spoofchecker/spoofchecker_create.c b/ext/intl/spoofchecker/spoofchecker_create.c
index c1cecac8412..4614d44c317 100644
--- a/ext/intl/spoofchecker/spoofchecker_create.c
+++ b/ext/intl/spoofchecker/spoofchecker_create.c
@@ -31,9 +31,13 @@ PHP_METHOD(Spoofchecker, __construct)

 	ZEND_PARSE_PARAMETERS_NONE();

-	zend_replace_error_handling(EH_THROW, IntlException_ce_ptr, &error_handling);
-
 	SPOOFCHECKER_METHOD_FETCH_OBJECT_NO_CHECK;
+	if (co->uspoof) {
+		zend_throw_error(NULL, "Spoofchecker object is already constructed");
+		RETURN_THROWS();
+	}
+
+	zend_replace_error_handling(EH_THROW, IntlException_ce_ptr, &error_handling);

 	co->uspoof = uspoof_open(SPOOFCHECKER_ERROR_CODE_P(co));
 	INTL_METHOD_CHECK_STATUS(co, "spoofchecker: unable to open ICU Spoof Checker");
diff --git a/ext/intl/tests/collator_double_ctor.phpt b/ext/intl/tests/collator_double_ctor.phpt
new file mode 100644
index 00000000000..93b72f7392b
--- /dev/null
+++ b/ext/intl/tests/collator_double_ctor.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Collator double construction should not be allowed
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+$collator = new Collator('en_US');
+
+try {
+    $collator->__construct('en_US');
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Collator object is already constructed
diff --git a/ext/intl/tests/spoofchecker_double_ctor.phpt b/ext/intl/tests/spoofchecker_double_ctor.phpt
new file mode 100644
index 00000000000..01dae5ab4bc
--- /dev/null
+++ b/ext/intl/tests/spoofchecker_double_ctor.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Spoofchecker double construction should not be allowed
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (!class_exists("Spoofchecker")) print "skip"; ?>
+--FILE--
+<?php
+$checker = new Spoofchecker();
+
+try {
+    $checker->__construct();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Spoofchecker object is already constructed