Subject: Use thread-safe locale APIs

From: "Tristan Partin" <[email-masked]>
Date: Tue, 31 Oct 2023 15:02:36 -0500

Postgres has been bitten by a few locale-related bugs, most recently via 
libperl[0]. I had previously submitted this patchset in the bug thread 
for the aforementioned bug, but here is a standalone submission for the 
purposes of an eventual commitfest submission, and to get discussion 
going. I was also flubbing up the commitfest bot with my patches. Sorry 
Joe! I feel fairly good about the patch, but I think I need some more 
testing and feedback. Localization is such a fickle beast.

I did leave one TODO because I need some input:

	/* TODO: This does not handle "" as the locale */

check_locale() takes a canonname argument, which the caller expects to 
be the "canonical name" of the locale the caller passed in. The 
setlocale() man page is not very explicit about under what conditions 
the return value is different from the input string, and I haven't found 
much on the internet. Best I can tell is that the empty string is the 
only input value that differs from the output value of setlocale(). If 
that's the case, on Postmaster startup, I can query setlocale() for what 
the empty string canonicalizes to for all the locale categories we care 
about, and save them off. The other solution to the problem would be to 
find the equivalent API in the uselocale() family of functions, but I am 
under the impression that such an API doesn't exist given I haven't 
found it yet.

Also, should we just remove HAVE_USELOCALE? It seems like Windows is the 
only platform that doesn't support it. Then we can just use _WIN32 
instead.

I do not think this should be backpatched. Please see Joe's patch in the 
bug thread as a way to fix the libperl bug on pre-17 versions.

[0]: https://www.postgresql.org/message-id/17946-3e84cb577e9551c3%40postgresql.org

