Commit 1fd0c64beba for php.net
commit 1fd0c64beba81ba552ad2969c24068d432d859c6
Author: Weilin Du <weilindu@php.net>
Date: Tue Jul 28 00:03:52 2026 +0800
ext/standard: Fix `setlocale()` NUL byte truncation (#22843)
This reject NUL byte in the setlocale() function in the standard extension.
diff --git a/NEWS b/NEWS
index cc2eed87ccf..7ec4eb4b809 100644
--- a/NEWS
+++ b/NEWS
@@ -65,6 +65,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 34817c10974..d30a5663274 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 823c32a8cf1..d8a7479d2a9 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 3735eab0067..39ae11ef61b 100644
--- a/ext/standard/tests/strings/gh18823_strict.phpt
+++ b/ext/standard/tests/strings/gh18823_strict.phpt
@@ -16,4 +16,4 @@
?>
--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 00000000000..85663b98a71
--- /dev/null
+++ b/ext/standard/tests/strings/setlocale_null_byte.phpt
@@ -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