From 314fbf148b0059ac7744eec3e2f516f97d02dc9a Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 21 Jul 2026 17:36:31 +0800 Subject: [PATCH 1/5] ext/standard: Fix `setlocale()` NUL byte truncation --- ext/standard/string.c | 16 ++++++---- .../tests/strings/setlocale_null_byte.phpt | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 ext/standard/tests/strings/setlocale_null_byte.phpt diff --git a/ext/standard/string.c b/ext/standard/string.c index 823c32a8cf1e..e4a86f166895 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -4837,12 +4837,16 @@ PHP_FUNCTION(strip_tags) } /* }}} */ -static zend_string *try_setlocale_str(zend_long cat, zend_string *loc) { +static zend_string *try_setlocale_str(zend_long cat, zend_string *loc, uint32_t arg_num) { const char *retval; if (zend_string_equals_literal(loc, "0")) { loc = NULL; } else { + if (zend_str_has_nul_byte(loc)) { + zend_argument_value_error(arg_num, "must not contain any null bytes"); + return NULL; + } if (ZSTR_LEN(loc) >= 255) { php_error_docref(NULL, E_WARNING, "Specified locale name is too long"); return NULL; @@ -4903,13 +4907,13 @@ static zend_string *try_setlocale_str(zend_long cat, zend_string *loc) { return zend_string_init(retval, strlen(retval), 0); } -static zend_string *try_setlocale_zval(zend_long cat, zval *loc_zv) { +static zend_string *try_setlocale_zval(zend_long cat, zval *loc_zv, uint32_t arg_num) { zend_string *tmp_loc_str; zend_string *loc_str = zval_try_get_tmp_string(loc_zv, &tmp_loc_str); if (UNEXPECTED(loc_str == NULL)) { return NULL; } - zend_string *result = try_setlocale_str(cat, loc_str); + zend_string *result = try_setlocale_str(cat, loc_str, arg_num); zend_tmp_string_release(tmp_loc_str); return result; } @@ -4941,7 +4945,7 @@ PHP_FUNCTION(setlocale) if (Z_TYPE(args[i]) == IS_ARRAY) { zval *elem; ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[i]), elem) { - result = try_setlocale_zval(cat, elem); + result = try_setlocale_zval(cat, elem, i + 2); if (EG(exception)) { goto out; } @@ -4952,9 +4956,9 @@ PHP_FUNCTION(setlocale) } ZEND_HASH_FOREACH_END(); continue; } else if (Z_ISNULL(args[i])) { - result = try_setlocale_str(cat, ZSTR_EMPTY_ALLOC()); + result = try_setlocale_str(cat, ZSTR_EMPTY_ALLOC(), i + 2); } else { - result = try_setlocale_str(cat, strings[i]); + result = try_setlocale_str(cat, strings[i], i + 2); } if (EG(exception)) { goto out; 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..d55f711b02ac --- /dev/null +++ b/ext/standard/tests/strings/setlocale_null_byte.phpt @@ -0,0 +1,32 @@ +--TEST-- +setlocale() rejects locale names with null bytes +--FILE-- +getMessage(), "\n"; +} + +try { + var_dump(setlocale(LC_ALL, ["invalid\0locale", "C"])); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +try { + var_dump(setlocale(LC_ALL, [], new NullByteStringable())); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +setlocale(): Argument #2 ($locales) must not contain any null bytes +setlocale(): Argument #2 ($locales) must not contain any null bytes +setlocale(): Argument #3 must not contain any null bytes From 74a3051c598fbf57989e29f76c68c692b9d1453a Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 21 Jul 2026 17:45:55 +0800 Subject: [PATCH 2/5] NEWS and UPGRADING --- NEWS | 4 ++++ UPGRADING | 2 ++ 2 files changed, 6 insertions(+) diff --git a/NEWS b/NEWS index fd5b2d859ee0..ade0b853f4da 100644 --- a/NEWS +++ b/NEWS @@ -49,6 +49,10 @@ 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. (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 077a7faf6562..40b0bbb0a2d2 100644 --- a/UPGRADING +++ b/UPGRADING @@ -201,6 +201,8 @@ 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. . linkinfo() now raises a ValueError when the $path argument is empty. . pathinfo() now raises a ValueError when an invalid $flag argument value is passed. From 05d61a627be4087ecbc5b693b1657efc78e53d70 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Tue, 21 Jul 2026 22:30:29 +0800 Subject: [PATCH 3/5] Use zend_str_has_nul_byte to directly throw error --- ext/standard/string.c | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ext/standard/string.c b/ext/standard/string.c index e4a86f166895..56bf6df87160 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -4837,16 +4837,12 @@ PHP_FUNCTION(strip_tags) } /* }}} */ -static zend_string *try_setlocale_str(zend_long cat, zend_string *loc, uint32_t arg_num) { +static zend_string *try_setlocale_str(zend_long cat, zend_string *loc) { const char *retval; if (zend_string_equals_literal(loc, "0")) { loc = NULL; } else { - if (zend_str_has_nul_byte(loc)) { - zend_argument_value_error(arg_num, "must not contain any null bytes"); - return NULL; - } if (ZSTR_LEN(loc) >= 255) { php_error_docref(NULL, E_WARNING, "Specified locale name is too long"); return NULL; @@ -4907,13 +4903,18 @@ static zend_string *try_setlocale_str(zend_long cat, zend_string *loc, uint32_t return zend_string_init(retval, strlen(retval), 0); } -static zend_string *try_setlocale_zval(zend_long cat, zval *loc_zv, uint32_t arg_num) { +static zend_string *try_setlocale_zval(zend_long cat, zval *loc_zv) { zend_string *tmp_loc_str; zend_string *loc_str = zval_try_get_tmp_string(loc_zv, &tmp_loc_str); if (UNEXPECTED(loc_str == NULL)) { return NULL; } - zend_string *result = try_setlocale_str(cat, loc_str, arg_num); + 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; } @@ -4934,8 +4935,11 @@ 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 (UNEXPECTED(Z_TYPE(args[i]) != IS_ARRAY && !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 : Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, + &args[i]); goto out; } } @@ -4945,7 +4949,7 @@ PHP_FUNCTION(setlocale) if (Z_TYPE(args[i]) == IS_ARRAY) { zval *elem; ZEND_HASH_FOREACH_VAL(Z_ARRVAL(args[i]), elem) { - result = try_setlocale_zval(cat, elem, i + 2); + result = try_setlocale_zval(cat, elem); if (EG(exception)) { goto out; } @@ -4956,9 +4960,9 @@ PHP_FUNCTION(setlocale) } ZEND_HASH_FOREACH_END(); continue; } else if (Z_ISNULL(args[i])) { - result = try_setlocale_str(cat, ZSTR_EMPTY_ALLOC(), i + 2); + result = try_setlocale_str(cat, ZSTR_EMPTY_ALLOC()); } else { - result = try_setlocale_str(cat, strings[i], i + 2); + result = try_setlocale_str(cat, strings[i]); } if (EG(exception)) { goto out; From 32a4b7cdecc79c7c0deb15182e2479b0977fa2ca Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 22 Jul 2026 03:58:43 +0800 Subject: [PATCH 4/5] Add suggested test case Co-Authored-By: Jorg Adam Sowa <74921107+jorgsowa@users.noreply.github.com> --- ext/standard/tests/strings/setlocale_null_byte.phpt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ext/standard/tests/strings/setlocale_null_byte.phpt b/ext/standard/tests/strings/setlocale_null_byte.phpt index d55f711b02ac..1f8ef187277b 100644 --- a/ext/standard/tests/strings/setlocale_null_byte.phpt +++ b/ext/standard/tests/strings/setlocale_null_byte.phpt @@ -20,6 +20,12 @@ try { echo $e->getMessage(), "\n"; } +try { + var_dump(setlocale(LC_ALL, ["invalid", "C\0invalid"])); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + try { var_dump(setlocale(LC_ALL, [], new NullByteStringable())); } catch (ValueError $e) { @@ -29,4 +35,5 @@ try { --EXPECT-- setlocale(): Argument #2 ($locales) must not contain any null bytes setlocale(): Argument #2 ($locales) must not contain any null bytes +setlocale(): Argument #2 ($locales) must not contain any null bytes setlocale(): Argument #3 must not contain any null bytes From 0d71ef5e8d5f9b7f146a5a73aedfa44a7d2241ac Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Wed, 22 Jul 2026 15:36:48 +0800 Subject: [PATCH 5/5] fix CI Co-Authored-By: Jorg Adam Sowa <74921107+jorgsowa@users.noreply.github.com> --- ext/standard/tests/strings/setlocale_null_byte.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/standard/tests/strings/setlocale_null_byte.phpt b/ext/standard/tests/strings/setlocale_null_byte.phpt index 1f8ef187277b..e84b8f8d203f 100644 --- a/ext/standard/tests/strings/setlocale_null_byte.phpt +++ b/ext/standard/tests/strings/setlocale_null_byte.phpt @@ -4,24 +4,24 @@ setlocale() rejects locale names with null bytes getMessage(), "\n"; } try { - var_dump(setlocale(LC_ALL, ["invalid\0locale", "C"])); + var_dump(setlocale(LC_ALL, ["locale\0name", "C"])); } catch (ValueError $e) { echo $e->getMessage(), "\n"; } try { - var_dump(setlocale(LC_ALL, ["invalid", "C\0invalid"])); + var_dump(@setlocale(LC_ALL, [str_repeat("x", 255), "C\0locale"])); } catch (ValueError $e) { echo $e->getMessage(), "\n"; }