-- 
Tristan Partin
Neon (https://neon.tech)
From d336d84cf60b22147e1234260b3199a52e9863e3 Mon Sep 17 00:00:00 2001
From: Tristan Partin <[email-masked]>
Date: Fri, 30 Jun 2023 09:31:04 -0500
Subject: [PATCH v1 1/3] Skip checking for uselocale on Windows

Windows doesn't have uselocale, so skip it.
---
 meson.build | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/meson.build b/meson.build
index 2d516c8f37..aeaf75d45a 100644
--- a/meson.build
+++ b/meson.build
@@ -2454,7 +2454,7 @@ func_checks = [
   ['strsignal'],
   ['sync_file_range'],
   ['syncfs'],
-  ['uselocale'],
+  ['uselocale', {'skip': host_system == 'windows'}],
   ['wcstombs_l'],
 ]
 
-- 
Tristan Partin
Neon (https://neon.tech)

From 557870f1846990246b80cafdf3e06349302a38a5 Mon Sep 17 00:00:00 2001
From: Tristan Partin <[email-masked]>
Date: Fri, 30 Jun 2023 11:13:29 -0500
Subject: [PATCH v1 2/3] Add locale_is_c function

In some places throughout the codebase, there are string comparisons for
locales matching C or POSIX. Encapsulate this logic into a single
function and use it.
---
 src/backend/utils/adt/pg_locale.c | 39 ++++++++++++++++++-------------
 src/backend/utils/init/postinit.c |  4 +---
 src/backend/utils/mb/mbutils.c    |  5 ++--
 src/include/utils/pg_locale.h     |  1 +
 4 files changed, 27 insertions(+), 22 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index d5003da417..61d5c5dc4c 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -195,6 +195,23 @@ wcstombs_l(char *dest, const wchar_t *src, size_t n, locale_t loc)
 }
 #endif
 
+
+/*
+ * Check if a locale is the C locale
+ *
+ * POSIX is an alias for C. Passing ingore_case as true will use
+ * pg_strcasecmp() instead of strcmp().
+ */
+bool
+locale_is_c(const char *locale, bool ignore_case)
+{
+	if (!ignore_case)
+		return strcmp(locale, "C") == 0 || strcmp(locale, "POSIX") == 0;
+
+	return pg_strcasecmp(locale, "C") == 0 || pg_strcasecmp(locale, "POSIX") == 0;
+}
+
+
 /*
  * pg_perm_setlocale
  *
@@ -1280,10 +1297,8 @@ lookup_collation_cache(Oid collation, bool set_flags)
 			datum = SysCacheGetAttrNotNull(COLLOID, tp, Anum_pg_collation_collctype);
 			collctype = TextDatumGetCString(datum);
 
-			cache_entry->collate_is_c = ((strcmp(collcollate, "C") == 0) ||
-										 (strcmp(collcollate, "POSIX") == 0));
-			cache_entry->ctype_is_c = ((strcmp(collctype, "C") == 0) ||
-									   (strcmp(collctype, "POSIX") == 0));
+			cache_entry->collate_is_c = locale_is_c(collcollate, false);
+			cache_entry->ctype_is_c = locale_is_c(collctype, false);
 		}
 		else
 		{
@@ -1331,12 +1346,8 @@ lc_collate_is_c(Oid collation)
 		if (!localeptr)
 			elog(ERROR, "invalid LC_COLLATE setting");
 
-		if (strcmp(localeptr, "C") == 0)
-			result = true;
-		else if (strcmp(localeptr, "POSIX") == 0)
-			result = true;
-		else
-			result = false;
+		result = locale_is_c(localeptr, false);
+
 		return (bool) result;
 	}
 
@@ -1384,12 +1395,8 @@ lc_ctype_is_c(Oid collation)
 		if (!localeptr)
 			elog(ERROR, "invalid LC_CTYPE setting");
 
-		if (strcmp(localeptr, "C") == 0)
-			result = true;
-		else if (strcmp(localeptr, "POSIX") == 0)
-			result = true;
-		else
-			result = false;
+		result = locale_is_c(localeptr, false);
+
 		return (bool) result;
 	}
 
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 552cf9d950..544312a099 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -419,9 +419,7 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
 						   " which is not recognized by setlocale().", ctype),
 				 errhint("Recreate the database with another locale or install the missing locale.")));
 
-	if (strcmp(ctype, "C") == 0 ||
-		strcmp(ctype, "POSIX") == 0)
-		database_ctype_is_c = true;
+	database_ctype_is_c = locale_is_c(ctype, false);
 
 	if (dbform->datlocprovider == COLLPROVIDER_ICU)
 	{
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 67a1ab2ab2..997156515c 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -39,6 +39,7 @@
 #include "mb/pg_wchar.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
+#include "utils/pg_locale.h"
 #include "utils/syscache.h"
 #include "varatt.h"
 
@@ -1237,9 +1238,7 @@ pg_bind_textdomain_codeset(const char *domainname)
 	int			new_msgenc;
 
 #ifndef WIN32
-	const char *ctype = setlocale(LC_CTYPE, NULL);
-
-	if (pg_strcasecmp(ctype, "C") == 0 || pg_strcasecmp(ctype, "POSIX") == 0)
+	if (locale_is_c(locale_ctype, true))
 #endif
 		if (encoding != PG_SQL_ASCII &&
 			raw_pg_bind_textdomain_codeset(domainname, encoding))
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index 6447bea8e0..e08d96e099 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -54,6 +54,7 @@ extern PGDLLIMPORT bool database_ctype_is_c;
 extern bool check_locale(int category, const char *locale, char **canonname);
 extern char *pg_perm_setlocale(int category, const char *locale);
 
+extern bool locale_is_c(const char *locale, bool ignore_case);
 extern bool lc_collate_is_c(Oid collation);
 extern bool lc_ctype_is_c(Oid collation);
 
-- 
Tristan Partin
Neon (https://neon.tech)

From a601560d10e1fb818fe9f7685521a4f4d0cb28c3 Mon Sep 17 00:00:00 2001
From: Tristan Partin <[email-masked]>
Date: Mon, 3 Jul 2023 08:51:59 -0500
Subject: [PATCH v1 3/3] Use thread-safe locale APIs

setlocale() has been a thorn in Postgres's side for a while. Most
recently this has manifested in bug #17946 where loading libperl would
cause the localization to change out from under Postgres. On Windows,
use _ENABLE_PER_THREAD_LOCALE.
---
 src/backend/main/main.c            |  55 +++-
 src/backend/utils/adt/cash.c       |  29 +++
 src/backend/utils/adt/formatting.c |  11 +-
 src/backend/utils/adt/pg_locale.c  | 393 ++++++++++++++++++-----------
 src/backend/utils/init/postinit.c  |   4 +-
 src/backend/utils/mb/mbutils.c     |   2 +-
 src/common/exec.c                  |  16 --
 src/include/utils/pg_locale.h      |   2 +-
 8 files changed, 330 insertions(+), 182 deletions(-)

diff --git a/src/backend/main/main.c b/src/backend/main/main.c
index ed11e8be7f..093df88f14 100644
--- a/src/backend/main/main.c
+++ b/src/backend/main/main.c
@@ -59,6 +59,11 @@ int
 main(int argc, char *argv[])
 {
 	bool		do_check_root = true;
+	char	   *collate_locale;
+	char	   *ctype_locale;
+#ifdef LC_MESSAGES
+	char	   *messages_locale;
+#endif
 
 	reached_main = true;
 
@@ -111,15 +116,39 @@ main(int argc, char *argv[])
 	 * these set to "C" then message localization might not work well in the
 	 * postmaster.
 	 */
-	init_locale("LC_COLLATE", LC_COLLATE, "");
-	init_locale("LC_CTYPE", LC_CTYPE, "");
+
+	/*
+	 * Save off the original locale values prior to the first call of
+	 * uselocale(). This ensures that we don't call pg_setlocale() with an empty
+	 * string. See the function comment for pg_setlocale() for further
+	 * explanation. Note that this restriction doesn't exist on Windows, but we
+	 * can use the same code path anyway.
+	 */
+	collate_locale = setlocale(LC_COLLATE, "");
+	ctype_locale = setlocale(LC_CTYPE, "");
+
+#ifdef LC_MESSAGES
+	messages_locale = setlocale(LC_MESSAGES, "");
+#endif
+
+
+#ifdef HAVE__CONFIGTHREADLOCALE
+	/*
+	 * This call could most likely happen sooner, but just to err on the side of
+	 * caution, call it after absorbing locales from the environment.
+	 */
+	_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
+#endif
+
+	init_locale("LC_COLLATE", LC_COLLATE, collate_locale);
+	init_locale("LC_CTYPE", LC_CTYPE, ctype_locale);
 
 	/*
 	 * LC_MESSAGES will get set later during GUC option processing, but we set
 	 * it here to allow startup error messages to be localized.
 	 */
 #ifdef LC_MESSAGES
-	init_locale("LC_MESSAGES", LC_MESSAGES, "");
+	init_locale("LC_MESSAGES", LC_MESSAGES, messages_locale);
 #endif
 
 	/*
@@ -130,13 +159,6 @@ main(int argc, char *argv[])
 	init_locale("LC_NUMERIC", LC_NUMERIC, "C");
 	init_locale("LC_TIME", LC_TIME, "C");
 
-	/*
-	 * Now that we have absorbed as much as we wish to from the locale
-	 * environment, remove any LC_ALL setting, so that the environment
-	 * variables installed by pg_perm_setlocale have force.
-	 */
-	unsetenv("LC_ALL");
-
 	/*
 	 * Catch standard options before doing much else, in particular before we
 	 * insist on not being root.
@@ -307,8 +329,17 @@ startup_hacks(const char *progname)
 static void
 init_locale(const char *categoryname, int category, const char *locale)
 {
-	if (pg_perm_setlocale(category, locale) == NULL &&
-		pg_perm_setlocale(category, "C") == NULL)
+	/*
+	 * Since we are initializing global locales, NULL and empty string are
+	 * invalid arguments. See the pg_setlocale() function description for more
+	 * explanation on the empty string. NULL just doesn't make sense in this
+	 * context of initializing locales.
+	 */
+	Assert(locale);
+	Assert(locale[0] != '\0');
+
+	if (pg_setlocale(category, locale) == NULL &&
+		pg_setlocale(category, "C") == NULL)
 		elog(FATAL, "could not adopt \"%s\" locale nor C locale for %s",
 			 locale, categoryname);
 }
