From 1fb6af0024848bf0431c755312a537398cb92670 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:14:49 -0400 Subject: [PATCH 1/3] Fixed GH-23094: Use byte offsets in NumberFormatter parsing --- NEWS | 4 +++ ext/intl/formatter/formatter_parse.c | 41 +++++++++++++++++++++++++++- ext/intl/tests/gh23094.phpt | 28 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/gh23094.phpt diff --git a/NEWS b/NEWS index ecb4d105e917..ec6e0f599681 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,10 @@ PHP NEWS . Fixed bug GH-15375 (Nested "yield from" skips items after a valid() or next() call on the inner generator). (iliaal) +- Intl: + . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions + for UTF-8 strings). (ColumbusLabs) + - Opcache: . Fixed opcache.protect_memory race under ZTS. (realFlowControl) diff --git a/ext/intl/formatter/formatter_parse.c b/ext/intl/formatter/formatter_parse.c index 993990065040..6755f2c05e56 100644 --- a/ext/intl/formatter/formatter_parse.c +++ b/ext/intl/formatter/formatter_parse.c @@ -27,6 +27,40 @@ #define ICU_LOCALE_BUG 1 +static int32_t numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t position) +{ + int32_t utf16_position; + UErrorCode status = U_ZERO_ERROR; + + if (position < 0 || (size_t) position > str_len) { + return position; + } + + u_strFromUTF8(NULL, 0, &utf16_position, str, position, &status); + if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + return position; + } + + return utf16_position; +} + +static int32_t numfmt_utf16_offset_to_utf8(const UChar *str, int32_t str_len, int32_t position) +{ + int32_t utf8_position; + UErrorCode status = U_ZERO_ERROR; + + if (position < 0 || position > str_len) { + return position; + } + + u_strToUTF8(NULL, 0, &utf8_position, str, position, &status); + if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + return position; + } + + return utf8_position; +} + /* {{{ Parse a number. */ PHP_FUNCTION( numfmt_parse ) { @@ -61,6 +95,9 @@ PHP_FUNCTION( numfmt_parse ) /* Convert given string to UTF-16. */ intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); + if (zposition) { + position = numfmt_utf8_offset_to_utf16(str, str_len, position); + } #if ICU_LOCALE_BUG && defined(LC_NUMERIC) /* need to copy here since setlocale may change it later */ @@ -101,6 +138,7 @@ PHP_FUNCTION( numfmt_parse ) } if (zposition) { + position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } @@ -149,12 +187,13 @@ PHP_FUNCTION( numfmt_parse_currency ) INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); if(zposition) { - position = (int32_t) zval_get_long(zposition); + position = numfmt_utf8_offset_to_utf16(str, str_len, (int32_t) zval_get_long(zposition)); position_p = &position; } number = unum_parseDoubleCurrency(FORMATTER_OBJECT(nfo), sstr, sstr_len, position_p, currency, &INTL_DATA_ERROR_CODE(nfo)); if(zposition) { + position = numfmt_utf16_offset_to_utf8(sstr, sstr_len, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } if (sstr) { diff --git a/ext/intl/tests/gh23094.phpt b/ext/intl/tests/gh23094.phpt new file mode 100644 index 000000000000..4ec2f0d422f0 --- /dev/null +++ b/ext/intl/tests/gh23094.phpt @@ -0,0 +1,28 @@ +--TEST-- +GH-23094 NumberFormatter parse offsets use UTF-8 byte positions +--EXTENSIONS-- +intl +--FILE-- +parse($prefix . '123', NumberFormatter::TYPE_INT32, $offset)); +var_dump($offset); + +$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); +$offset = strlen($prefix); +$currency = null; +var_dump($formatter->parseCurrency($prefix . '$123.45', $currency, $offset)); +var_dump($currency); +var_dump($offset); + +?> +--EXPECT-- +int(123) +int(7) +float(123.45) +string(3) "USD" +int(11) From 9439b0b8fbb1fb93012743d773818962ce56e2bb Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:46:26 -0400 Subject: [PATCH 2/3] Reject parsing offsets inside UTF-8 sequences --- ext/intl/formatter/formatter_parse.c | 24 +++++++++++++++--------- ext/intl/tests/gh23094.phpt | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/ext/intl/formatter/formatter_parse.c b/ext/intl/formatter/formatter_parse.c index 6755f2c05e56..b0f80be24bb8 100644 --- a/ext/intl/formatter/formatter_parse.c +++ b/ext/intl/formatter/formatter_parse.c @@ -27,21 +27,22 @@ #define ICU_LOCALE_BUG 1 -static int32_t numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t position) +static bool numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t *position) { int32_t utf16_position; UErrorCode status = U_ZERO_ERROR; - if (position < 0 || (size_t) position > str_len) { - return position; + if (*position < 0 || (size_t) *position > str_len) { + return true; } - u_strFromUTF8(NULL, 0, &utf16_position, str, position, &status); + u_strFromUTF8(NULL, 0, &utf16_position, str, *position, &status); if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { - return position; + return false; } - return utf16_position; + *position = utf16_position; + return true; } static int32_t numfmt_utf16_offset_to_utf8(const UChar *str, int32_t str_len, int32_t position) @@ -95,8 +96,9 @@ PHP_FUNCTION( numfmt_parse ) /* Convert given string to UTF-16. */ intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); - if (zposition) { - position = numfmt_utf8_offset_to_utf16(str, str_len, position); + if (zposition && !numfmt_utf8_offset_to_utf16(str, str_len, &position)) { + efree(sstr); + RETURN_FALSE; } #if ICU_LOCALE_BUG && defined(LC_NUMERIC) @@ -187,7 +189,11 @@ PHP_FUNCTION( numfmt_parse_currency ) INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); if(zposition) { - position = numfmt_utf8_offset_to_utf16(str, str_len, (int32_t) zval_get_long(zposition)); + position = (int32_t) zval_get_long(zposition); + if (!numfmt_utf8_offset_to_utf16(str, str_len, &position)) { + efree(sstr); + RETURN_FALSE; + } position_p = &position; } diff --git a/ext/intl/tests/gh23094.phpt b/ext/intl/tests/gh23094.phpt index 4ec2f0d422f0..6cbed2ef0cf5 100644 --- a/ext/intl/tests/gh23094.phpt +++ b/ext/intl/tests/gh23094.phpt @@ -12,6 +12,10 @@ $offset = strlen($prefix); var_dump($formatter->parse($prefix . '123', NumberFormatter::TYPE_INT32, $offset)); var_dump($offset); +$offset = 1; +var_dump($formatter->parse("\u{00E9}123", NumberFormatter::TYPE_INT32, $offset)); +var_dump($offset); + $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $offset = strlen($prefix); $currency = null; @@ -19,10 +23,21 @@ var_dump($formatter->parseCurrency($prefix . '$123.45', $currency, $offset)); var_dump($currency); var_dump($offset); +$offset = 1; +$currency = null; +var_dump($formatter->parseCurrency("\u{00E9}$123.45", $currency, $offset)); +var_dump($currency); +var_dump($offset); + ?> --EXPECT-- int(123) int(7) +bool(false) +int(1) float(123.45) string(3) "USD" int(11) +bool(false) +NULL +int(1) From db2f70375bc7345b30211f31434f3ce776893668 Mon Sep 17 00:00:00 2001 From: ColumbusLabs <287001685+ColumbusLabs@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:21:09 -0400 Subject: [PATCH 3/3] Fix NumberFormatter error state for invalid offsets --- ext/intl/formatter/formatter_parse.c | 17 +++++++++-------- ext/intl/tests/gh23094.phpt | 4 ++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ext/intl/formatter/formatter_parse.c b/ext/intl/formatter/formatter_parse.c index b0f80be24bb8..2c5ac3222f3d 100644 --- a/ext/intl/formatter/formatter_parse.c +++ b/ext/intl/formatter/formatter_parse.c @@ -27,19 +27,20 @@ #define ICU_LOCALE_BUG 1 -static bool numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t *position) +static bool numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t *position, UErrorCode *status) { int32_t utf16_position; - UErrorCode status = U_ZERO_ERROR; if (*position < 0 || (size_t) *position > str_len) { return true; } - u_strFromUTF8(NULL, 0, &utf16_position, str, *position, &status); - if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + *status = U_ZERO_ERROR; + u_strFromUTF8(NULL, 0, &utf16_position, str, *position, status); + if (*status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(*status)) { return false; } + *status = U_ZERO_ERROR; *position = utf16_position; return true; @@ -96,9 +97,9 @@ PHP_FUNCTION( numfmt_parse ) /* Convert given string to UTF-16. */ intl_convert_utf8_to_utf16(&sstr, &sstr_len, str, str_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" ); - if (zposition && !numfmt_utf8_offset_to_utf16(str, str_len, &position)) { + if (zposition && !numfmt_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { efree(sstr); - RETURN_FALSE; + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); } #if ICU_LOCALE_BUG && defined(LC_NUMERIC) @@ -190,9 +191,9 @@ PHP_FUNCTION( numfmt_parse_currency ) if(zposition) { position = (int32_t) zval_get_long(zposition); - if (!numfmt_utf8_offset_to_utf16(str, str_len, &position)) { + if (!numfmt_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { efree(sstr); - RETURN_FALSE; + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); } position_p = &position; } diff --git a/ext/intl/tests/gh23094.phpt b/ext/intl/tests/gh23094.phpt index 6cbed2ef0cf5..9ace16fa481f 100644 --- a/ext/intl/tests/gh23094.phpt +++ b/ext/intl/tests/gh23094.phpt @@ -15,6 +15,7 @@ var_dump($offset); $offset = 1; var_dump($formatter->parse("\u{00E9}123", NumberFormatter::TYPE_INT32, $offset)); var_dump($offset); +var_dump(intl_is_failure($formatter->getErrorCode())); $formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY); $offset = strlen($prefix); @@ -28,6 +29,7 @@ $currency = null; var_dump($formatter->parseCurrency("\u{00E9}$123.45", $currency, $offset)); var_dump($currency); var_dump($offset); +var_dump(intl_is_failure($formatter->getErrorCode())); ?> --EXPECT-- @@ -35,9 +37,11 @@ int(123) int(7) bool(false) int(1) +bool(true) float(123.45) string(3) "USD" int(11) bool(false) NULL int(1) +bool(true)