Commit 154881aa4ec for php.net
commit 154881aa4ecaded7bee07b07debf108da061e8a3
Author: Weilin Du <weilindu@php.net>
Date: Sat Jul 4 02:03:08 2026 +0800
ext/intl: Fix NumberFormatter parse offset overflow (#22572)
Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to
reject offset values outside the 32-bit range instead of silently
truncating them.
diff --git a/NEWS b/NEWS
index 3ea51b89f23..61a49cc1750 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,9 @@ PHP NEWS
warning when an IFD is not followed by a next-IFD offset). (Eyüp Can Akman)
- Intl:
+ . Fixed NumberFormatter::parse() and NumberFormatter::parseCurrency() to
+ reject offset values outside the 32-bit range instead of silently
+ truncating them. (Weilin Du)
. IntlDateFormatter::parse()/datefmt_parse() and
IntlDateFormatter::localtime()/datefmt_localtime() now raise TypeError
when the offset argument is not of type int. (Weilin Du)
diff --git a/ext/intl/formatter/formatter_parse.cpp b/ext/intl/formatter/formatter_parse.cpp
index a475960809b..63c0fba4329 100644
--- a/ext/intl/formatter/formatter_parse.cpp
+++ b/ext/intl/formatter/formatter_parse.cpp
@@ -50,7 +50,12 @@ U_CFUNC PHP_FUNCTION( numfmt_parse )
}
if (zposition) {
- position = (int32_t) zval_get_long(zposition);
+ zend_long long_position = zval_get_long(zposition);
+ if (long_position < INT32_MIN || long_position > INT32_MAX) {
+ zend_argument_value_error(hasThis() ? 3 : 4, "must be between %d and %d", INT32_MIN, INT32_MAX);
+ RETURN_THROWS();
+ }
+ position = (int32_t) long_position;
}
/* Fetch the object. */
@@ -155,8 +160,13 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency )
intl_stringFromChar(ustr, str, str_len, &INTL_DATA_ERROR_CODE(nfo));
INTL_METHOD_CHECK_STATUS( nfo, "String conversion to UTF-16 failed" );
- if(zposition) {
- position = (int32_t) zval_get_long(zposition);
+ if (zposition) {
+ zend_long long_position = zval_get_long(zposition);
+ if (long_position < INT32_MIN || long_position > INT32_MAX) {
+ zend_argument_value_error(hasThis() ? 3 : 4, "must be between %d and %d", INT32_MIN, INT32_MAX);
+ RETURN_THROWS();
+ }
+ position = (int32_t) long_position;
}
icu::ParsePosition pp(position);
diff --git a/ext/intl/tests/formatter_parse_offset_overflow.phpt b/ext/intl/tests/formatter_parse_offset_overflow.phpt
new file mode 100644
index 00000000000..9421c889751
--- /dev/null
+++ b/ext/intl/tests/formatter_parse_offset_overflow.phpt
@@ -0,0 +1,46 @@
+--TEST--
+NumberFormatter parse offset overflow
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (PHP_INT_SIZE != 8) die("skip: 64-bit only"); ?>
+--FILE--
+<?php
+$fmt = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+$currencyFmt = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
+
+function print_error(callable $callback): void {
+ try {
+ $callback();
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+}
+
+$offset = PHP_INT_MAX;
+print_error(function () use ($fmt, &$offset) {
+ $fmt->parse('123', NumberFormatter::TYPE_DOUBLE, $offset);
+});
+
+$offset = PHP_INT_MAX;
+print_error(function () use ($fmt, &$offset) {
+ numfmt_parse($fmt, '123', NumberFormatter::TYPE_DOUBLE, $offset);
+});
+
+$currency = '';
+$offset = PHP_INT_MAX;
+print_error(function () use ($currencyFmt, &$currency, &$offset) {
+ $currencyFmt->parseCurrency('$123.00', $currency, $offset);
+});
+
+$currency = '';
+$offset = PHP_INT_MAX;
+print_error(function () use ($currencyFmt, &$currency, &$offset) {
+ numfmt_parse_currency($currencyFmt, '$123.00', $currency, $offset);
+});
+?>
+--EXPECT--
+ValueError: NumberFormatter::parse(): Argument #3 ($offset) must be between -2147483648 and 2147483647
+ValueError: numfmt_parse(): Argument #4 ($offset) must be between -2147483648 and 2147483647
+ValueError: NumberFormatter::parseCurrency(): Argument #3 ($offset) must be between -2147483648 and 2147483647
+ValueError: numfmt_parse_currency(): Argument #4 ($offset) must be between -2147483648 and 2147483647