diff --git a/src/backend/utils/adt/cash.c b/src/backend/utils/adt/cash.c
index 32fbad2f57..5c254d912f 100644
--- a/src/backend/utils/adt/cash.c
+++ b/src/backend/utils/adt/cash.c
@@ -111,6 +111,11 @@ cash_in(PG_FUNCTION_ARGS)
 			   *csymbol;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	if (lconvert == NULL)
+		ereturn(escontext, (Datum) 0,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("could not allocate memory for locale information")));
+
 	/*
 	 * frac_digits will be CHAR_MAX in some locales, notably C.  However, just
 	 * testing for == CHAR_MAX is risky, because of compilers like gcc that
@@ -325,6 +330,11 @@ cash_out(PG_FUNCTION_ARGS)
 				sep_by_space;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	if (lconvert == NULL)
+		ereturn(fcinfo->context, (Datum) 0,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("could not allocate memory for locale information")));
+
 	/* see comments about frac_digits in cash_in() */
 	points = lconvert->frac_digits;
 	if (points < 0 || points > 10)
@@ -1036,6 +1046,11 @@ cash_numeric(PG_FUNCTION_ARGS)
 	int			fpoint;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	if (lconvert == NULL)
+		ereturn(fcinfo->context, (Datum) 0,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("could not allocate memory for locale information")));
+
 	/* see comments about frac_digits in cash_in() */
 	fpoint = lconvert->frac_digits;
 	if (fpoint < 0 || fpoint > 10)
@@ -1095,6 +1110,11 @@ numeric_cash(PG_FUNCTION_ARGS)
 	Datum		numeric_scale;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	if (lconvert == NULL)
+		ereturn(fcinfo->context, (Datum) 0,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("could not allocate memory for locale information")));
+
 	/* see comments about frac_digits in cash_in() */
 	fpoint = lconvert->frac_digits;
 	if (fpoint < 0 || fpoint > 10)
@@ -1128,6 +1148,11 @@ int4_cash(PG_FUNCTION_ARGS)
 	int			i;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	if (lconvert == NULL)
+		ereturn(fcinfo->context, (Datum) 0,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("could not allocate memory for locale information")));
+
 	/* see comments about frac_digits in cash_in() */
 	fpoint = lconvert->frac_digits;
 	if (fpoint < 0 || fpoint > 10)
@@ -1158,6 +1183,10 @@ int8_cash(PG_FUNCTION_ARGS)
 	int			i;
 	struct lconv *lconvert = PGLC_localeconv();
 
+	ereturn(fcinfo->context, (Datum) 0,
+			(errcode(ERRCODE_OUT_OF_MEMORY),
+			 errmsg("could not allocate memory for locale information")));
+
 	/* see comments about frac_digits in cash_in() */
 	fpoint = lconvert->frac_digits;
 	if (fpoint < 0 || fpoint > 10)
diff --git a/src/backend/utils/adt/formatting.c b/src/backend/utils/adt/formatting.c
index 8131091f79..695bb2a491 100644
--- a/src/backend/utils/adt/formatting.c
+++ b/src/backend/utils/adt/formatting.c
@@ -5049,12 +5049,12 @@ NUM_prepare_locale(NUMProc *Np)
 		/*
 		 * Positive / Negative number sign
 		 */
