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