[php-src] master: ext/intl: Fix IntlNumberFormatter unbounded allocation on exponent strings
| From: | David Carlier | Date: | Thu, 22 Oct 2026 17:40:49 +0000 |
| Subject: | [php-src] master: ext/intl: Fix IntlNumberFormatter unbounded allocation on exponent strings | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-142599@lists.php.net to get a copy of this message | ||
Author: David Carlier (devnexen)
Date: 2026-10-22T18:40:27+01:00
Commit: https://github.com/php/php-src/commit/d0fc9664b527af937d745569015fa73ebb2df93a
Raw diff: https://github.com/php/php-src/commit/d0fc9664b527af937d745569015fa73ebb2df93a.diff
ext/intl: Fix IntlNumberFormatter 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. The same applies to a
scale/ skeleton option in exponent notation.
Close GH-24443
Changed paths:
A ext/intl/tests/numberformatter/format_string_exponent.phpt
A ext/intl/tests/numberformatter/skeleton_scale_exponent.phpt
M UPGRADING
M ext/intl/numberformatter/numberformatter_class.cpp
M ext/intl/tests/numberformatter/format_string.phpt
Diff:
diff --git a/UPGRADING b/UPGRADING
index adfe422d90ab..39f8d9bbc0ba 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -122,8 +122,11 @@ 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.
+ IntlNumberFormatter::createFromSkeleton() rejects a scale/ option in
+ exponent notation, e.g. "scale/1e3".
It is supported from icu 63.
========================================
diff --git a/ext/intl/numberformatter/numberformatter_class.cpp
b/ext/intl/numberformatter/numberformatter_class.cpp
index a7d68993bec4..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;
@@ -128,6 +139,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<int32_t>(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--
+<?php
+if (!class_exists('IntlNumberFormatter')) {
+ die('skip IntlNumberFormatter not available');
+}
+?>
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+
+$formatter = IntlNumberFormatter::createFromSkeleton('precision-unlimited',
'en_US');
+
+var_dump(strlen($formatter->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
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..09f5da863ad1
--- /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--
+<?php
+if (!class_exists('IntlNumberFormatter')) {
+ die('skip IntlNumberFormatter not available');
+}
+?>
+--INI--
+memory_limit=16M
+--FILE--
+<?php
+
+var_dump(IntlNumberFormatter::createFromSkeleton('precision-unlimited scale/1000',
'en_US')->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(4) "0.5%"
+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