diff --git a/NEWS b/NEWS index 3db6bc520e84..9690787e59bf 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,8 @@ PHP NEWS the result without a terminating NUL. (iliaal) . Fixed a double-free when IntlGregorianCalendar construction fails after the ICU constructor adopts the TimeZone. (iliaal) + . Fixed bug GH-23094 (NumberFormatter parsing offsets use UTF-16 positions + for UTF-8 strings). (ColumbusLabs) - Phar: . Fixed Phar archives being automatically detected when ".phar" only occurs diff --git a/ext/intl/formatter/formatter_parse.cpp b/ext/intl/formatter/formatter_parse.cpp index 40107229e0ae..7a233f012d46 100644 --- a/ext/intl/formatter/formatter_parse.cpp +++ b/ext/intl/formatter/formatter_parse.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "../intl_convertcpp.h" #include "formatter_class.h" #include "formatter_format.h" @@ -31,6 +32,42 @@ extern "C" { #define ICU_LOCALE_BUG 1 +static bool numfmt_utf8_offset_to_utf16(const char *str, size_t str_len, int32_t *position, UErrorCode *status) +{ + int32_t utf16_position; + + if (*position < 0 || (size_t) *position > str_len) { + return true; + } + + *status = U_ZERO_ERROR; + u_strFromUTF8(nullptr, 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; +} + +static int32_t numfmt_utf16_offset_to_utf8(const icu::UnicodeString &str, int32_t position) +{ + int32_t utf8_position; + UErrorCode status = U_ZERO_ERROR; + + if (position < 0 || position > str.length()) { + return position; + } + + u_strToUTF8(nullptr, 0, &utf8_position, str.getBuffer(), position, &status); + if (status != U_BUFFER_OVERFLOW_ERROR && U_FAILURE(status)) { + return position; + } + + return utf8_position; +} + /* {{{ Parse a number. */ U_CFUNC PHP_FUNCTION( numfmt_parse ) { @@ -65,6 +102,9 @@ U_CFUNC PHP_FUNCTION( numfmt_parse ) icu::UnicodeString ustr; intl_stringFromChar(ustr, 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, &INTL_DATA_ERROR_CODE(nfo))) { + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); + } #if ICU_LOCALE_BUG && defined(LC_NUMERIC) /* need to copy here since setlocale may change it later */ @@ -122,6 +162,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse ) } if (zposition) { + position = numfmt_utf16_offset_to_utf8(ustr, position); ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } @@ -167,6 +208,9 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency ) RETURN_THROWS(); } position = (int32_t) long_position; + if (!numfmt_utf8_offset_to_utf16(str, str_len, &position, &INTL_DATA_ERROR_CODE(nfo))) { + INTL_METHOD_CHECK_STATUS(nfo, "Invalid UTF-8 offset"); + } } icu::ParsePosition pp(position); @@ -178,7 +222,8 @@ U_CFUNC PHP_FUNCTION( numfmt_parse_currency ) } if(zposition) { - ZEND_TRY_ASSIGN_REF_LONG(zposition, pp.getIndex()); + position = numfmt_utf16_offset_to_utf8(ustr, pp.getIndex()); + ZEND_TRY_ASSIGN_REF_LONG(zposition, position); } const double number = currAmt->getNumber().getDouble(INTL_DATA_ERROR_CODE(nfo)); diff --git a/ext/intl/tests/gh23094.phpt b/ext/intl/tests/gh23094.phpt new file mode 100644 index 000000000000..9ace16fa481f --- /dev/null +++ b/ext/intl/tests/gh23094.phpt @@ -0,0 +1,47 @@ +--TEST-- +GH-23094 NumberFormatter parse offsets use UTF-8 byte positions +--EXTENSIONS-- +intl +--FILE-- +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); +var_dump(intl_is_failure($formatter->getErrorCode())); + +$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); + +$offset = 1; +$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-- +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) diff --git a/ext/standard/array.c b/ext/standard/array.c index 648b48479099..acf65c07bd67 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -4037,12 +4037,13 @@ PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src) /* {{{ */ GC_TRY_UNPROTECT_RECURSION(thash); } if (!ret) { + zval_ptr_dtor(&tmp); return 0; } } else { Z_TRY_ADDREF_P(src_zval); zval *zv = zend_hash_next_index_insert(Z_ARRVAL_P(dest_zval), src_zval); - if (EXPECTED(!zv)) { + if (UNEXPECTED(!zv)) { Z_TRY_DELREF_P(src_zval); zend_cannot_add_element(); return 0; diff --git a/ext/standard/tests/array/array_merge_recursive_object_leak.phpt b/ext/standard/tests/array/array_merge_recursive_object_leak.phpt new file mode 100644 index 000000000000..f4313057cf81 --- /dev/null +++ b/ext/standard/tests/array/array_merge_recursive_object_leak.phpt @@ -0,0 +1,55 @@ +--TEST-- +array_merge_recursive() must not leak the array converted from an object when the merge below it fails +--FILE-- + (object) ['k' => 1]]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +/* Control: same failing exit, array source, nothing to release. */ +$control = []; +$control['k'] = &$control; +try { + array_merge_recursive($control, ['k' => ['k' => 1]]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +/* Several nested levels convert an object before the failure unwinds through them. */ +$ring = [[], [], []]; +for ($i = 0; $i < 3; $i++) { + $ring[$i]['k'] = &$ring[($i + 1) % 3]; +} +$src = (object) ['k' => 1]; +for ($i = 1; $i < 3; $i++) { + $src = (object) ['k' => $src]; +} +try { + array_merge_recursive($ring[0], ['k' => $src]); +} catch (\Throwable $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +/* The successful path still releases it exactly once. */ +$ok = ['k' => ['a']]; +var_dump(array_merge_recursive($ok, ['k' => (object) ['b']])); + +?> +--EXPECT-- +Error: Recursion detected +Error: Recursion detected +Error: Recursion detected +array(1) { + ["k"]=> + array(2) { + [0]=> + string(1) "a" + [1]=> + string(1) "b" + } +}