-		if (lconv->negative_sign && *lconv->negative_sign)
+		if (lconv && lconv->negative_sign && *lconv->negative_sign)
 			Np->L_negative_sign = lconv->negative_sign;
 		else
 			Np->L_negative_sign = "-";
 
-		if (lconv->positive_sign && *lconv->positive_sign)
+		if (lconv && lconv->positive_sign && *lconv->positive_sign)
 			Np->L_positive_sign = lconv->positive_sign;
 		else
 			Np->L_positive_sign = "+";
@@ -5062,9 +5062,8 @@ NUM_prepare_locale(NUMProc *Np)
 		/*
 		 * Number decimal point
 		 */
-		if (lconv->decimal_point && *lconv->decimal_point)
+		if (lconv && lconv->decimal_point && *lconv->decimal_point)
 			Np->decimal = lconv->decimal_point;
-
 		else
 			Np->decimal = ".";
 
@@ -5078,7 +5077,7 @@ NUM_prepare_locale(NUMProc *Np)
 		 * but "" for thousands_sep, so we set the thousands_sep too.
 		 * http://archives.postgresql.org/pgsql-hackers/2007-11/msg00772.php
 		 */
-		if (lconv->thousands_sep && *lconv->thousands_sep)
+		if (lconv && lconv->thousands_sep && *lconv->thousands_sep)
 			Np->L_thousands_sep = lconv->thousands_sep;
 		/* Make sure thousands separator doesn't match decimal point symbol. */
 		else if (strcmp(Np->decimal, ",") != 0)
@@ -5089,7 +5088,7 @@ NUM_prepare_locale(NUMProc *Np)
 		/*
 		 * Currency symbol
 		 */
-		if (lconv->currency_symbol && *lconv->currency_symbol)
+		if (lconv && lconv->currency_symbol && *lconv->currency_symbol)
 			Np->L_currency_symbol = lconv->currency_symbol;
 		else
 			Np->L_currency_symbol = " ";
diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 61d5c5dc4c..7ea38b1880 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -102,6 +102,23 @@ char	   *locale_time;
 
 int			icu_validation_level = WARNING;
 
+#ifdef HAVE_USELOCALE
+/*
+ * Current locale settings
+ *
+ * uselocale() and friends don't have an equivalent to setlocale(cat, NULL), so
+ * as we set locales for various categories, stash them for retrieval as needed.
+ */
+static struct {
+	char *collate;
+	char *ctype;
+	char *messages;
+	char *monetary;
+	char *numeric;
+	char *time;
+} curr_locales = { 0 };
+#endif /* HAVE_USELOCALE */
+
 /*
  * lc_time localization cache.
  *
@@ -212,28 +229,149 @@ locale_is_c(const char *locale, bool ignore_case)
 }

+#ifdef HAVE_USELOCALE
+/*
+ * Turns locale categories into their mask equivalents for use in newlocale(3)
+ *
+ * GCC has the masks defined as (1 << category), but this isn't guaranteed to be
+ * the same in other libcs.
+ */
+static int
+category_to_mask(int category)
+{
+	switch (category)
+	{
+		case LC_ALL:
+			return LC_ALL_MASK;
+		case LC_COLLATE:
+			return LC_COLLATE_MASK;
+		case LC_CTYPE:
+			return LC_CTYPE_MASK;
+	#ifdef LC_MESSAGES
+		case LC_MESSAGES:
+			return LC_MESSAGES_MASK;
+	#endif
+		case LC_MONETARY:
+			return LC_MONETARY_MASK;
+		case LC_NUMERIC:
+			return LC_NUMERIC_MASK;
+		case LC_TIME:
+			return LC_TIME_MASK;
+	}
+
+	pg_unreachable();
+}
+
+
+static char *
+pg_getlocale(int category)
+{
+	switch (category)
+	{
+		case LC_COLLATE:
+			return curr_locales.collate;
+		case LC_CTYPE:
+			return curr_locales.ctype;
+		case LC_MESSAGES:
+			return curr_locales.messages;
+		case LC_MONETARY:
+			return curr_locales.monetary;
+		case LC_NUMERIC:
+			return curr_locales.numeric;
+		case LC_TIME:
+			return curr_locales.time;
+		default:
+			pg_unreachable();
+	}
+}
+#endif /* HAVE_USELOCALE */
+
+
 /*
- * pg_perm_setlocale
- *
- * This wraps the libc function setlocale(), with two additions.  First, when
- * changing LC_CTYPE, update gettext's encoding for the current message
- * domain.  GNU gettext automatically tracks LC_CTYPE on most platforms, but
- * not on Windows.  Second, if the operation is successful, the corresponding
- * LC_XXX environment variable is set to match.  By setting the environment
- * variable, we ensure that any subsequent use of setlocale(..., "") will
- * preserve the settings made through this routine.  Of course, LC_ALL must
- * also be unset to fully ensure that, but that has to be done elsewhere after
- * all the individual LC_XXX variables have been set correctly.  (Thank you
- * Perl for making this kluge necessary.)
+ * pg_setlocale
+ *
+ * This wraps the libc functions uselocale() or setlocale() (depending on the
+ * platform), with a single addition.  When changing LC_CTYPE, update gettext's
+ * encoding for the current message domain. GNU gettext automatically tracks
+ * LC_CTYPE on most platforms, but not on Windows.  This function will abort if
+ * locale is the empty string.  A workaround is to pass the output of
+ * setlocale(category, "") to the locale argument.
  */
 char *
