[php-src] master: ext/intl: IntlNumberRangeFormatter::format() crash when the formatting fails.
David Carlier <[email protected]>
| Newsgroups | gmane.comp.php.cvs.general |
|---|---|
| Message-ID | <[email protected]> |
Author: David Carlier (devnexen)
Date: 2026-08-10T16:29:00+01:00
Commit: https://github.com/php/php-src/commit/2f08d44e867b567fafe79b4da3a692d64263f003
Raw diff: https://github.com/php/php-src/commit/2f08d44e867b567fafe79b4da3a692d64263f003.diff
ext/intl: IntlNumberRangeFormatter::format() crash when the formatting fails.
When formatFormattableRange() failed, the error was set (which throws, as
exceptions are force-enabled there) but execution fell through to
intl_charFromString(), which returns NULL for the bogus result, and the NULL
zend_string ended up in return_value as an IS_STRING zval. The engine then
dereferenced it while discarding the return value.
The conversion is now only attempted for a successful formatting and both
failure paths return early.
createFromSkeleton() had the same shape of defect without the crash: the
skeleton failure path threw and then still built a
LocalizedNumberRangeFormatter out of the failed skeleton, so it bails out
early as well.
Additionally, format() reset the global error slot and the object one
separately, and both methods reset before parsing their parameters, so a
TypeError also cleared the state. Use intl_errors_reset(), which covers both
slots, and reset once the parameters are known to be good.
Close GH-23198
Changed paths:
A ext/intl/tests/rangeformatter/rangeformatter_error_reset_scope.phpt
A ext/intl/tests/rangeformatter/rangeformatter_format_failure.phpt
M NEWS
M ext/intl/rangeformatter/rangeformatter_class.cpp
Diff:
diff --git a/NEWS b/NEWS
index 1fa28e590846..2d6cde54b325 100644
--- a/NEWS
+++ b/NEWS
@@ -56,6 +56,8 @@ PHP NEWS
. Added SpoofChecker::areBidiConfusable(). (David Carlier)
. Added SpoofChecker::getBidiSkeleton(). (Weilin Du)
. Added SpoofChecker::getSkeleton(). (David Carlier)
+ . Fixed IntlNumberRangeFormatter::format() crash when the formatting fails.
+ (David Carlier)
- PDO:
. Fixed pdo_raise_impl_error() emitting a warning under ERRMODE_SILENT.
diff --git a/ext/intl/rangeformatter/rangeformatter_class.cpp b/ext/intl/rangeformatter/rangeformatter_class.cpp
index 95acfccd2452..37b49e4f1310 100644
--- a/ext/intl/rangeformatter/rangeformatter_class.cpp
+++ b/ext/intl/rangeformatter/rangeformatter_class.cpp
@@ -88,8 +88,6 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, createFromSkeleton)
zend_long collapse;
zend_long identityFallback;
- intl_error_reset(NULL);
-
ZEND_PARSE_PARAMETERS_START(4,4)
Z_PARAM_STRING(skeleton, skeleton_len)
Z_PARAM_STRING(locale, locale_len)
@@ -97,6 +95,8 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, createFromSkeleton)
Z_PARAM_LONG(identityFallback)
ZEND_PARSE_PARAMETERS_END();
+ intl_error_reset(NULL);
+
if (locale_len == 0) {
locale = (char *)intl_locale_get_default();
}
@@ -138,6 +138,8 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, createFromSkeleton)
INTL_G(use_exceptions) = old_use_exception;
INTL_G(error_level) = old_error_level;
+
+ RETURN_THROWS();
}
LocalizedNumberRangeFormatter* nrf = new LocalizedNumberRangeFormatter(
@@ -160,16 +162,17 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, format)
zval *start;
zval *end;
- intl_error_reset(NULL);
-
IntlNumberRangeFormatter_object* obj = Z_INTL_RANGEFORMATTER_P(ZEND_THIS);
- intl_error_reset(RANGEFORMATTER_ERROR_P(obj));
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_NUMBER(start)
Z_PARAM_NUMBER(end)
ZEND_PARSE_PARAMETERS_END();
+ intl_errors_reset(RANGEFORMATTER_ERROR_P(obj));
+
+ ZEND_ASSERT(RANGEFORMATTER_OBJECT(obj) != NULL);
+
UErrorCode error = U_ZERO_ERROR;
icu::Formattable start_formattable = rangeformatter_create_formattable(start);
@@ -183,19 +186,29 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, format)
INTL_G(use_exceptions) = true;
INTL_G(error_level) = 0;
+ zend_string *ret = NULL;
+
if (U_FAILURE(error)) {
intl_errors_set(RANGEFORMATTER_ERROR_P(obj), error, "Failed to format number range");
- }
+ } else {
+ ret = intl_charFromString(result, &error);
- zend_string *ret = intl_charFromString(result, &error);
+ if (UNEXPECTED(ret == NULL)) {
+ if (U_SUCCESS(error)) {
+ error = U_ILLEGAL_ARGUMENT_ERROR;
+ }
- if (U_FAILURE(error)) {
- intl_errors_set(RANGEFORMATTER_ERROR_P(obj), error, "Failed to convert result to UTF-8");
+ intl_errors_set(RANGEFORMATTER_ERROR_P(obj), error, "Failed to convert result to UTF-8");
+ }
}
INTL_G(use_exceptions) = old_use_exception;
INTL_G(error_level) = old_error_level;
+ if (UNEXPECTED(ret == NULL)) {
+ RETURN_THROWS();
+ }
+
RETVAL_NEW_STR(ret);
}
diff --git a/ext/intl/tests/rangeformatter/rangeformatter_error_reset_scope.phpt b/ext/intl/tests/rangeformatter/rangeformatter_error_reset_scope.phpt
new file mode 100644
index 000000000000..7fcd37a00c69
--- /dev/null
+++ b/ext/intl/tests/rangeformatter/rangeformatter_error_reset_scope.phpt
@@ -0,0 +1,64 @@
+--TEST--
+IntlNumberRangeFormatter keeps the intl error state on a parameter error
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '63.0') < 0) {
+ die('skip for ICU < 63.0');
+}
+?>
+--FILE--
+<?php
+
+$formatter = IntlNumberRangeFormatter::createFromSkeleton(
+ '',
+ 'en_US',
+ IntlNumberRangeFormatter::COLLAPSE_AUTO,
+ IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE
+);
+
+try {
+ IntlNumberRangeFormatter::createFromSkeleton(
+ 'invalid skeleton here',
+ 'en_US',
+ IntlNumberRangeFormatter::COLLAPSE_AUTO,
+ IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE
+ );
+} catch (IntlException $exception) {
+}
+
+try {
+ $formatter->format([], 2);
+} catch (TypeError $error) {
+ echo $error::class, ': ', $error->getMessage(), PHP_EOL;
+}
+
+var_dump(intl_get_error_code() !== 0);
+
+try {
+ IntlNumberRangeFormatter::createFromSkeleton(
+ [],
+ 'en_US',
+ IntlNumberRangeFormatter::COLLAPSE_AUTO,
+ IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE
+ );
+} catch (TypeError $error) {
+ echo $error::class, ': ', $error->getMessage(), PHP_EOL;
+}
+
+var_dump(intl_get_error_code() !== 0);
+
+$formatter->format(1, 2);
+
+var_dump(intl_get_error_code());
+var_dump($formatter->getErrorCode());
+
+?>
+--EXPECT--
+TypeError: IntlNumberRangeFormatter::format(): Argument #1 ($start) must be of type int|float, array given
+bool(true)
+TypeError: IntlNumberRangeFormatter::createFromSkeleton(): Argument #1 ($skeleton) must be of type string, array given
+bool(true)
+int(0)
+int(0)
diff --git a/ext/intl/tests/rangeformatter/rangeformatter_format_failure.phpt b/ext/intl/tests/rangeformatter/rangeformatter_format_failure.phpt
new file mode 100644
index 000000000000..854d0f74b995
--- /dev/null
+++ b/ext/intl/tests/rangeformatter/rangeformatter_format_failure.phpt
@@ -0,0 +1,54 @@
+--TEST--
+IntlNumberRangeFormatter::format() with a failing formatter
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '63.0') < 0) {
+ die('skip for ICU < 63.0');
+}
+?>
+--FILE--
+<?php
+
+$formatter = IntlNumberRangeFormatter::createFromSkeleton(
+ '',
+ 'en_US@numbers=foobar',
+ IntlNumberRangeFormatter::COLLAPSE_AUTO,
+ IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE
+);
+
+try {
+ $formatter->format(1, 2);
+} catch (IntlException $exception) {
+ echo $exception::class, ': ', $exception->getMessage(), PHP_EOL;
+}
+
+var_dump($formatter->getErrorCode() !== 0);
+var_dump(str_starts_with(
+ $formatter->getErrorMessage(),
+ 'IntlNumberRangeFormatter::format(): Failed to format number range: '
+));
+
+var_dump(intl_get_error_code() !== 0);
+
+$formatter = IntlNumberRangeFormatter::createFromSkeleton(
+ '',
+ 'en_US',
+ IntlNumberRangeFormatter::COLLAPSE_AUTO,
+ IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE
+);
+
+var_dump($formatter->format(1, 2) !== '');
+var_dump($formatter->getErrorCode());
+var_dump($formatter->getErrorMessage());
+
+?>
+--EXPECT--
+IntlException: IntlNumberRangeFormatter::format(): Failed to format number range
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(0)
+string(12) "U_ZERO_ERROR"