diff --git a/NEWS b/NEWS index 4938b79b0713..d60f4f4f13d7 100644 --- a/NEWS +++ b/NEWS @@ -205,6 +205,8 @@ PHP NEWS specified number base. (Sjoerd Langkemper) . Improved performance of in_array() and array_search() in strict mode when searching for a string. (Alexander Danilov) + . setlocale() now only changes the locale of the current thread. + (Felix Bünemann) - URI: . Fix casing of enum cases in UriHostType and UrlHostType to match the RFC diff --git a/UPGRADING b/UPGRADING index d9242ecb3c17..d0f53db2333d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -120,6 +120,10 @@ PHP 8.7 UPGRADE NOTES rebuilding PHP with --enable-fd-setsize. On Windows, selecting on more than 64 pipe or file handles no longer crashes; it fails with a warning, as that path remains bounded by WaitForMultipleObjects(). + . setlocale() now only changes the locale of the current thread, as it + already did in thread-safe (ZTS) builds on Windows. Previously it changed + the locale of the whole process: in ZTS builds, requests running in other + threads used that locale too, and concurrent calls could crash. ======================================== 6. New Functions diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index b5134705b375..a37ce029ce41 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -60,6 +60,11 @@ PHP 8.7 INTERNALS UPGRADE NOTES - The ZTS symbols compiler_globals_offset and executor_globals_offset have been removed: CG()/EG() now reach the globals through a __thread struct. - Removed zend_error_noreturn_unchecked(). +- Added zend_setlocale(). During a request it only changes the locale of the + current thread, which is what the setlocale() function of PHP now does. + Extensions should use it instead of the setlocale() function of the C + library, which still changes the locale of the whole process and doesn't + return the locale of the current thread. ======================== 2. Build system changes diff --git a/Zend/zend_locale.c b/Zend/zend_locale.c new file mode 100644 index 000000000000..f0752e44fe61 --- /dev/null +++ b/Zend/zend_locale.c @@ -0,0 +1,357 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright © Zend Technologies Ltd., a subsidiary company of | + | Perforce Software, Inc., and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Felix Bünemann | + +----------------------------------------------------------------------+ +*/ + +#include "zend_locale.h" + +#ifdef ZEND_THREAD_LOCALE + +#include "zend.h" +#include "zend_globals.h" +#include "zend_smart_str.h" + +#ifdef HAVE_XLOCALE_H +# include +#endif + +/* setlocale() changes the locale of the whole process and isn't thread-safe. + * zend_setlocale() switches the current thread to a locale of its own with + * uselocale() instead. The locale dependent functions of the C library then + * use that locale in place of the process locale. Builds that aren't + * thread-safe work the same way, with a single thread. */ + +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) +# define ZEND_LOCALE_BSD 1 +#elif defined(__MUSL__) +# define ZEND_LOCALE_MUSL 1 +#endif + +/* Indexed by category */ +static const struct { + int mask; + const char *name; +} categories[] = { + [LC_ALL] = {LC_ALL_MASK, "LC_ALL"}, + [LC_COLLATE] = {LC_COLLATE_MASK, "LC_COLLATE"}, + [LC_CTYPE] = {LC_CTYPE_MASK, "LC_CTYPE"}, + [LC_MONETARY] = {LC_MONETARY_MASK, "LC_MONETARY"}, + [LC_NUMERIC] = {LC_NUMERIC_MASK, "LC_NUMERIC"}, + [LC_TIME] = {LC_TIME_MASK, "LC_TIME"}, + [LC_MESSAGES] = {LC_MESSAGES_MASK, "LC_MESSAGES"}, +#ifdef LC_PAPER_MASK + [LC_PAPER] = {LC_PAPER_MASK, "LC_PAPER"}, +#endif +#ifdef LC_NAME_MASK + [LC_NAME] = {LC_NAME_MASK, "LC_NAME"}, +#endif +#ifdef LC_ADDRESS_MASK + [LC_ADDRESS] = {LC_ADDRESS_MASK, "LC_ADDRESS"}, +#endif +#ifdef LC_TELEPHONE_MASK + [LC_TELEPHONE] = {LC_TELEPHONE_MASK, "LC_TELEPHONE"}, +#endif +#ifdef LC_MEASUREMENT_MASK + [LC_MEASUREMENT] = {LC_MEASUREMENT_MASK, "LC_MEASUREMENT"}, +#endif +#ifdef LC_IDENTIFICATION_MASK + [LC_IDENTIFICATION] = {LC_IDENTIFICATION_MASK, "LC_IDENTIFICATION"}, +#endif +}; + +#define CATEGORY_COUNT ((int) (sizeof(categories) / sizeof(categories[0]))) + +/* All categories except LC_ALL, in the order of their constants. The C library + * uses the same order for the names in the name of LC_ALL. */ +#define FOREACH_CATEGORY(category) \ + for (int category = 0; category < CATEGORY_COUNT; category++) \ + if (category != LC_ALL && categories[category].name) + +/* When the categories have different locales, the name of LC_ALL is made of + * their names, as in the C library: "C/C.UTF-8/C/C/C/C" on macOS and BSD, + * "LC_CTYPE=C.UTF-8;LC_NUMERIC=C;..." on glibc and "C.UTF-8;C;C;C;C;C" + * elsewhere (musl). */ +#ifdef ZEND_LOCALE_BSD +# define NAME_SEPARATOR "/" +#else +# define NAME_SEPARATOR ";" +#endif + +typedef struct _zend_locale { + locale_t locale; + struct _zend_locale *next; + /* Indexed by category */ + char *names[CATEGORY_COUNT]; +} zend_locale; + +/* Shared by all threads and never freed: threads keep using them, and a + * process only needs one for each combination of locales. A locale doesn't + * change once it is in the list and new ones become the head, so the list can + * be read without a lock. */ +static _Atomic(zend_locale *) locales = NULL; + +/* Locale of the current thread, NULL for the process locale */ +ZEND_TLS zend_locale *thread_locale = NULL; + +/* The name setlocale() returns for a locale. The name "" is the locale of the + * environment and needs the environment lock. */ +static const char *zend_locale_resolve_name(int category, const char *name) +{ + if (name[0] == '\0') { + /* The locale of the environment */ + const char *variables[] = {"LC_ALL", categories[category].name, "LANG"}; + +#ifdef ZEND_LOCALE_MUSL + name = "C.UTF-8"; +#else + name = "C"; +#endif + for (size_t i = 0; i < sizeof(variables) / sizeof(variables[0]); i++) { + const char *value = getenv(variables[i]); + if (value && value[0] != '\0') { + name = value; + break; + } + } + } + +#ifndef ZEND_LOCALE_BSD + if (strcmp(name, "POSIX") == 0) { + return "C"; + } +#endif +#ifdef ZEND_LOCALE_MUSL + /* musl only has a C.UTF-8 locale for LC_CTYPE */ + if (category != LC_CTYPE && strcmp(name, "C.UTF-8") == 0) { + return "C"; + } +#endif + + return name; +} + +/* Splits a name of LC_ALL into the names of the categories */ +static bool zend_locale_split_name(char *all, const char **names) +{ +#ifdef __GLIBC__ + int count = 0; + char *last = NULL; + + for (char *name = strtok_r(all, NAME_SEPARATOR, &last); name; name = strtok_r(NULL, NAME_SEPARATOR, &last)) { + char *value = strchr(name, '='); + if (!value) { + return false; + } + *value = '\0'; + + int category = 0; + while (category < CATEGORY_COUNT && (category == LC_ALL || strcmp(categories[category].name, name) != 0)) { + category++; + } + if (category == CATEGORY_COUNT) { + return false; + } + names[category] = value + 1; + count++; + } + + return count == CATEGORY_COUNT - 1; +#else + FOREACH_CATEGORY(category) { + char *end = strstr(all, NAME_SEPARATOR); + if (end) { + *end = '\0'; + } +#ifdef ZEND_LOCALE_BSD + /* Elsewhere, an empty name is the locale of the environment */ + if (all[0] == '\0') { + return false; + } +#endif + names[category] = all; + /* The last name applies to the remaining categories */ + if (end) { + all = end + 1; + } + } + + return true; +#endif +} + +/* The name of LC_ALL for the names of the categories */ +static char *zend_locale_join_names(const char **names) +{ + smart_str all = {0}; + bool equal = true; + + FOREACH_CATEGORY(category) { + if (all.s) { + smart_str_appends_ex(&all, NAME_SEPARATOR, true); + } +#ifdef __GLIBC__ + smart_str_appends_ex(&all, categories[category].name, true); + smart_str_appendc_ex(&all, '=', true); +#endif + smart_str_appends_ex(&all, names[category], true); + equal = equal && strcmp(names[category], names[LC_CTYPE]) == 0; + } + smart_str_0(&all); + + char *name = pestrdup(equal ? names[LC_CTYPE] : ZSTR_VAL(all.s), 1); + smart_str_free_ex(&all, true); + + return name; +} + +static zend_locale *zend_locale_find(const char **names) +{ + for (zend_locale *locale = atomic_load_explicit(&locales, memory_order_acquire); locale; locale = locale->next) { + bool equal = true; + + FOREACH_CATEGORY(category) { + equal = equal && strcmp(locale->names[category], names[category]) == 0; + } + if (equal) { + return locale; + } + } + + return NULL; +} + +/* Returns the shared locale with these names, NULL if it isn't available */ +static zend_locale *zend_locale_get(const char **names) +{ + bool environment = false; + + FOREACH_CATEGORY(category) { + environment = environment || names[category][0] == '\0'; + } + if (!environment) { + FOREACH_CATEGORY(category) { + names[category] = zend_locale_resolve_name(category, names[category]); + } + + zend_locale *found = zend_locale_find(names); + if (found) { + return found; + } + } + + /* Locales are created one at a time: concurrent newlocale() calls crash + * on macOS, and the "" locale reads the environment */ + tsrm_env_lock(); + + FOREACH_CATEGORY(category) { + names[category] = zend_locale_resolve_name(category, names[category]); + } + + zend_locale *found = zend_locale_find(names); + if (!found) { + locale_t locale = newlocale(LC_ALL_MASK, "C", (locale_t) 0); + + FOREACH_CATEGORY(category) { + if (locale) { + locale_t base = locale; + locale = newlocale(categories[category].mask, names[category], base); + /* On success, newlocale() takes over the base locale, except + * on macOS */ +#ifdef __APPLE__ + freelocale(base); +#else + if (!locale) { + freelocale(base); + } +#endif + } + } + + if (locale) { + found = pemalloc(sizeof(zend_locale), 1); + found->locale = locale; + FOREACH_CATEGORY(category) { + found->names[category] = pestrdup(names[category], 1); + } + found->names[LC_ALL] = zend_locale_join_names(names); + found->next = atomic_load_explicit(&locales, memory_order_relaxed); + atomic_store_explicit(&locales, found, memory_order_release); + } + } + + tsrm_env_unlock(); + + return found; +} + +ZEND_API const char *zend_setlocale(int category, const char *locale) +{ + /* Outside of a request, the process locale is changed: it's set during + * startup and is the initial locale of all threads */ + if (!EG(active)) { + if (locale && thread_locale) { + uselocale(LC_GLOBAL_LOCALE); + thread_locale = NULL; + } + return setlocale(category, locale); + } + + if (category < 0 || category >= CATEGORY_COUNT || !categories[category].name) { + return NULL; + } + + const char *names[CATEGORY_COUNT]; + zend_locale *current = thread_locale; + + if (!current) { + /* The thread still uses the process locale. Its name of LC_ALL comes + * from a shared locale, as querying it isn't thread-safe. */ + FOREACH_CATEGORY(i) { + names[i] = setlocale(i, NULL); + } + current = zend_locale_get(names); + } + if (!locale || !current) { + return current ? current->names[category] : NULL; + } + + char *all = NULL; + + FOREACH_CATEGORY(i) { + names[i] = (category == LC_ALL || category == i) ? locale : current->names[i]; + } + if (category == LC_ALL && strstr(locale, NAME_SEPARATOR)) { + all = estrdup(locale); + if (!zend_locale_split_name(all, names)) { + efree(all); + return NULL; + } + } + + zend_locale *found = zend_locale_get(names); + if (all) { + efree(all); + } + if (!found) { + return NULL; + } + + uselocale(found->locale); + thread_locale = found; + + return found->names[category]; +} + +#endif diff --git a/Zend/zend_locale.h b/Zend/zend_locale.h new file mode 100644 index 000000000000..ec38ef40aa25 --- /dev/null +++ b/Zend/zend_locale.h @@ -0,0 +1,42 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright © Zend Technologies Ltd., a subsidiary company of | + | Perforce Software, Inc., and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Felix Bünemann | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_LOCALE_H +#define ZEND_LOCALE_H + +#include "zend_portability.h" + +#include + +BEGIN_EXTERN_C() + +/* zend_setlocale() keeps a table indexed by category, which needs LC_ALL not + * to be negative, as it is on AIX */ +#if defined(HAVE_USELOCALE) && LC_ALL >= 0 +# define ZEND_THREAD_LOCALE 1 +/* Like setlocale(), but during a request it only changes the locale of the + * current thread */ +ZEND_API const char *zend_setlocale(int category, const char *locale); +#else +/* On Windows the locale is already per thread in thread-safe builds, see + * _configthreadlocale() */ +# define zend_setlocale(category, locale) setlocale(category, locale) +#endif + +END_EXTERN_C() + +#endif /* ZEND_LOCALE_H */ diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index 2960b4be1400..ebde442a5a2c 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -24,6 +24,7 @@ #include "zend_variables.h" #include "zend_globals.h" #include "zend_list.h" +#include "zend_locale.h" #include "zend_API.h" #include "zend_strtod.h" #include "zend_exceptions.h" @@ -3016,7 +3017,7 @@ ZEND_API void zend_update_current_locale(void) /* {{{ */ #else char buf[16]; const char *charmap = NULL; - const char *locale = setlocale(LC_CTYPE, NULL); + const char *locale = zend_setlocale(LC_CTYPE, NULL); if (locale) { const char *dot = strchr(locale, '.'); @@ -3072,8 +3073,8 @@ ZEND_API void zend_reset_lc_ctype_locale(void) { /* Use the C.UTF-8 locale so that readline can process UTF-8 input, while not interfering * with single-byte locale-dependent functions used by PHP. */ - if (!setlocale(LC_CTYPE, "C.UTF-8")) { - setlocale(LC_CTYPE, "C"); + if (!zend_setlocale(LC_CTYPE, "C.UTF-8")) { + zend_setlocale(LC_CTYPE, "C"); } } diff --git a/configure.ac b/configure.ac index abfddd1f28e4..f313bcc93893 100644 --- a/configure.ac +++ b/configure.ac @@ -416,6 +416,7 @@ AC_CHECK_HEADERS([ unistd.h unix.h utime.h + xlocale.h sys/utsname.h sys/ipc.h dlfcn.h @@ -586,6 +587,7 @@ AC_CHECK_FUNCS([ symlink tzset unsetenv + uselocale usleep utime vasprintf @@ -1801,6 +1803,7 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ zend_lazy_objects.c zend_list.c zend_llist.c + zend_locale.c zend_max_execution_timer.c zend_multibyte.c zend_object_handlers.c diff --git a/ext/intl/formatter/formatter_parse.cpp b/ext/intl/formatter/formatter_parse.cpp index 0acab835085c..ae7135bdb0f5 100644 --- a/ext/intl/formatter/formatter_parse.cpp +++ b/ext/intl/formatter/formatter_parse.cpp @@ -25,6 +25,7 @@ #include "php_intl.h" #include "intl_convert.h" +#include "zend_locale.h" #include #include @@ -71,8 +72,8 @@ U_CFUNC PHP_FUNCTION( numfmt_parse ) #if ICU_LOCALE_BUG && defined(LC_NUMERIC) /* need to copy here since setlocale may change it later */ - oldlocale = estrdup(setlocale(LC_NUMERIC, NULL)); - setlocale(LC_NUMERIC, "C"); + oldlocale = estrdup(zend_setlocale(LC_NUMERIC, NULL)); + zend_setlocale(LC_NUMERIC, "C"); #endif switch(type) { @@ -132,7 +133,7 @@ U_CFUNC PHP_FUNCTION( numfmt_parse ) cleanup: #if ICU_LOCALE_BUG && defined(LC_NUMERIC) - setlocale(LC_NUMERIC, oldlocale); + zend_setlocale(LC_NUMERIC, oldlocale); efree(oldlocale); #endif diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index db302ab724ab..166b793fe8a6 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -33,6 +33,7 @@ #include "zend_exceptions.h" #include "zend_ini.h" #include "zend_hrtime.h" +#include "zend_locale.h" #include "zend_operators.h" #include "ext/standard/php_dns.h" #include "ext/standard/php_uuencode.h" @@ -448,7 +449,7 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ /* Check if locale was changed and change it back * to the value in startup environment */ if (BG(locale_changed)) { - setlocale(LC_ALL, "C"); + zend_setlocale(LC_ALL, "C"); zend_reset_lc_ctype_locale(); zend_update_current_locale(); if (BG(ctype_string)) { diff --git a/ext/standard/string.c b/ext/standard/string.c index 40057af16a75..b1757acd16f4 100644 --- a/ext/standard/string.c +++ b/ext/standard/string.c @@ -29,6 +29,7 @@ #include "scanf.h" #include "zend_API.h" #include "zend_execute.h" +#include "zend_locale.h" #include "basic_functions.h" #include "zend_smart_str.h" #include @@ -5093,7 +5094,7 @@ static zend_string *try_setlocale_str(zend_long cat, zend_string *loc) { } # ifndef PHP_WIN32 - retval = setlocale(cat, loc ? ZSTR_VAL(loc) : NULL); + retval = zend_setlocale(cat, loc ? ZSTR_VAL(loc) : NULL); # else if (loc) { /* BC: don't try /^[a-z]{2}_[A-Z]{2}($|\..*)/ except for /^u[ks]_U[KS]$/ */ @@ -5108,10 +5109,10 @@ static zend_string *try_setlocale_str(zend_long cat, zend_string *loc) { ) { retval = NULL; } else { - retval = setlocale(cat, ZSTR_VAL(loc)); + retval = zend_setlocale(cat, ZSTR_VAL(loc)); } } else { - retval = setlocale(cat, NULL); + retval = zend_setlocale(cat, NULL); } # endif if (!retval) { diff --git a/ext/standard/tests/strings/setlocale_variation4.phpt b/ext/standard/tests/strings/setlocale_variation4.phpt index a2b236c146c8..843c67e2e3b4 100644 --- a/ext/standard/tests/strings/setlocale_variation4.phpt +++ b/ext/standard/tests/strings/setlocale_variation4.phpt @@ -11,7 +11,7 @@ if (setlocale(LC_ALL,'en_US.utf8') === false || setlocale(LC_ALL,'en_AU.utf8') = } ?> --ENV-- -LC_ALL=en_US.utf8; +LC_ALL=en_US.utf8 --FILE-- --ENV-- -LC_ALL=en_US.utf8; +LC_ALL=en_US.utf8 --FILE-- +--FILE-- + +--EXPECT-- +bool(true) +string(1) "C" +bool(true) +bool(true) +string(1) "C" +bool(true)