-pg_perm_setlocale(int category, const char *locale)
+pg_setlocale(int category, const char *locale)
 {
 	char	   *result;
-	const char *envvar;
+#ifdef HAVE_USELOCALE
+	char	  **save;
+	int			category_mask;
+	locale_t	prev_locale;
+	locale_t	temp_locale;
+	locale_t	work_locale;
+
+	if (locale == NULL)
+		return pg_getlocale(category);
+
+	Assert(locale[0] != '\0');
+
+	category_mask = category_to_mask(category);
+
+	prev_locale = uselocale((locale_t) 0);
+	Assert(prev_locale != (locale_t) 0);
+
+	result = strdup(locale);
+	work_locale = duplocale(prev_locale);
+	if (!result || work_locale == (locale_t) 0) {
+		ereport(ERROR,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("out of memory")));
+
+		return NULL;
+	}
+
+	temp_locale = newlocale(category_mask, locale, work_locale);
+	if (temp_locale == (locale_t) 0)
+	{
+		freelocale(work_locale);
+
+		return NULL;			/* fall out immediately on failure */
+	}
+	work_locale = temp_locale;
+
+	uselocale(work_locale);
+	if (prev_locale != LC_GLOBAL_LOCALE)
+		freelocale(prev_locale);
+
+	/*
+	 * Copying the locale must occur before we potentially bind the text domain.
+	 */
+	switch (category)
+	{
+		case LC_COLLATE:
+			save = &curr_locales.collate;
+			break;
+		case LC_CTYPE:
+			save = &curr_locales.ctype;
+			break;
+		case LC_MESSAGES:
+			save = &curr_locales.messages;
+			break;
+		case LC_MONETARY:
+			save = &curr_locales.monetary;
+			break;
+		case LC_NUMERIC:
+			save = &curr_locales.numeric;
+			break;
+		case LC_TIME:
+			save = &curr_locales.time;
+			break;
+		default:
+			pg_unreachable();
+	}
+
+	/* Free old locale string, and stash the new one */
+	free(*save);
+	*save = result;
 
-#ifndef WIN32
-	result = setlocale(category, locale);
 #else
 
 	/*
@@ -252,11 +390,12 @@ pg_perm_setlocale(int category, const char *locale)
 	else
 #endif
 		result = setlocale(category, locale);
-#endif							/* WIN32 */
 
 	if (result == NULL)
 		return result;			/* fall out immediately on failure */
 
+#endif /* HAVE_USELOCALE */
+
 	/*
 	 * Use the right encoding in translated messages.  Under ENABLE_NLS, let
 	 * pg_bind_textdomain_codeset() figure it out.  Under !ENABLE_NLS, message
@@ -266,11 +405,13 @@ pg_perm_setlocale(int category, const char *locale)
 	 */
 	if (category == LC_CTYPE)
 	{
+#ifndef HAVE_USELOCALE
 		static char save_lc_ctype[LOCALE_NAME_BUFLEN];
 
 		/* copy setlocale() return value before callee invokes it again */
 		strlcpy(save_lc_ctype, result, sizeof(save_lc_ctype));
 		result = save_lc_ctype;
+#endif
 
 #ifdef ENABLE_NLS
 		SetMessageEncoding(pg_bind_textdomain_codeset(textdomain(NULL)));
@@ -279,42 +420,6 @@ pg_perm_setlocale(int category, const char *locale)
 #endif
 	}
 
-	switch (category)
-	{
-		case LC_COLLATE:
-			envvar = "LC_COLLATE";
-			break;
-		case LC_CTYPE:
-			envvar = "LC_CTYPE";
-			break;
-#ifdef LC_MESSAGES
-		case LC_MESSAGES:
-			envvar = "LC_MESSAGES";
-#ifdef WIN32
-			result = IsoLocaleName(locale);
-			if (result == NULL)
-				result = (char *) locale;
-			elog(DEBUG3, "IsoLocaleName() executed; locale: \"%s\"", result);
-#endif							/* WIN32 */
-			break;
-#endif							/* LC_MESSAGES */
-		case LC_MONETARY:
-			envvar = "LC_MONETARY";
-			break;
-		case LC_NUMERIC:
-			envvar = "LC_NUMERIC";
-			break;
-		case LC_TIME:
-			envvar = "LC_TIME";
-			break;
-		default:
-			elog(FATAL, "unrecognized LC category: %d", category);
-			return NULL;		/* keep compiler quiet */
-	}
-
-	if (setenv(envvar, result, 1) != 0)
-		return NULL;
-
 	return result;
 }
 
