Skip to content
Open
6 changes: 6 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 25 additions & 2 deletions ext/standard/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Expand Down
2 changes: 1 addition & 1 deletion ext/standard/tests/strings/gh18823_strict.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
53 changes: 53 additions & 0 deletions ext/standard/tests/strings/setlocale_null_byte.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
setlocale() rejects locale names with null bytes
--FILE--
<?php
class NullByteStringable {
public function __toString(): string {
return "C\0locale";
}
}

try {
var_dump(setlocale(LC_ALL, "C\0locale"));
} catch (ValueError $e) {
echo $e::class, ": ", $e->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
Loading