From aa216258a44955268b84754abedb775dc60dfeb9 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 23 Jun 2026 01:51:24 +0800 Subject: [PATCH 1/2] ext/Intl: Fix IntlListFormatter double construction leak (#22394) IntlListFormatter stores a UListFormatter pointer. Calling the constructor again on an already initialized object overwrote the existing pointer and leaked the previous formatter. Follow up to the double construction fixes from GH-22386 by rejecting repeated IntlListFormatter::__construct() calls. Closes #22394 --- NEWS | 2 ++ ext/intl/listformatter/listformatter_class.c | 5 +++++ .../listformatter/listformatter_double_ctor.phpt | 16 ++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 ext/intl/tests/listformatter/listformatter_double_ctor.phpt diff --git a/NEWS b/NEWS index cb2a8846fcba..2d5316b6a2ac 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ PHP NEWS fallback locale when a language tag cannot be canonicalized. (Weilin Du) . Fixed memory leaks when calling Collator::__construct() or Spoofchecker::__construct() twice. (Weilin Du) + . Fixed memory leak when calling IntlListFormatter::__construct() twice. + (Weilin Du) - Reflection: . Fixed bug GH-22324 (Ignore leading namespace separator in diff --git a/ext/intl/listformatter/listformatter_class.c b/ext/intl/listformatter/listformatter_class.c index e4f8b18d7dd6..d8c9c792e036 100644 --- a/ext/intl/listformatter/listformatter_class.c +++ b/ext/intl/listformatter/listformatter_class.c @@ -67,6 +67,11 @@ PHP_METHOD(IntlListFormatter, __construct) Z_PARAM_LONG(width) ZEND_PARSE_PARAMETERS_END(); + if (LISTFORMATTER_OBJECT(obj)) { + zend_throw_error(NULL, "IntlListFormatter object is already constructed"); + RETURN_THROWS(); + } + if(locale_len == 0) { locale = (char *)intl_locale_get_default(); } diff --git a/ext/intl/tests/listformatter/listformatter_double_ctor.phpt b/ext/intl/tests/listformatter/listformatter_double_ctor.phpt new file mode 100644 index 000000000000..f8b0ca1e1633 --- /dev/null +++ b/ext/intl/tests/listformatter/listformatter_double_ctor.phpt @@ -0,0 +1,16 @@ +--TEST-- +IntlListFormatter double construction should not be allowed +--EXTENSIONS-- +intl +--FILE-- +__construct('en_US'); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +IntlListFormatter object is already constructed From e0113cd1d640144715059189ddae874a829f1669 Mon Sep 17 00:00:00 2001 From: Calvin Buckley Date: Mon, 22 Jun 2026 19:07:21 -0300 Subject: [PATCH 2/2] gen_stub: Fix handling of escape sequences in generated C strings (#22273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When handling sequences like this in a stub: ```php expr has all its PHP constants replaced by C constants $prettyPrinter = new Standard; $expr = $prettyPrinter->prettyPrintExpr($this->expr); - // PHP single-quote to C double-quote string if ($this->type->isString()) { - $expr = preg_replace("/(^'|'$)/", '"', $expr); + // The string in $expr has had the octal, hex, and unicode + // backslash sequences already applied for double-quoted strings, + // but not the other sequences. + // + // PHP has single quote strings, C doesn't (they're one char). + // Single-quoted strings need handling to replace their escapes + // with the double-quoted equivalent; namely single quote escapes. + // + // Double-quoted strings have similar escape sequences as C does, + // so we can pass them through directly. However, C does *not* + // support the \$ escape sequence (in ""), so strip that. Variable + // interpolation shouldn't be possible in a stub, so we don't need + // to worry about mangling such a case. + if (preg_match("/(^'|'$)/", $expr)) { + $expr = substr($expr, 1, -1); // strip quotes, readd later + $expr = str_replace("\\'", "'", $expr); + $expr = addcslashes($expr, "\\\""); + $expr = "\"$expr\""; + } else { + $expr = str_replace('\$', "$", $expr); + } } return $expr[0] == '"' ? $expr : preg_replace('(\bnull\b)', 'NULL', str_replace('\\', '', $expr)); } diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php index 489d7d0a260b..a4562368735e 100644 --- a/ext/zend_test/test.stub.php +++ b/ext/zend_test/test.stub.php @@ -57,6 +57,12 @@ class _ZendTestClass implements _ZendTestInterface { public static $_StaticProp; public static int $staticIntProp = 123; + /* If there's a problem with escapes in quotes in generated headers, + * the generated header won't compile. (tests/gh22169.phpt) */ + public static string $doubleQuoteEscaped = "BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END"; + public static string $singleQuoteEscaped = 'BEGIN \n\r\t\v\e\f\\\$\"\101\x41\u{41} END'; + public static string $escapeInterpolated = "begin \$ \\$ end"; + public int $intProp = 123; public ?stdClass $classProp = null; public stdClass|Iterator|null $classUnionProp = null; diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h index 94f75cdb3601..bd6548e7bffa 100644 --- a/ext/zend_test/test_arginfo.h +++ b/ext/zend_test/test_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, IS_NEVER, 0) @@ -795,6 +795,27 @@ static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_e zend_declare_typed_property(class_entry, property_staticIntProp_name, &property_staticIntProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); zend_string_release_ex(property_staticIntProp_name, true); + zval property_doubleQuoteEscaped_default_value; + zend_string *property_doubleQuoteEscaped_default_value_str = zend_string_init("BEGIN \n\r\t\v\x1b\f\\$\"AAA END", strlen("BEGIN \n\r\t\v\x1b\f\\$\"AAA END"), 1); + ZVAL_STR(&property_doubleQuoteEscaped_default_value, property_doubleQuoteEscaped_default_value_str); + zend_string *property_doubleQuoteEscaped_name = zend_string_init("doubleQuoteEscaped", sizeof("doubleQuoteEscaped") - 1, true); + zend_declare_typed_property(class_entry, property_doubleQuoteEscaped_name, &property_doubleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_doubleQuoteEscaped_name, true); + + zval property_singleQuoteEscaped_default_value; + zend_string *property_singleQuoteEscaped_default_value_str = zend_string_init("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END", strlen("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END"), 1); + ZVAL_STR(&property_singleQuoteEscaped_default_value, property_singleQuoteEscaped_default_value_str); + zend_string *property_singleQuoteEscaped_name = zend_string_init("singleQuoteEscaped", sizeof("singleQuoteEscaped") - 1, true); + zend_declare_typed_property(class_entry, property_singleQuoteEscaped_name, &property_singleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_singleQuoteEscaped_name, true); + + zval property_escapeInterpolated_default_value; + zend_string *property_escapeInterpolated_default_value_str = zend_string_init("begin $ \\$ end", strlen("begin $ \\$ end"), 1); + ZVAL_STR(&property_escapeInterpolated_default_value, property_escapeInterpolated_default_value_str); + zend_string *property_escapeInterpolated_name = zend_string_init("escapeInterpolated", sizeof("escapeInterpolated") - 1, true); + zend_declare_typed_property(class_entry, property_escapeInterpolated_name, &property_escapeInterpolated_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_escapeInterpolated_name, true); + zval property_intProp_default_value; ZVAL_LONG(&property_intProp_default_value, 123); zend_string *property_intProp_name = zend_string_init("intProp", sizeof("intProp") - 1, true); diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h index 4a6babbe12b9..2561000f4b60 100644 --- a/ext/zend_test/test_decl.h +++ b/ext/zend_test/test_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 */ + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 */ -#ifndef ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H -#define ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H +#ifndef ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H +#define ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H typedef enum zend_enum_ZendTestUnitEnum { ZEND_ENUM_ZendTestUnitEnum_Foo = 1, @@ -27,4 +27,4 @@ typedef enum zend_enum_ZendTestEnumWithInterface { ZEND_ENUM_ZendTestEnumWithInterface_Bar = 2, } zend_enum_ZendTestEnumWithInterface; -#endif /* ZEND_TEST_DECL_4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9_H */ +#endif /* ZEND_TEST_DECL_8ca2fc33013d5a1c325bf5f0090cc6416a242297_H */ diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h index a4c1ae3f2c96..a254a637e07d 100644 --- a/ext/zend_test/test_legacy_arginfo.h +++ b/ext/zend_test/test_legacy_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit test.stub.php instead. - * Stub hash: 4bb5b467b9d62c0e0c6a7c1e069e8755403a0af9 + * Stub hash: 8ca2fc33013d5a1c325bf5f0090cc6416a242297 * Has decl header: yes */ ZEND_BEGIN_ARG_INFO_EX(arginfo_zend_trigger_bailout, 0, 0, 0) @@ -650,6 +650,27 @@ static zend_class_entry *register_class__ZendTestClass(zend_class_entry *class_e zend_declare_property_ex(class_entry, property_staticIntProp_name, &property_staticIntProp_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); zend_string_release_ex(property_staticIntProp_name, true); + zval property_doubleQuoteEscaped_default_value; + zend_string *property_doubleQuoteEscaped_default_value_str = zend_string_init("BEGIN \n\r\t\v\x1b\f\\$\"AAA END", strlen("BEGIN \n\r\t\v\x1b\f\\$\"AAA END"), 1); + ZVAL_STR(&property_doubleQuoteEscaped_default_value, property_doubleQuoteEscaped_default_value_str); + zend_string *property_doubleQuoteEscaped_name = zend_string_init("doubleQuoteEscaped", sizeof("doubleQuoteEscaped") - 1, true); + zend_declare_property_ex(class_entry, property_doubleQuoteEscaped_name, &property_doubleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_doubleQuoteEscaped_name, true); + + zval property_singleQuoteEscaped_default_value; + zend_string *property_singleQuoteEscaped_default_value_str = zend_string_init("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END", strlen("BEGIN \\n\\r\\t\\v\\e\\f\\\\\\\\$\\\"\\101\\x41\\u{41} END"), 1); + ZVAL_STR(&property_singleQuoteEscaped_default_value, property_singleQuoteEscaped_default_value_str); + zend_string *property_singleQuoteEscaped_name = zend_string_init("singleQuoteEscaped", sizeof("singleQuoteEscaped") - 1, true); + zend_declare_property_ex(class_entry, property_singleQuoteEscaped_name, &property_singleQuoteEscaped_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_singleQuoteEscaped_name, true); + + zval property_escapeInterpolated_default_value; + zend_string *property_escapeInterpolated_default_value_str = zend_string_init("begin $ \\$ end", strlen("begin $ \\$ end"), 1); + ZVAL_STR(&property_escapeInterpolated_default_value, property_escapeInterpolated_default_value_str); + zend_string *property_escapeInterpolated_name = zend_string_init("escapeInterpolated", sizeof("escapeInterpolated") - 1, true); + zend_declare_property_ex(class_entry, property_escapeInterpolated_name, &property_escapeInterpolated_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC, NULL); + zend_string_release_ex(property_escapeInterpolated_name, true); + zval property_intProp_default_value; ZVAL_LONG(&property_intProp_default_value, 123); zend_string *property_intProp_name = zend_string_init("intProp", sizeof("intProp") - 1, true); diff --git a/ext/zend_test/tests/gh22169.phpt b/ext/zend_test/tests/gh22169.phpt new file mode 100644 index 000000000000..5ed2ab22fa21 --- /dev/null +++ b/ext/zend_test/tests/gh22169.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-22169: Ensure escaped strings in stubs are valid +--EXTENSIONS-- +zend_test +--FILE-- + +--EXPECT-- +string(44) "424547494e200a0d090b1b0c5c242241414120454e44" +string(43) "BEGIN \n\r\t\v\e\f\\\\$\"\101\x41\u{41} END" +string(14) "begin $ \$ end" +