@@ -332,6 +437,48 @@ pg_perm_setlocale(int category, const char *locale)
 bool
 check_locale(int category, const char *locale, char **canonname)
 {
+#ifdef HAVE_USELOCALE
+	locale_t	saved_locale;
+	locale_t	temp_locale;
+	locale_t	work_locale;
+	int			category_mask;
+
+	category_mask = category_to_mask(category);
+
+	if (canonname)
+		*canonname = NULL;		/* in case of failure */
+
+	saved_locale = uselocale((locale_t) 0);
+	work_locale = duplocale(saved_locale);
+	if (work_locale == (locale_t) 0)
+	{
+		if (errno == ENOMEM)
+			ereport(ERROR,
+					(errcode(ERRCODE_OUT_OF_MEMORY),
+					 errmsg("out of memory")));
+
+		return false;
+	}
+
+	temp_locale = newlocale(category_mask, locale, work_locale);
+	if (temp_locale == (locale_t) 0) {
+		freelocale(work_locale);
+
+		return false;
+	}
+	work_locale = temp_locale;
+
+	uselocale(work_locale);
+	if (canonname)
+		*canonname = pstrdup(locale); /* TODO: This does not handle "" as the locale */
+
+	uselocale(saved_locale);
+	freelocale(work_locale);
+
+	return true;
+
+#else
+
 	char	   *save;
 	char	   *res;
 
@@ -358,6 +505,7 @@ check_locale(int category, const char *locale, char **canonname)
 	pfree(save);
 
 	return (res != NULL);
+#endif /* HAVE_USELOCALE */
 }

@@ -370,7 +518,7 @@ check_locale(int category, const char *locale, char **canonname)
  *
  * Note: we accept value = "" as selecting the postmaster's environment
  * value, whatever it was (so long as the environment setting is legal).
- * This will have been locked down by an earlier call to pg_perm_setlocale.
+ * This will have been locked down by an earlier call to pg_setlocale.
  */
 bool
 check_locale_monetary(char **newval, void **extra, GucSource source)
@@ -449,7 +597,7 @@ assign_locale_messages(const char *newval, void *extra)
 	 * We ignore failure, as per comment above.
 	 */
 #ifdef LC_MESSAGES
-	(void) pg_perm_setlocale(LC_MESSAGES, newval);
+	(void) pg_setlocale(LC_MESSAGES, newval);
 #endif
 }
 
@@ -545,11 +693,8 @@ PGLC_localeconv(void)
 	static bool CurrentLocaleConvAllocated = false;
 	struct lconv *extlconv;
 	struct lconv worklconv;
-	char	   *save_lc_monetary;
-	char	   *save_lc_numeric;
-#ifdef WIN32
-	char	   *save_lc_ctype;
-#endif
+	locale_t	saved_locale;
+	locale_t	work_locale;
 
 	/* Did we do it already? */
 	if (CurrentLocaleConvValid)
@@ -576,16 +721,15 @@ PGLC_localeconv(void)
 	 */
 	memset(&worklconv, 0, sizeof(worklconv));
 
-	/* Save prevailing values of monetary and numeric locales */
-	save_lc_monetary = setlocale(LC_MONETARY, NULL);
-	if (!save_lc_monetary)
-		elog(ERROR, "setlocale(NULL) failed");
-	save_lc_monetary = pstrdup(save_lc_monetary);
+	/* Save prevailing locale */
+	saved_locale = uselocale((locale_t) 0);
+	Assert(saved_locale != (locale_t) 0);
 
-	save_lc_numeric = setlocale(LC_NUMERIC, NULL);
-	if (!save_lc_numeric)
-		elog(ERROR, "setlocale(NULL) failed");
-	save_lc_numeric = pstrdup(save_lc_numeric);
+	work_locale = duplocale(saved_locale);
+	if (work_locale == (locale_t) 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("out of memory")));
 
 #ifdef WIN32
 
@@ -603,37 +747,37 @@ PGLC_localeconv(void)
 	 * results.  Hence, we must temporarily set that category as well.
 	 */
 
-	/* Save prevailing value of ctype locale */
-	save_lc_ctype = setlocale(LC_CTYPE, NULL);
-	if (!save_lc_ctype)
-		elog(ERROR, "setlocale(NULL) failed");
-	save_lc_ctype = pstrdup(save_lc_ctype);
-
 	/* Here begins the critical section where we must not throw error */
 
 	/* use numeric to set the ctype */
-	setlocale(LC_CTYPE, locale_numeric);
+	work_locale = newlocale(LC_CTYPE_MASK, locale_numeric, work_locale);
+	Assert(work_locale != (locale_t) 0);
 #endif
 
 	/* Get formatting information for numeric */
-	setlocale(LC_NUMERIC, locale_numeric);
+	work_locale = newlocale(LC_NUMERIC_MASK, locale_numeric, work_locale);
+	Assert(work_locale != (locale_t) 0);
+	uselocale(work_locale);
 	extlconv = localeconv();
 
-	/* Must copy data now in case setlocale() overwrites it */
+	/* Must copy data now in case updating the locale overwrites it */
 	worklconv.decimal_point = strdup(extlconv->decimal_point);
 	worklconv.thousands_sep = strdup(extlconv->thousands_sep);
 	worklconv.grouping = strdup(extlconv->grouping);
 
 #ifdef WIN32
 	/* use monetary to set the ctype */
-	setlocale(LC_CTYPE, locale_monetary);
+	work_locale = newlocale(LC_CTYPE_MASK, locale_monetary, work_locale);
+	Assert(work_locale != (locale_t) 0);
 #endif
 
 	/* Get formatting information for monetary */
