diff --git a/NEWS b/NEWS index 79a15820a902..0556d7857e20 100644 --- a/NEWS +++ b/NEWS @@ -59,6 +59,12 @@ PHP NEWS TCP_USER_TIMEOUT, and SO_LINGER options. (Weilin Du) . Fixed various memory related issues in ext/sockets. (David Carlier) +- Standard: + . Fixed setlocale() to reject locale names containing NUL bytes instead of + silently truncating them, and to reject arrays passed after the $locales + argument or additional arguments passed after an array $locales argument. + (Weilin Du) + - Streams: . Added a new IO copy API used by php_stream_copy_to_stream_ex() that leverages platform primitives (sendfile, splice, copy_file_range, diff --git a/UPGRADING b/UPGRADING index 97670e09246d..1b37cdd95104 100644 --- a/UPGRADING +++ b/UPGRADING @@ -201,6 +201,12 @@ PHP 8.6 UPGRADE NOTES bytes. . parse_str() now raises a ValueError when the $string argument contains NUL bytes. + . setlocale() now raises a ValueError when a locale name contains NUL bytes, + instead of silently truncating it. + Arrays are now accepted only for the $locales argument. Passing an array as + a later variadic locale argument now throws a TypeError. Passing any + additional locale arguments when $locales is an array now throws an + ArgumentCountError. . linkinfo() now raises a ValueError when the $path argument is empty. . pathinfo() now raises a ValueError when an invalid $flag argument value is passed. diff --git a/ext/standard/string.c b/ext/standard/string.c index 823c32a8cf1e..d8a7479d2a98 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -4909,6 +4909,11 @@ static zend_string *try_setlocale_zval(zend_long cat, zval *loc_zv) { if (UNEXPECTED(loc_str == NULL)) { return NULL; } + if (zend_str_has_nul_byte(loc_str)) { + zend_argument_value_error(2, "must not contain any null bytes"); + zend_tmp_string_release(tmp_loc_str); + return NULL; + } zend_string *result = try_setlocale_str(cat, loc_str); zend_tmp_string_release(tmp_loc_str); return result; @@ -4930,8 +4935,26 @@ PHP_FUNCTION(setlocale) zend_string **strings = do_alloca(sizeof(zend_string *) * num_args, use_heap); for (uint32_t i = 0; i < num_args; i++) { - if (UNEXPECTED(Z_TYPE(args[i]) != IS_ARRAY && !zend_parse_arg_str(&args[i], &strings[i], true, i + 2))) { - zend_wrong_parameter_type_error(i + 2, Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, &args[i]); + if (Z_TYPE(args[i]) == IS_ARRAY) { + if (UNEXPECTED(i != 0)) { + zend_wrong_parameter_type_error(i + 2, Z_EXPECTED_STRING_OR_NULL, &args[i]); + goto out; + } + if (UNEXPECTED(num_args > 1)) { + zend_argument_count_error( + "setlocale() expects exactly 2 arguments when argument #2 ($locales) is an array, %d given", + ZEND_NUM_ARGS()); + goto out; + } + break; + } + if (UNEXPECTED(!zend_parse_arg_path_str(&args[i], &strings[i], true, i + 2))) { + zend_wrong_parameter_type_error( + i + 2, + Z_TYPE(args[i]) == IS_STRING + ? Z_EXPECTED_PATH + : (i == 0 ? Z_EXPECTED_ARRAY_OR_STRING_OR_NULL : Z_EXPECTED_STRING_OR_NULL), + &args[i]); goto out; } } diff --git a/ext/standard/tests/strings/gh18823_strict.phpt b/ext/standard/tests/strings/gh18823_strict.phpt index 3735eab00670..39ae11ef61bc 100644 --- a/ext/standard/tests/strings/gh18823_strict.phpt +++ b/ext/standard/tests/strings/gh18823_strict.phpt @@ -16,4 +16,4 @@ try { ?> --EXPECT-- setlocale(): Argument #2 ($locales) must be of type array|string|null, int given -setlocale(): Argument #3 must be of type array|string|null, int given +setlocale(): Argument #3 must be of type ?string, int given diff --git a/ext/standard/tests/strings/setlocale_null_byte.phpt b/ext/standard/tests/strings/setlocale_null_byte.phpt new file mode 100644 index 000000000000..85663b98a71a --- /dev/null +++ b/ext/standard/tests/strings/setlocale_null_byte.phpt @@ -0,0 +1,53 @@ +--TEST-- +setlocale() rejects locale names with null bytes +--FILE-- +getMessage(), \PHP_EOL; +} + +try { + var_dump(setlocale(LC_ALL, ["locale\0name", "C"])); +} catch (ValueError $e) { + echo $e::class, ": ", $e->getMessage(), \PHP_EOL; +} + +try { + var_dump(@setlocale(LC_ALL, [str_repeat("x", 255), "C\0locale"])); +} catch (ValueError $e) { + echo $e::class, ": ", $e->getMessage(), \PHP_EOL; +} + +try { + var_dump(setlocale(LC_ALL, "zz_ZZ.nope", ["C\0locale"])); +} catch (TypeError $e) { + echo $e::class, ": ", $e->getMessage(), \PHP_EOL; +} + +try { + var_dump(setlocale(LC_ALL, "zz_ZZ.nope", new NullByteStringable())); +} catch (ValueError $e) { + echo $e::class, ": ", $e->getMessage(), \PHP_EOL; +} + +try { + var_dump(setlocale(LC_ALL, [], new NullByteStringable())); +} catch (ArgumentCountError $e) { + echo $e::class, ": ", $e->getMessage(), \PHP_EOL; +} +?> +--EXPECT-- +ValueError: setlocale(): Argument #2 ($locales) must not contain any null bytes +ValueError: setlocale(): Argument #2 ($locales) must not contain any null bytes +ValueError: setlocale(): Argument #2 ($locales) must not contain any null bytes +TypeError: setlocale(): Argument #3 must be of type ?string, array given +ValueError: setlocale(): Argument #3 must not contain any null bytes +ArgumentCountError: setlocale() expects exactly 2 arguments when argument #2 ($locales) is an array, 3 given