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)