-	setlocale(LC_MONETARY, locale_monetary);
+	work_locale = newlocale(LC_MONETARY_MASK, locale_monetary, work_locale);
+	Assert(work_locale != (locale_t) 0);
+	uselocale(work_locale);
 	extlconv = localeconv();
 
-	/* Must copy data now in case setlocale() overwrites it */
+	/* Must copy data now in case updating the locale overwrites it */
 	worklconv.int_curr_symbol = strdup(extlconv->int_curr_symbol);
 	worklconv.currency_symbol = strdup(extlconv->currency_symbol);
 	worklconv.mon_decimal_point = strdup(extlconv->mon_decimal_point);
@@ -651,22 +795,9 @@ PGLC_localeconv(void)
 	worklconv.p_sign_posn = extlconv->p_sign_posn;
 	worklconv.n_sign_posn = extlconv->n_sign_posn;
 
-	/*
-	 * Restore the prevailing locale settings; failure to do so is fatal.
-	 * Possibly we could limp along with nondefault LC_MONETARY or LC_NUMERIC,
-	 * but proceeding with the wrong value of LC_CTYPE would certainly be bad
-	 * news; and considering that the prevailing LC_MONETARY and LC_NUMERIC
-	 * are almost certainly "C", there's really no reason that restoring those
-	 * should fail.
-	 */
-#ifdef WIN32
-	if (!setlocale(LC_CTYPE, save_lc_ctype))
-		elog(FATAL, "failed to restore LC_CTYPE to \"%s\"", save_lc_ctype);
-#endif
-	if (!setlocale(LC_MONETARY, save_lc_monetary))
-		elog(FATAL, "failed to restore LC_MONETARY to \"%s\"", save_lc_monetary);
-	if (!setlocale(LC_NUMERIC, save_lc_numeric))
-		elog(FATAL, "failed to restore LC_NUMERIC to \"%s\"", save_lc_numeric);
+	/* Restore the prevailing locale settings; failure to do so is fatal. */
+	uselocale(saved_locale);
+	freelocale(work_locale);
 
 	/*
 	 * At this point we've done our best to clean up, and can call functions
@@ -677,13 +808,6 @@ PGLC_localeconv(void)
 	{
 		int			encoding;
 
-		/* Release the pstrdup'd locale names */
-		pfree(save_lc_monetary);
-		pfree(save_lc_numeric);
-#ifdef WIN32
-		pfree(save_lc_ctype);
-#endif
-
 		/* If any of the preceding strdup calls failed, complain now. */
 		if (!struct_lconv_is_valid(&worklconv))
 			ereport(ERROR,
@@ -830,10 +954,8 @@ cache_locale_time(void)
 	bool		strftimefail = false;
 	int			encoding;
 	int			i;
-	char	   *save_lc_time;
-#ifdef WIN32
-	char	   *save_lc_ctype;
-#endif
+	locale_t	saved_locale;
+	locale_t	work_locale;
 
 	/* did we do this already? */
 	if (CurrentLCTimeValid)
@@ -848,11 +970,15 @@ cache_locale_time(void)
 	 * results afterwards.
 	 */
 
-	/* Save prevailing value of time locale */
-	save_lc_time = setlocale(LC_TIME, NULL);
-	if (!save_lc_time)
-		elog(ERROR, "setlocale(NULL) failed");
-	save_lc_time = pstrdup(save_lc_time);
+	/* Save prevailing locale */
+	saved_locale = uselocale((locale_t) 0);
+	Assert(saved_locale != (locale_t) 0);
+
+	work_locale = duplocale(saved_locale);
+	if (work_locale == (locale_t) 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_OUT_OF_MEMORY),
+				 errmsg("out of memory")));
 
 #ifdef WIN32
 
@@ -863,17 +989,14 @@ cache_locale_time(void)
 	 * the encoding we'll get back from strftime_win32().
 	 */
 
-	/* Save prevailing value of ctype locale */
-	save_lc_ctype = setlocale(LC_CTYPE, NULL);
-	if (!save_lc_ctype)
-		elog(ERROR, "setlocale(NULL) failed");
-	save_lc_ctype = pstrdup(save_lc_ctype);
-
 	/* use lc_time to set the ctype */
-	setlocale(LC_CTYPE, locale_time);
+	work_locale = newlocale(LC_CTYPE_MASK, locale_time, work_locale);
+	Assert(work_locale != (locale_t) 0);
 #endif
 
-	setlocale(LC_TIME, locale_time);
+	work_locale = newlocale(LC_TIME_MASK, locale_time, work_locale);
+	Assert(work_locale != (locale_t) 0);
+	uselocale(work_locale);
 
 	/* We use times close to current time as data for strftime(). */
 	timenow = time(NULL);
@@ -921,12 +1044,8 @@ cache_locale_time(void)
 	 * Restore the prevailing locale settings; as in PGLC_localeconv(),
 	 * failure to do so is fatal.
 	 */
