From 8a98c37d4fe8105a5479a5b899c2e46f20d76302 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 19 Oct 2026 18:32:36 +0100 Subject: [PATCH 1/3] ext/intl: IntlNumberFormatter::format() unbounded allocation on exponent strings. A numeric string such as "1e999999999" made ICU expand every digit outside ZendMM, ignoring memory_limit. Strings in exponent notation are now rejected, so the output stays proportional to the input length. --- UPGRADING | 5 ++- .../numberformatter/numberformatter_class.cpp | 4 ++ .../tests/numberformatter/format_string.phpt | 6 +-- .../format_string_exponent.phpt | 42 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 ext/intl/tests/numberformatter/format_string_exponent.phpt diff --git a/UPGRADING b/UPGRADING index fbc8ddce7878..f59d75d5b448 100644 --- a/UPGRADING +++ b/UPGRADING @@ -98,8 +98,9 @@ PHP 8.7 UPGRADE NOTES . Added IntlNumberFormatter class to format a number with a given locale and ICU number skeleton, which also covers measurement units, e.g. "measure-unit/speed-kilometer-per-hour unit-width-full-name". - IntlNumberFormatter::format() accepts an int, a float or a numeric - string, the latter being formatted as a decimal of arbitrary precision. + IntlNumberFormatter::format() accepts an int, a float or a decimal + string without exponent, the latter being formatted with arbitrary + precision. It is supported from icu 63. ======================================== diff --git a/ext/intl/numberformatter/numberformatter_class.cpp b/ext/intl/numberformatter/numberformatter_class.cpp index a7d68993bec4..1a8b8ee085ee 100644 --- a/ext/intl/numberformatter/numberformatter_class.cpp +++ b/ext/intl/numberformatter/numberformatter_class.cpp @@ -128,6 +128,10 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, format) zend_argument_value_error(1, "is too long"); RETURN_THROWS(); } + if (UNEXPECTED(strpbrk(Z_STRVAL_P(value), "eE"))) { + intl_errors_set(INTL_DATA_ERROR_P(nfo), U_DECIMAL_NUMBER_SYNTAX_ERROR, "Number formatting failed"); + RETURN_FALSE; + } result = NUMBERFORMATTER_OBJECT(nfo)->formatDecimal(icu::StringPiece(Z_STRVAL_P(value), static_cast(Z_STRLEN_P(value))), status).toString(status); } else if (Z_TYPE_P(value) == IS_DOUBLE) { result = NUMBERFORMATTER_OBJECT(nfo)->formatDouble(Z_DVAL_P(value), status).toString(status); diff --git a/ext/intl/tests/numberformatter/format_string.phpt b/ext/intl/tests/numberformatter/format_string.phpt index 51ac283c0962..0a345304d0d5 100644 --- a/ext/intl/tests/numberformatter/format_string.phpt +++ b/ext/intl/tests/numberformatter/format_string.phpt @@ -16,12 +16,11 @@ $formatter = IntlNumberFormatter::createFromSkeleton('precision-unlimited', 'en_ var_dump($formatter->format('12345678901234567890.123456789')); var_dump($formatter->format('9223372036854775808')); var_dump($formatter->format('-0.1')); -var_dump($formatter->format('1e3')); var_dump(IntlNumberFormatter::createFromSkeleton('', 'en_US')->format('12345678901234567890.123456789')); var_dump(IntlNumberFormatter::createFromSkeleton('measure-unit/length-meter .00', 'de')->format('1234.5')); -foreach (['abc', '', ' 12', '1,5'] as $value) { +foreach (['abc', '', ' 12', '1,5', '1e3'] as $value) { var_dump($formatter->format($value)); var_dump($formatter->getErrorMessage()); } @@ -40,7 +39,6 @@ try { string(36) "12,345,678,901,234,567,890.123456789" string(25) "9,223,372,036,854,775,808" string(4) "-0.1" -string(5) "1,000" string(33) "12,345,678,901,234,567,890.123457" string(10) "1.234,50 m" bool(false) @@ -51,6 +49,8 @@ bool(false) string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" bool(false) string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" string(2) "12" int(0) ValueError: IntlNumberFormatter::format(): Argument #1 ($value) must not contain any null bytes diff --git a/ext/intl/tests/numberformatter/format_string_exponent.phpt b/ext/intl/tests/numberformatter/format_string_exponent.phpt new file mode 100644 index 000000000000..b490edb7954c --- /dev/null +++ b/ext/intl/tests/numberformatter/format_string_exponent.phpt @@ -0,0 +1,42 @@ +--TEST-- +IntlNumberFormatter::format() rejects numeric strings in exponent notation +--EXTENSIONS-- +intl +--SKIPIF-- + +--INI-- +memory_limit=16M +--FILE-- +format('1' . str_repeat('0', 999)))); + +foreach (['1e3', '1E-3', '1e999999999'] as $value) { + var_dump($formatter->format($value)); + var_dump($formatter->getErrorMessage()); +} + +ini_set('intl.use_exceptions', '1'); + +try { + $formatter->format('1e999999999'); +} catch (IntlException $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +int(1333) +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +bool(false) +string(86) "IntlNumberFormatter::format(): Number formatting failed: U_DECIMAL_NUMBER_SYNTAX_ERROR" +IntlException: IntlNumberFormatter::format(): Number formatting failed From cf6c4100178edf2fc16c6d853a4edfe556235f0e Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 22 Oct 2026 18:04:44 +0100 Subject: [PATCH 2/3] scale/ fix --- .../numberformatter/numberformatter_class.cpp | 11 +++++ .../skeleton_scale_exponent.phpt | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt diff --git a/ext/intl/numberformatter/numberformatter_class.cpp b/ext/intl/numberformatter/numberformatter_class.cpp index 1a8b8ee085ee..71427da58aeb 100644 --- a/ext/intl/numberformatter/numberformatter_class.cpp +++ b/ext/intl/numberformatter/numberformatter_class.cpp @@ -92,6 +92,17 @@ U_CFUNC PHP_METHOD(IntlNumberFormatter, createFromSkeleton) RETURN_THROWS(); } + const char *scale = zend_memnstr(skeleton, "scale/", sizeof("scale/") - 1, skeleton + skeleton_len); + + if (scale) { + scale += sizeof("scale/") - 1; + scale += strspn(scale, "0123456789.+-"); + if (UNEXPECTED(*scale == 'e' || *scale == 'E')) { + intl_error_set(nullptr, U_NUMBER_SKELETON_SYNTAX_ERROR, "Number formatter creation failed"); + RETURN_NULL(); + } + } + UErrorCode status = U_ZERO_ERROR; UnicodeString uskeleton; diff --git a/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt b/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt new file mode 100644 index 000000000000..bc3bbf3fb842 --- /dev/null +++ b/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt @@ -0,0 +1,42 @@ +--TEST-- +IntlNumberFormatter::createFromSkeleton() rejects a scale option in exponent notation +--EXTENSIONS-- +intl +--SKIPIF-- + +--INI-- +memory_limit=16M +--FILE-- +format(1)); +var_dump(IntlNumberFormatter::createFromSkeleton('scale/0.5 percent', 'en_US')->format(1)); + +foreach (['precision-unlimited scale/1e3000000', 'precision-unlimited scale/1E-3000000', 'scale/1.5e3'] as $skeleton) { + var_dump(IntlNumberFormatter::createFromSkeleton($skeleton, 'en_US')); + var_dump(intl_get_error_message()); +} + +ini_set('intl.use_exceptions', '1'); + +try { + IntlNumberFormatter::createFromSkeleton('precision-unlimited scale/1e3000000', 'en_US'); +} catch (IntlException $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +string(5) "1,000" +string(3) "50%" +NULL +string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" +NULL +string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" +NULL +string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" +IntlException: IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed From d8aeaae4387ec187979190085161db51de4e2977 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 22 Oct 2026 18:25:48 +0100 Subject: [PATCH 3/3] fix test --- ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt b/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt index bc3bbf3fb842..09f5da863ad1 100644 --- a/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt +++ b/ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt @@ -32,7 +32,7 @@ try { ?> --EXPECT-- string(5) "1,000" -string(3) "50%" +string(4) "0.5%" NULL string(107) "IntlNumberFormatter::createFromSkeleton(): Number formatter creation failed: U_NUMBER_SKELETON_SYNTAX_ERROR" NULL