-#ifdef WIN32
-	if (!setlocale(LC_CTYPE, save_lc_ctype))
-		elog(FATAL, "failed to restore LC_CTYPE to \"%s\"", save_lc_ctype);
-#endif
-	if (!setlocale(LC_TIME, save_lc_time))
-		elog(FATAL, "failed to restore LC_TIME to \"%s\"", save_lc_time);
+	uselocale(saved_locale);
+	freelocale(work_locale);
 
 	/*
 	 * At this point we've done our best to clean up, and can throw errors, or
@@ -935,12 +1054,6 @@ cache_locale_time(void)
 	if (strftimefail)
 		elog(ERROR, "strftime() failed: %m");
 
-	/* Release the pstrdup'd locale names */
-	pfree(save_lc_time);
-#ifdef WIN32
-	pfree(save_lc_ctype);
-#endif
-
 #ifndef WIN32
 
 	/*
@@ -1335,18 +1448,14 @@ lc_collate_is_c(Oid collation)
 	if (collation == DEFAULT_COLLATION_OID)
 	{
 		static int	result = -1;
-		char	   *localeptr;
 
 		if (default_locale.provider == COLLPROVIDER_ICU)
 			return false;
 
 		if (result >= 0)
 			return (bool) result;
-		localeptr = setlocale(LC_COLLATE, NULL);
-		if (!localeptr)
-			elog(ERROR, "invalid LC_COLLATE setting");
 
-		result = locale_is_c(localeptr, false);
+		result = locale_is_c(curr_locales.collate, false);
 
 		return (bool) result;
 	}
@@ -1379,23 +1488,19 @@ lc_ctype_is_c(Oid collation)
 
 	/*
 	 * If we're asked about the default collation, we have to inquire of the C
-	 * library.  Cache the result so we only have to compute it once.
+	 * library.
 	 */
 	if (collation == DEFAULT_COLLATION_OID)
 	{
 		static int	result = -1;
-		char	   *localeptr;
 
 		if (default_locale.provider == COLLPROVIDER_ICU)
 			return false;
 
 		if (result >= 0)
 			return (bool) result;
-		localeptr = setlocale(LC_CTYPE, NULL);
-		if (!localeptr)
-			elog(ERROR, "invalid LC_CTYPE setting");
 
-		result = locale_is_c(localeptr, false);
+		result = locale_is_c(curr_locales.ctype, false);
 
 		return (bool) result;
 	}
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 544312a099..2009991635 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -405,14 +405,14 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect
 	datum = SysCacheGetAttrNotNull(DATABASEOID, tup, Anum_pg_database_datctype);
 	ctype = TextDatumGetCString(datum);
 
-	if (pg_perm_setlocale(LC_COLLATE, collate) == NULL)
+	if (pg_setlocale(LC_COLLATE, collate) == NULL)
 		ereport(FATAL,
 				(errmsg("database locale is incompatible with operating system"),
 				 errdetail("The database was initialized with LC_COLLATE \"%s\", "
 						   " which is not recognized by setlocale().", collate),
 				 errhint("Recreate the database with another locale or install the missing locale.")));
 
-	if (pg_perm_setlocale(LC_CTYPE, ctype) == NULL)
+	if (pg_setlocale(LC_CTYPE, ctype) == NULL)
 		ereport(FATAL,
 				(errmsg("database locale is incompatible with operating system"),
 				 errdetail("The database was initialized with LC_CTYPE \"%s\", "
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 997156515c..8dc7da47bd 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -1238,7 +1238,7 @@ pg_bind_textdomain_codeset(const char *domainname)
 	int			new_msgenc;
 
 #ifndef WIN32
-	if (locale_is_c(locale_ctype, true))
+	if (locale_is_c(pg_setlocale(LC_CTYPE, NULL), true))
 #endif
 		if (encoding != PG_SQL_ASCII &&
 			raw_pg_bind_textdomain_codeset(domainname, encoding))
diff --git a/src/common/exec.c b/src/common/exec.c
index f209b934df..f7a9643be4 100644
--- a/src/common/exec.c
+++ b/src/common/exec.c
@@ -438,22 +438,6 @@ set_pglocale_pgservice(const char *argv0, const char *app)
 	char		path[MAXPGPATH];
 	char		my_exec_path[MAXPGPATH];
 
-	/* don't set LC_ALL in the backend */
-	if (strcmp(app, PG_TEXTDOMAIN("postgres")) != 0)
-	{
-		setlocale(LC_ALL, "");
-
-		/*
-		 * One could make a case for reproducing here PostmasterMain()'s test
-		 * for whether the process is multithreaded.  Unlike the postmaster,
-		 * no frontend program calls sigprocmask() or otherwise provides for
-		 * mutual exclusion between signal handlers.  While frontends using
-		 * fork(), if multithreaded, are formally exposed to undefined
-		 * behavior, we have not witnessed a concrete bug.  Therefore,
-		 * complaining about multithreading here may be mere pedantry.
-		 */
-	}
-
 	if (find_my_exec(argv0, my_exec_path) < 0)
 		return;
 
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index e08d96e099..585cdd3931 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -52,7 +52,7 @@ extern PGDLLIMPORT char *localized_full_months[];
 extern PGDLLIMPORT bool database_ctype_is_c;
 
 extern bool check_locale(int category, const char *locale, char **canonname);
-extern char *pg_perm_setlocale(int category, const char *locale);
+extern char *pg_setlocale(int category, const char *locale);
 
 extern bool locale_is_c(const char *locale, bool ignore_case);
 extern bool lc_collate_is_c(Oid collation);
-- 
Tristan Partin
Neon (https://neon.tech)