Subject: Re: On non-Windows, hard depend on uselocale(3)

From: Tom Lane <[email-masked]>
Date: Wed, 15 Nov 2023 12:45:46 -0500

"Tristan Partin" <[email-masked]> writes:
> I would like to propose removing HAVE_USELOCALE, and just have WIN32, 
> which means that Postgres would require uselocale(3) on anything that 
> isn't WIN32.

You would need to do some research and try to prove that that won't
be a problem on any modern platform.  Presumably it once was a problem,
or we'd not have bothered with a configure check.

(Some git archaeology might yield useful info about when and why
we added the check.)

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Thu, 16 Nov 2023 07:38:55 +1300

> (Some git archaeology might yield useful info about when and why

According to data I scraped from the build farm, the last two systems
we had that didn't have uselocale() were curculio (OpenBSD 5.9) and
wrasse (Solaris 11.3), but those were both shut down (though wrasse
still runs old branches) as they were well out of support.  OpenBSD
gained uselocale() in 6.2, and Solaris in 11.4, as part of the same
suite of POSIX changes that we already required in commit 8d9a9f03.

+1 for the change.

https://man.openbsd.org/uselocale.3
https://docs.oracle.com/cd/E88353_01/html/E37843/uselocale-3c.html

---

From: Dagfinn Ilmari Mannsåker <[email-masked]>
Date: Wed, 15 Nov 2023 18:42:31 +0000

> (Some git archaeology might yield useful info about when and why

For reference, the Perl effort to use the POSIX.1-2008 thread-safe
locale APIs have revealed several platform-specific bugs that cause it
to disable them on FreeBSD and macOS:

https://github.com/perl/perl5/commit/9cbc12c368981c56d4d8e40cc9417ac26bec2c35
https://github.com/perl/perl5/commit/dd4eb78c55aab441aec1639b1dd49f88bd960831

and work around bugs on others (e.g. OpenBSD):

https://github.com/perl/perl5/commit/0f3830f3997cf7ef1531bad26d2e0f13220dd862

But Perl actually makes use of per-thread locales, because it has a
separate interpereer per thread, each of which can have a different
locale active.  Since Postgres isn't actually multi-threaded (yet),
these concerns might not apply to the same degree.

> 			regards, tom lane

- ilmari

---

From: Tom Lane <[email-masked]>
Date: Wed, 15 Nov 2023 14:04:03 -0500

> these concerns might not apply to the same degree.

Interesting.  That need not stop us from dropping the configure
check for uselocale(), but it might be a problem for Tristan's
larger ambitions.

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Thu, 16 Nov 2023 08:16:31 +1300

> https://github.com/perl/perl5/commit/9cbc12c368981c56d4d8e40cc9417ac26bec2c35

Interesting that C vs C.UTF-8 has come up there, something that has
also confused us and others (in fact I still owe Daniel Vérité a
response to his complaint about how we treat the latter; I got stuck
on a logical problem with the proposal and then dumped core...).  The
idea of C.UTF-8 is relatively new, and seems to have shaken a few bugs
out in a few places.  Anyway, that in particular is a brand new
FreeBSD bug report and I am sure it will be addressed soon.

> https://github.com/perl/perl5/commit/dd4eb78c55aab441aec1639b1dd49f88bd960831

As for macOS, one thing I noticed is that the FreeBSD -> macOS
pipeline appears to have re-awoken after many years of slumber.  I
don't know anything about that other than that when I recently
upgraded my Mac to 14.1, suddenly a few userspace tools are now
running the recentish FreeBSD versions of certain userland tools (tar,
grep, ...?), instead of something from the Jurassic.  Whether that
might apply to libc, who can say... they seemed to have quite ancient
BSD locale code last time I checked.

> https://github.com/perl/perl5/commit/0f3830f3997cf7ef1531bad26d2e0f13220dd862

That linked issue appears to be fixed already.

> these concerns might not apply to the same degree.

ECPG might use them in multi-threaded code.  I'm not sure if it's a
problem and whose problem it is.

---

From: Tom Lane <[email-masked]>
Date: Wed, 15 Nov 2023 15:51:38 -0500

> still runs old branches) as they were well out of support.

AFAICS, NetBSD still doesn't have it.  They have no on-line man page
for it, and my animal mamba shows it as not found.

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Thu, 16 Nov 2023 10:08:12 +1300

> for it, and my animal mamba shows it as not found.

Oh :-(  I see that but had missed that sidewinder was NetBSD and my
scraped data predates mamba.  Sorry for the wrong info.

Currently pg_locale.c requires systems to have *either* uselocale() or
mbstowcs_l()/wcstombs_l(), but NetBSD satisfies the second
requirement.  The other uses of uselocale() are in ECPG code that must
be falling back to the setlocale() path.  In other words, isn't it the
case that we don't require uselocale() to compile ECPG stuff, but it'll
probably crash or corrupt itself or give wrong answers if you push it
on NetBSD, so... uhh, really we do require it?

---

From: Tom Lane <[email-masked]>
Date: Wed, 15 Nov 2023 16:17:08 -0500

Thomas Munro <[email-masked]> writes:
> requirement.

Check.

> The other uses of uselocale() are in ECPG code that must

Dunno.  mamba is getting through the ecpg regression tests okay,
but we all know that doesn't prove a lot.  (AFAICS, ecpg only
cares about this to the extent of not wanting an LC_NUMERIC
locale where the decimal point isn't '.'.  I'm not sure that
NetBSD supports any such locale anyway --- I think they're like
OpenBSD in having only pro-forma locale support.)

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Thu, 16 Nov 2023 11:40:07 +1300

> OpenBSD in having only pro-forma locale support.)

Idea #1

For output, which happens with sprintf(ptr, "%.15g%s", ...) in
execute.c, perhaps we could use our in-tree Ryu routine instead?

For input, which happens with strtod() in data.c, rats, we don't have
a parser and I understand that it is not for the faint of heart (naive
implementation gets subtle things wrong, cf "How to read floating
point numbers accurately" by W D Clinger + whatever improvements have
happened in this space since 1990).

Idea #2

Perhaps we could use snprintf_l() and strtod_l() where available.
They're not standard, but they are obvious extensions that NetBSD and
Windows have, and those are the two systems for which we are doing
grotty things in that code.  That would amount to extending
pg_locale.c's philosophy: either you must have uselocale(), or the
full set of _l() functions (that POSIX failed to standardise, dunno
what the history is behind that, seems weird).

---

From: Tom Lane <[email-masked]>
Date: Wed, 15 Nov 2023 18:06:22 -0500

> a parser and I understand that it is not for the faint of heart

Yeah.  Getting rid of ecpg's use of uselocale() would certainly be
nice, but I'm not ready to add our own implementation of strtod()
to get there.

> grotty things in that code.

Oooh, shiny.  I do not see any man page for strtod_l, but I do see
that it's declared on mamba's host.  I wonder how long they've had it?
The man page for snprintf_l appears to be quite ancient, so we could
hope that strtod_l is available on all versions anyone cares about.

> That would amount to extending

Yeah.  I'd say the _l functions should be preferred over uselocale()
if available, but sadly they're not there on common systems.  (It
looks like glibc has strtod_l but not snprintf_l, which is odd.)

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Fri, 17 Nov 2023 08:57:47 +1300

> hope that strtod_l is available on all versions anyone cares about.

A decade[1].  And while I'm doing archeology, I noticed that POSIX has
agreed[2] in principle that *all* functions affected by the thread's
current locale should have a _l() variant, it's just that no one has
sent in the patch.

> looks like glibc has strtod_l but not snprintf_l, which is odd.)

Here is a first attempt.  In this version, new functions are exported
by pgtypeslib.  I realised that I had to do it in there because ECPG's
uselocale() jiggery-pokery is clearly intended to affect the
conversions happening in there too, and we probably don't want
circular dependencies between pgtypeslib and ecpglib.  I think this
means that pgtypeslib is actually subtly b0rked if you use it
independently without an ECPG connection (is that a thing people do?),
because all that code copied-and-pasted from the backend when run in
frontend code with eg a French locale will produce eg "0,42"; this
patch doesn't change that.

I also had a go[3] at doing it with static inlined functions, to avoid
creating a load of new exported functions and associated function call
overheads.  It worked fine, except on Windows: I needed a global
variable PGTYPESclocale that all the inlined functions can see when
called from ecpglib or pgtypeslib code, but if I put that in the
exports list then on that platform it seems to contain garbage; there
is probably some other magic needed to export non-function symbols
from the DLL or something like that, I didn't look into it.  See CI
failure + crash dumps.

[1] https://github.com/NetBSD/src/commit/c99aac45e540bc210cc660619a6b5323cbb5c17f
[2] https://www.austingroupbugs.net/view.php?id=1004
[3] https://github.com/macdice/postgres/tree/strtod_l_inline

---

From: Tom Lane <[email-masked]>
Date: Fri, 17 Nov 2023 13:18:28 -0500

> Here is a first attempt.

I've not reviewed this closely, but I did try it on mamba's host.
It compiles and passes regression testing, but I see two warnings:

common.c: In function 'PGTYPESsprintf':
common.c:120:2: warning: function 'PGTYPESsprintf' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
  120 |  return vsprintf_l(str, PGTYPESclocale, format, args);
      |  ^~~~~~
common.c: In function 'PGTYPESsnprintf':
common.c:136:2: warning: function 'PGTYPESsnprintf' might be a candidate for 'gnu_printf' format attribute [-Wsuggest-attribute=format]
  136 |  return vsnprintf_l(str, size, PGTYPESclocale, format, args);
      |  ^~~~~~

That happens because on NetBSD, we define PG_PRINTF_ATTRIBUTE as
"__syslog__" so that the compiler will not warn about use of %m
(apparently, they support %m in syslog() but not printf(), sigh).

I think this is telling us about an actual problem: these new
functions are based on libc's printf not what we have in snprintf.c,
and therefore we really shouldn't be assuming that they will support
any format specs beyond what POSIX requires for printf.  If somebody
tried to use %m in one of these calls, we'd like to get warnings about
that.

I experimented with the attached delta patch and it does silence
these warnings.  I suspect that ecpg_log() should be marked as
pg_attribute_std_printf() too, because it has the same issue,
but I didn't try that.  (Probably, we see no warning for that
because the compiler isn't quite bright enough to connect the
format argument with the string that gets passed to vfprintf().)

			regards, tom lane

diff --git a/src/include/c.h b/src/include/c.h
index 82f8e9d4c7..98e3bbf386 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -171,13 +171,19 @@
 #define PG_USED_FOR_ASSERTS_ONLY pg_attribute_unused()
 #endif
 
-/* GCC and XLC support format attributes */
+/*
+ * GCC and XLC support format attributes.  Use pg_attribute_printf()
+ * for our src/port/snprintf.c implementation and functions based on it.
+ * Use pg_attribute_std_printf() for functions based on libc's printf.
+ */
 #if defined(__GNUC__) || defined(__IBMC__)
 #define pg_attribute_format_arg(a) __attribute__((format_arg(a)))
 #define pg_attribute_printf(f,a) __attribute__((format(PG_PRINTF_ATTRIBUTE, f, a)))
+#define pg_attribute_std_printf(f,a) __attribute__((format(printf, f, a)))
 #else
 #define pg_attribute_format_arg(a)
 #define pg_attribute_printf(f,a)
+#define pg_attribute_std_printf(f,a)
 #endif
 
 /* GCC, Sunpro and XLC support aligned, packed and noreturn */
diff --git a/src/interfaces/ecpg/include/pgtypes_format.h b/src/interfaces/ecpg/include/pgtypes_format.h
index d6dd06d361..87160fab59 100644
--- a/src/interfaces/ecpg/include/pgtypes_format.h
+++ b/src/interfaces/ecpg/include/pgtypes_format.h
@@ -20,7 +20,7 @@ extern int PGTYPESbegin_clocale(locale_t *old_locale);
 extern void PGTYPESend_clocale(locale_t old_locale);
 
 extern double PGTYPESstrtod(const char *str, char **endptr);
-extern int PGTYPESsprintf(char *str, const char *format, ...) pg_attribute_printf(2, 3);
-extern int PGTYPESsnprintf(char *str, size_t size, const char *format, ...) pg_attribute_printf(3, 4);
+extern int PGTYPESsprintf(char *str, const char *format, ...) pg_attribute_std_printf(2, 3);
+extern int PGTYPESsnprintf(char *str, size_t size, const char *format, ...) pg_attribute_std_printf(3, 4);
 
 #endif

---

From: Tom Lane <[email-masked]>
Date: Fri, 17 Nov 2023 17:58:48 -0500

> any format specs beyond what POSIX requires for printf.

Wait, I just realized that there's more to this.  ecpglib *does*
rely on our snprintf.c functions:

$ nm --ext --undef src/interfaces/ecpg/ecpglib/*.o | grep printf 
                 U pg_snprintf
                 U pg_fprintf
                 U pg_snprintf
                 U pg_printf
                 U pg_snprintf
                 U pg_sprintf
                 U pg_fprintf
                 U pg_snprintf
                 U pg_vfprintf
                 U pg_snprintf
                 U pg_sprintf
                 U pg_sprintf

We are getting these warnings because vsprintf_l and
vsnprintf_l don't have snprintf.c implementations, so the
compiler sees the attributes attached to them by stdio.h.

This raises the question of whether changing snprintf.c
could be part of the solution.  I'm not sure that we want
to try to emulate vs[n]printf_l directly, but perhaps there's
another way?

In any case, my concern about ecpg_log() is misplaced.
That is really using pg_vfprintf, so it's correctly marked.

			regards, tom lane

---

From: Andres Freund <[email-masked]>
Date: Fri, 17 Nov 2023 16:03:23 -0800

Hi,

> from the DLL or something like that, I didn't look into it.  See CI

I suspect you'd need __declspec(dllimport) on the variable to make that work.
I.e. use PGDLLIMPORT and define BUILDING_DLL while building the libraries, so
they see __declspec (dllexport).  I luckily forgot the details, but functions
just call into some thunk that does necessary magic, but that option doesn't
exist for variables, so the compiler/linker have to do stuff, hence needing
__declspec(dllimport).

Greetings,

Andres Freund

---

From: Thomas Munro <[email-masked]>
Date: Mon, 20 Nov 2023 11:00:14 +1300

> > any format specs beyond what POSIX requires for printf.

Right, thanks.

> to try to emulate vs[n]printf_l directly, but perhaps there's

Yeah, I have been wondering about that too.

The stuff I posted so far was just about how to remove some gross and
incorrect code from ecpg, a somewhat niche frontend part of
PostgreSQL.  I guess Tristan is thinking bigger: removing obstacles to
going multi-threaded in the backend.  Clearly locales are one of the
places where global state will bite us, so we either need to replace
setlocale() with uselocale() for the database default locale, or use
explicit locale arguments with _l() functions everywhere and pass in
the right locale.  Due to incompleteness of (a) libc implementations
and (b) the standard, we can't directly do either, so we'll need to
cope with that.

Thought experiment:  If we supplied our own fallback _l() replacement
functions where missing, and those did uselocale() save/restore, many
systems wouldn't need them, for example glibc has strtod_l() as you
noted, and several other systems have systematically added them for
all sorts of stuff.  The case of the *printf* family is quite
interesting, because there we already have our own implement for other
reasons, so it might make sense to add the _l() variants to our
snprintf.c implementations.  On glibc, snprintf.c would have to do a
uselocale() save/restore where it punts %g to the system snprintf, but
if that offends some instruction cycle bean counter, perhaps we could
replace that bit with Ryu anyway (or is it not general enough to
handle all the stuff %g et al can do?  I haven't looked).

I am not sure how you would ever figure out what other stuff is
affected by the global locale in general, for example code hiding in
extensions etc, but, I mean, that's what's wrong with global state in
a nutshell and it has often been speculated that multi-threaded
PostgreSQL might have a way to say 'I still want one process per
session because my extensions don't all identify themselves as
thread-safe yet'.

BTW is this comment in snprintf.c true?

 * 1. No locale support: the radix character is always '.' and the '
 * (single quote) format flag is ignored.

It is in the backend but only because we nail down LC_NUMERIC early
on, not because of any property of snprintf.c, no?

---

From: Tom Lane <[email-masked]>
Date: Sun, 19 Nov 2023 17:36:24 -0500

> on, not because of any property of snprintf.c, no?

Hmm, the second part of it is true.  But given that we punt float
formatting to libc, I think you are right that the first part
depends on LC_NUMERIC being frozen.

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Mon, 20 Nov 2023 13:00:11 +1300

> depends on LC_NUMERIC being frozen.

If we are sure that we'll *never* want locale-aware printf-family
functions (ie we *always* want "C" locale), then in the thought
experiment above where I suggested we supply replacement _l()
functions, we could just skip that for the printf family, but make
that above comment actually true.  Perhaps with Ryu, but otherwise by
punting to libc _l() or uselocale() save/restore.

---

From: Tom Lane <[email-masked]>
Date: Mon, 20 Nov 2023 11:40:43 -0500

> punting to libc _l() or uselocale() save/restore.

It is pretty annoying that we've got that shiny Ryu code and can't
use it here.  From memory, we did look into that and concluded that
Ryu wasn't amenable to providing "exactly this many digits" as is
required by most variants of printf's conversion specs.  But maybe
somebody should go try harder.  (Worst case, you could do rounding
off by hand on the produced digit string, but that's ugly...)

			regards, tom lane

---

From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 13:29:51 +1200

> > punting to libc _l() or uselocale() save/restore.

Here is a new attempt at this can of portability worms.  This time:

* pg_get_c_locale() is available to anyone who needs a "C" locale_t
* ECPG uses strtod_l(..., pg_get_c_locale()) for parsing
* snprintf.c always uses "C" for floats, so it conforms to its own
documented behaviour, and ECPG doesn't have to do anything special

I'm not trying to offer a working *printf_l() family to the whole tree
because it seems like really we only ever care about "C" for this
purpose.  So snprintf.c internally uses pg_get_c_locale() with
snprintf_l(), _snprintf_l() or uselocale()/snprintf()/uselocale()
depending on platform.

> off by hand on the produced digit string, but that's ugly...)

Yeah it does seem like a promising idea, but I haven't looked into it myself.
From bc64f2db61b3e2cf7a0cd00b1deaa260d91e2b3c Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 11:01:21 +1200
Subject: [PATCH v2] Improve locale thread safety of ECPG.

Remove the use of setlocale() as a fallback strategy on systems that
don't have uselocale(), where ECPG tries to control LC_NUMERIC
formatting on input and output of floating point numbers.

Instead, create a new function pg_get_c_locale() that can be used to get
a locale_t object for use with thread-safe parsing and formatting
functions.  Then:

1.  Use strtod_l() for parsing, and supply an implementation using
uselocale() if it is missing.  (All systems have one of strtod_l(),
_strtod_l() or uselocale() for a replacement.)

2.  Inside our own snprintf.c, use snprintf_l() or _snprintf_l() where
we punt floating point numbers to the system snprintf(), or wrap it in
uselocale() if those are not available.

Since the non-standard _l() functions require <xlocale.h> on *BSD/macOS
systems, simplify the configure probes: instead of XXX_IN_XLOCALE_H for
several features XXX, let's just include <xlocale.h> if HAVE_XLOCALE_H.
The reason for the extra complication was apparently that some old glibc
systems also had an <xlocale.h>, and you weren't supposed to include it
directly, but it's gone now (as far as I can tell it was harmless to do
so anyway).

Discussion: https://postgr.es/m/CWZBBRR6YA8D.8EHMDRGLCKCD%40neon.tech
---
 config/c-library.m4                          |  55 ---------
 configure                                    | 115 +------------------
 configure.ac                                 |   6 +-
 meson.build                                  |  37 +-----
 src/include/pg_config.h.in                   |  15 +--
 src/include/port.h                           |  29 +++++
 src/include/utils/pg_locale.h                |   2 +-
 src/interfaces/ecpg/ecpglib/connect.c        |  37 ------
 src/interfaces/ecpg/ecpglib/data.c           |   2 +-
 src/interfaces/ecpg/ecpglib/descriptor.c     |  37 ------
 src/interfaces/ecpg/ecpglib/ecpglib_extern.h |  15 ---
 src/interfaces/ecpg/ecpglib/execute.c        |  55 ---------
 src/interfaces/ecpg/pgtypeslib/dt_common.c   |   6 +-
 src/interfaces/ecpg/pgtypeslib/interval.c    |   4 +-
 src/interfaces/ecpg/pgtypeslib/numeric.c     |   2 +-
 src/port/Makefile                            |   1 +
 src/port/locale.c                            |  73 ++++++++++++
 src/port/meson.build                         |   1 +
 src/port/snprintf.c                          |  33 ++++++
 19 files changed, 157 insertions(+), 368 deletions(-)
 create mode 100644 src/port/locale.c

diff --git a/config/c-library.m4 b/config/c-library.m4
index aa8223d2ef..421bc612b2 100644
--- a/config/c-library.m4
+++ b/config/c-library.m4
@@ -81,58 +81,3 @@ AC_DEFUN([PGAC_STRUCT_SOCKADDR_SA_LEN],
 [#include <sys/types.h>
 #include <sys/socket.h>
 ])])# PGAC_STRUCT_SOCKADDR_MEMBERS
-
-
-# PGAC_TYPE_LOCALE_T
-# ------------------
-# Check for the locale_t type and find the right header file.  macOS
-# needs xlocale.h; standard is locale.h, but glibc <= 2.25 also had an
-# xlocale.h file that we should not use, so we check the standard
-# header first.
-AC_DEFUN([PGAC_TYPE_LOCALE_T],
-[AC_CACHE_CHECK([for locale_t], pgac_cv_type_locale_t,
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <locale.h>
-locale_t x;],
-[])],
-[pgac_cv_type_locale_t=yes],
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <xlocale.h>
-locale_t x;],
-[])],
-[pgac_cv_type_locale_t='yes (in xlocale.h)'],
-[pgac_cv_type_locale_t=no])])])
-if test "$pgac_cv_type_locale_t" = 'yes (in xlocale.h)'; then
-  AC_DEFINE(LOCALE_T_IN_XLOCALE, 1,
-            [Define to 1 if `locale_t' requires <xlocale.h>.])
-fi])# PGAC_TYPE_LOCALE_T
-
-
-# PGAC_FUNC_WCSTOMBS_L
-# --------------------
-# Try to find a declaration for wcstombs_l().  It might be in stdlib.h
-# (following the POSIX requirement for wcstombs()), or in locale.h, or in
-# xlocale.h.  If it's in the latter, define WCSTOMBS_L_IN_XLOCALE.
-#
-AC_DEFUN([PGAC_FUNC_WCSTOMBS_L],
-[AC_CACHE_CHECK([for wcstombs_l declaration], pgac_cv_func_wcstombs_l,
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <stdlib.h>
-#include <locale.h>],
-[#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif])],
-[pgac_cv_func_wcstombs_l='yes'],
-[AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
-[#include <stdlib.h>
-#include <locale.h>
-#include <xlocale.h>],
-[#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif])],
-[pgac_cv_func_wcstombs_l='yes (in xlocale.h)'],
-[pgac_cv_func_wcstombs_l='no'])])])
-if test "$pgac_cv_func_wcstombs_l" = 'yes (in xlocale.h)'; then
-  AC_DEFINE(WCSTOMBS_L_IN_XLOCALE, 1,
-            [Define to 1 if `wcstombs_l' requires <xlocale.h>.])
-fi])# PGAC_FUNC_WCSTOMBS_L
diff --git a/configure b/configure
index 4f3aa44756..298492ec25 100755
--- a/configure
+++ b/configure
@@ -14635,55 +14635,6 @@ _ACEOF
 fi

-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for locale_t" >&5
-$as_echo_n "checking for locale_t... " >&6; }
-if ${pgac_cv_type_locale_t+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <locale.h>
-locale_t x;
-int
-main ()
-{
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_type_locale_t=yes
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <xlocale.h>
-locale_t x;
-int
-main ()
-{
-
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_type_locale_t='yes (in xlocale.h)'
-else
-  pgac_cv_type_locale_t=no
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_type_locale_t" >&5
-$as_echo "$pgac_cv_type_locale_t" >&6; }
-if test "$pgac_cv_type_locale_t" = 'yes (in xlocale.h)'; then
-
-$as_echo "#define LOCALE_T_IN_XLOCALE 1" >>confdefs.h
-
-fi
-
 # MSVC doesn't cope well with defining restrict to __restrict, the
 # spelling it understands, because it conflicts with
 # __declspec(restrict). Therefore we define pg_restrict to the
@@ -15170,59 +15121,6 @@ if test x"$pgac_cv_var_int_timezone" = xyes ; then
 
 $as_echo "#define HAVE_INT_TIMEZONE 1" >>confdefs.h
 
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for wcstombs_l declaration" >&5
-$as_echo_n "checking for wcstombs_l declaration... " >&6; }
-if ${pgac_cv_func_wcstombs_l+:} false; then :
-  $as_echo_n "(cached) " >&6
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <stdlib.h>
-#include <locale.h>
-int
-main ()
-{
-#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_func_wcstombs_l='yes'
-else
-  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-#include <stdlib.h>
-#include <locale.h>
-#include <xlocale.h>
-int
-main ()
-{
-#ifndef wcstombs_l
-(void) wcstombs_l;
-#endif
-  ;
-  return 0;
-}
-_ACEOF
-if ac_fn_c_try_compile "$LINENO"; then :
-  pgac_cv_func_wcstombs_l='yes (in xlocale.h)'
-else
-  pgac_cv_func_wcstombs_l='no'
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
-fi
-{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_func_wcstombs_l" >&5
-$as_echo "$pgac_cv_func_wcstombs_l" >&6; }
-if test "$pgac_cv_func_wcstombs_l" = 'yes (in xlocale.h)'; then
-
-$as_echo "#define WCSTOMBS_L_IN_XLOCALE 1" >>confdefs.h
-
 fi
 
 # Some versions of libedit contain strlcpy(), setproctitle(), and other
@@ -15232,7 +15130,7 @@ fi
 LIBS_including_readline="$LIBS"
 LIBS=`echo "$LIBS" | sed -e 's/-ledit//g' -e 's/-lreadline//g'`
 
-for ac_func in backtrace_symbols copyfile copy_file_range getifaddrs getpeerucred inet_pton kqueue mbstowcs_l memset_s posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast strchrnul strsignal syncfs sync_file_range uselocale wcstombs_l
+for ac_func in backtrace_symbols copyfile copy_file_range getifaddrs getpeerucred inet_pton kqueue mbstowcs_l memset_s posix_fallocate ppoll pthread_is_threaded_np setproctitle setproctitle_fast snprintf_l strtod_l strchrnul strsignal syncfs sync_file_range uselocale wcstombs_l
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
 ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
@@ -16005,17 +15903,6 @@ fi
 
 # Win32 (really MinGW) support
 if test "$PORTNAME" = "win32"; then
-  for ac_func in _configthreadlocale
-do :
-  ac_fn_c_check_func "$LINENO" "_configthreadlocale" "ac_cv_func__configthreadlocale"
-if test "x$ac_cv_func__configthreadlocale" = xyes; then :
-  cat >>confdefs.h <<_ACEOF
-#define HAVE__CONFIGTHREADLOCALE 1
-_ACEOF
-
-fi
-done
-
   case " $LIBOBJS " in
   *" dirmod.$ac_objext "* ) ;;
   *) LIBOBJS="$LIBOBJS dirmod.$ac_objext"
diff --git a/configure.ac b/configure.ac
index 049bc01491..302aadd427 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1620,8 +1620,6 @@ PGAC_UNION_SEMUN
 AC_CHECK_TYPES(socklen_t, [], [], [#include <sys/socket.h>])
 PGAC_STRUCT_SOCKADDR_SA_LEN
 
-PGAC_TYPE_LOCALE_T
-
 # MSVC doesn't cope well with defining restrict to __restrict, the
 # spelling it understands, because it conflicts with
 # __declspec(restrict). Therefore we define pg_restrict to the
@@ -1720,7 +1718,6 @@ fi
 ##
 
 PGAC_VAR_INT_TIMEZONE
-PGAC_FUNC_WCSTOMBS_L
 
 # Some versions of libedit contain strlcpy(), setproctitle(), and other
 # symbols that that library has no business exposing to the world.  Pending
@@ -1744,6 +1741,8 @@ AC_CHECK_FUNCS(m4_normalize([
 	pthread_is_threaded_np
 	setproctitle
 	setproctitle_fast
+	snprintf_l
+	strtod_l
 	strchrnul
 	strsignal
 	syncfs
@@ -1833,7 +1832,6 @@ fi
 
 # Win32 (really MinGW) support
 if test "$PORTNAME" = "win32"; then
-  AC_CHECK_FUNCS(_configthreadlocale)
   AC_LIBOBJ(dirmod)
   AC_LIBOBJ(kill)
   AC_LIBOBJ(open)
diff --git a/meson.build b/meson.build
index cc176f11b5..932334ed63 100644
--- a/meson.build
+++ b/meson.build
@@ -2407,6 +2407,7 @@ header_checks = [
   'sys/ucred.h',
   'termios.h',
   'ucred.h',
+  'xlocale.h',
 ]
 
 foreach header : header_checks
@@ -2550,15 +2551,6 @@ else
   cdata.set('STRERROR_R_INT', false)
 endif
 
-# Find the right header file for the locale_t type.  macOS needs xlocale.h;
-# standard is locale.h, but glibc <= 2.25 also had an xlocale.h file that
-# we should not use so we check the standard header first.  MSVC has a
-# replacement defined in src/include/port/win32_port.h.
-if not cc.has_type('locale_t', prefix: '#include <locale.h>') and \
-   cc.has_type('locale_t', prefix: '#include <xlocale.h>')
-  cdata.set('LOCALE_T_IN_XLOCALE', 1)
-endif
-
 # Check if the C compiler understands typeof or a variant.  Define
 # HAVE_TYPEOF if so, and define 'typeof' to the actual key word.
 foreach kw : ['typeof', '__typeof__', 'decltype']
@@ -2583,30 +2575,6 @@ int main(void)
   endif
 endforeach
 
-
-# Try to find a declaration for wcstombs_l().  It might be in stdlib.h
-# (following the POSIX requirement for wcstombs()), or in locale.h, or in
-# xlocale.h.  If it's in the latter, define WCSTOMBS_L_IN_XLOCALE.
-wcstombs_l_test = '''
-#include <stdlib.h>
-#include <locale.h>
-@0@
-
-void main(void)
-{
-#ifndef wcstombs_l
-    (void) wcstombs_l;
-#endif
-}
-'''
-if (not cc.compiles(wcstombs_l_test.format(''),
-      name: 'wcstombs_l') and
-    cc.compiles(wcstombs_l_test.format('#include <xlocale.h>'),
-      name: 'wcstombs_l in xlocale.h'))
-    cdata.set('WCSTOMBS_L_IN_XLOCALE', 1)
-endif
-
-
 # MSVC doesn't cope well with defining restrict to __restrict, the spelling it
 # understands, because it conflicts with __declspec(restrict). Therefore we
 # define pg_restrict to the appropriate definition, which presumably won't
@@ -2658,7 +2626,6 @@ endif
 # XXX: Might be worth conditioning some checks on the OS, to avoid doing
 # unnecessary checks over and over, particularly on windows.
 func_checks = [
-  ['_configthreadlocale', {'skip': host_system != 'windows'}],
   ['backtrace_symbols', {'dependencies': [execinfo_dep]}],
   ['clock_gettime', {'dependencies': [rt_dep], 'define': false}],
   ['copyfile'],
@@ -2690,6 +2657,8 @@ func_checks = [
   ['shm_open', {'dependencies': [rt_dep], 'define': false}],
   ['shm_unlink', {'dependencies': [rt_dep], 'define': false}],
   ['shmget', {'dependencies': [cygipc_dep], 'define': false}],
+  ['snprintf_l'],
+  ['strtod_l'],
   ['socket', {'dependencies': [socket_dep], 'define': false}],
   ['strchrnul'],
   ['strerror_r', {'dependencies': [thread_dep]}],
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 0e9b108e66..c0586edab1 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -376,6 +376,9 @@
 /* Define to 1 if you have the `setproctitle_fast' function. */
 #undef HAVE_SETPROCTITLE_FAST
 
+/* Define to 1 if you have the `snprintf_l' function. */
+#undef HAVE_SNPRINTF_L
+
 /* Define to 1 if the system has the type `socklen_t'. */
 #undef HAVE_SOCKLEN_T
 
@@ -421,6 +424,9 @@
 /* Define to 1 if you have the `strsignal' function. */
 #undef HAVE_STRSIGNAL
 
+/* Define to 1 if you have the `strtod_l' function. */
+#undef HAVE_STRTOD_L
+
 /* Define to 1 if the system has the type `struct option'. */
 #undef HAVE_STRUCT_OPTION
 
@@ -556,9 +562,6 @@
 /* Define to 1 if your compiler understands __builtin_unreachable. */
 #undef HAVE__BUILTIN_UNREACHABLE
 
-/* Define to 1 if you have the `_configthreadlocale' function. */
-#undef HAVE__CONFIGTHREADLOCALE
-
 /* Define to 1 if you have __cpuid. */
 #undef HAVE__CPUID
 
@@ -577,9 +580,6 @@
 /* Define to the appropriate printf length modifier for 64-bit ints. */
 #undef INT64_MODIFIER
 
-/* Define to 1 if `locale_t' requires <xlocale.h>. */
-#undef LOCALE_T_IN_XLOCALE
-
 /* Define as the maximum alignment requirement of any C data type. */
 #undef MAXIMUM_ALIGNOF
 
@@ -766,9 +766,6 @@
 /* Define to 1 to build with ZSTD support. (--with-zstd) */
 #undef USE_ZSTD
 
-/* Define to 1 if `wcstombs_l' requires <xlocale.h>. */
-#undef WCSTOMBS_L_IN_XLOCALE
-
 /* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
    significant byte first (like Motorola and SPARC, unlike Intel). */
 #if defined AC_APPLE_UNIVERSAL_BUILD
diff --git a/src/include/port.h b/src/include/port.h
index c740005267..26a78320f8 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -15,6 +15,11 @@
 
 #include <ctype.h>
 
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
 /*
  * Windows has enough specialized port stuff that we push most of it off
  * into another file.
@@ -217,6 +222,30 @@ extern int	pg_fprintf(FILE *stream, const char *fmt,...) pg_attribute_printf(2,
 extern int	pg_vprintf(const char *fmt, va_list args) pg_attribute_printf(1, 0);
 extern int	pg_printf(const char *fmt,...) pg_attribute_printf(1, 2);
 
+/* Acessor for a thread-safe process-lifetime "C" locale. */
+extern locale_t pg_get_c_locale(void);
+
+#ifndef HAVE_STRTOD_L
+/*
+ * POSIX doesn't define this function, but we can implement it with thread-safe
+ * save-and-restore.  Not all systems have uselocale(), but the ones that don't
+ * have strtod_l().
+ */
+static inline double
+strtod_l(const char *nptr, char **endptr, locale_t loc)
+{
+#ifdef WIN32
+	return _strtod_l(nptr, endptr, loc);
+#else
+	locale_t	save = uselocale(loc);
+	double		result = strtod(nptr, endptr);
+
+	uselocale(save);
+	return result;
+#endif
+}
+#endif
+
 #ifndef WIN32
 /*
  * We add a pg_ prefix as a warning that the Windows implementations have the
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index f41d33975b..9651f3c3ab 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -12,7 +12,7 @@
 #ifndef _PG_LOCALE_
 #define _PG_LOCALE_
 
-#if defined(LOCALE_T_IN_XLOCALE) || defined(WCSTOMBS_L_IN_XLOCALE)
+#ifdef HAVE_XLOCALE_H
 #include <xlocale.h>
 #endif
 #ifdef USE_ICU
diff --git a/src/interfaces/ecpg/ecpglib/connect.c b/src/interfaces/ecpg/ecpglib/connect.c
index 8afb1f0a26..eac1a2e9b5 100644
--- a/src/interfaces/ecpg/ecpglib/connect.c
+++ b/src/interfaces/ecpg/ecpglib/connect.c
@@ -10,10 +10,6 @@
 #include "ecpgtype.h"
 #include "sqlca.h"
 
-#ifdef HAVE_USELOCALE
-locale_t	ecpg_clocale = (locale_t) 0;
-#endif
-
 static pthread_mutex_t connections_mutex = PTHREAD_MUTEX_INITIALIZER;
 static pthread_key_t actual_connection_key;
 static pthread_once_t actual_connection_key_once = PTHREAD_ONCE_INIT;
@@ -483,39 +479,6 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
 	/* add connection to our list */
 	pthread_mutex_lock(&connections_mutex);
 
-	/*
-	 * ... but first, make certain we have created ecpg_clocale.  Rely on
-	 * holding connections_mutex to ensure this is done by only one thread.
-	 */
-#ifdef HAVE_USELOCALE
-	if (!ecpg_clocale)
-	{
-		ecpg_clocale = newlocale(LC_NUMERIC_MASK, "C", (locale_t) 0);
-		if (!ecpg_clocale)
-		{
-			pthread_mutex_unlock(&connections_mutex);
-			ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
-					   ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
-			if (host)
-				ecpg_free(host);
-			if (port)
-				ecpg_free(port);
-			if (options)
-				ecpg_free(options);
-			if (realname)
-				ecpg_free(realname);
-			if (dbname)
-				ecpg_free(dbname);
-			if (conn_keywords)
-				ecpg_free(conn_keywords);
-			if (conn_values)
-				ecpg_free(conn_values);
-			free(this);
-			return false;
-		}
-	}
-#endif
-
 	if (connection_name != NULL)
 		this->name = ecpg_strdup(connection_name, lineno);
 	else
diff --git a/src/interfaces/ecpg/ecpglib/data.c b/src/interfaces/ecpg/ecpglib/data.c
index fa56276758..228df49052 100644
--- a/src/interfaces/ecpg/ecpglib/data.c
+++ b/src/interfaces/ecpg/ecpglib/data.c
@@ -466,7 +466,7 @@ ecpg_get_data(const PGresult *results, int act_tuple, int act_field, int lineno,
 						pval++;
 
 					if (!check_special_value(pval, &dres, &scan_length))
-						dres = strtod(pval, &scan_length);
+						dres = strtod_l(pval, &scan_length, pg_get_c_locale());
 
 					if (isarray && *scan_length == '"')
 						scan_length++;
diff --git a/src/interfaces/ecpg/ecpglib/descriptor.c b/src/interfaces/ecpg/ecpglib/descriptor.c
index ad279e245c..855322e037 100644
--- a/src/interfaces/ecpg/ecpglib/descriptor.c
+++ b/src/interfaces/ecpg/ecpglib/descriptor.c
@@ -475,46 +475,9 @@ ECPGget_desc(int lineno, const char *desc_name, int index,...)
 		memset(&stmt, 0, sizeof stmt);
 		stmt.lineno = lineno;
 
-		/* Make sure we do NOT honor the locale for numeric input */
-		/* since the database gives the standard decimal point */
-		/* (see comments in execute.c) */
-#ifdef HAVE_USELOCALE
-
-		/*
-		 * To get here, the above PQnfields() test must have found nonzero
-		 * fields.  One needs a connection to create such a descriptor.  (EXEC
-		 * SQL SET DESCRIPTOR can populate the descriptor's "items", but it
-		 * can't change the descriptor's PQnfields().)  Any successful
-		 * connection initializes ecpg_clocale.
-		 */
-		Assert(ecpg_clocale);
-		stmt.oldlocale = uselocale(ecpg_clocale);
-#else
-#ifdef HAVE__CONFIGTHREADLOCALE
-		stmt.oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
-#endif
-		stmt.oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
-		setlocale(LC_NUMERIC, "C");
-#endif
-
 		/* desperate try to guess something sensible */
 		stmt.connection = ecpg_get_connection(NULL);
 		ecpg_store_result(ECPGresult, index, &stmt, &data_var);
-
-#ifdef HAVE_USELOCALE
-		if (stmt.oldlocale != (locale_t) 0)
-			uselocale(stmt.oldlocale);
-#else
-		if (stmt.oldlocale)
-		{
-			setlocale(LC_NUMERIC, stmt.oldlocale);
-			ecpg_free(stmt.oldlocale);
-		}
-#ifdef HAVE__CONFIGTHREADLOCALE
-		if (stmt.oldthreadlocale != -1)
-			(void) _configthreadlocale(stmt.oldthreadlocale);
-#endif
-#endif
 	}
 	else if (data_var.ind_type != ECPGt_NO_INDICATOR && data_var.ind_pointer != NULL)
 
diff --git a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
index 01b4309a71..40988d5357 100644
--- a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
+++ b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
@@ -13,9 +13,6 @@
 #ifndef CHAR_BIT
 #include <limits.h>
 #endif
-#ifdef LOCALE_T_IN_XLOCALE
-#include <xlocale.h>
-#endif
 
 enum COMPAT_MODE
 {
@@ -59,10 +56,6 @@ struct ECPGtype_information_cache
 	enum ARRAY_TYPE isarray;
 };
 
-#ifdef HAVE_USELOCALE
-extern locale_t ecpg_clocale;	/* LC_NUMERIC=C */
-#endif
-
 /* structure to store one statement */
 struct statement
 {
@@ -76,14 +69,6 @@ struct statement
 	bool		questionmarks;
 	struct variable *inlist;
 	struct variable *outlist;
-#ifdef HAVE_USELOCALE
-	locale_t	oldlocale;
-#else
-	char	   *oldlocale;
-#ifdef HAVE__CONFIGTHREADLOCALE
-	int			oldthreadlocale;
-#endif
-#endif
 	int			nparams;
 	char	  **paramvalues;
 	int		   *paramlengths;
diff --git a/src/interfaces/ecpg/ecpglib/execute.c b/src/interfaces/ecpg/ecpglib/execute.c
index 04d0b40c53..806d4779fa 100644
--- a/src/interfaces/ecpg/ecpglib/execute.c
+++ b/src/interfaces/ecpg/ecpglib/execute.c
@@ -101,9 +101,6 @@ free_statement(struct statement *stmt)
 	free_variable(stmt->outlist);
 	ecpg_free(stmt->command);
 	ecpg_free(stmt->name);
-#ifndef HAVE_USELOCALE
-	ecpg_free(stmt->oldlocale);
-#endif
 	ecpg_free(stmt);
 }
 
@@ -1973,40 +1970,6 @@ ecpg_do_prologue(int lineno, const int compat, const int force_indicator,
 	if (stmt == NULL)
 		return false;
 
-	/*
-	 * Make sure we do NOT honor the locale for numeric input/output since the
-	 * database wants the standard decimal point.  If available, use
-	 * uselocale() for this because it's thread-safe.  Windows doesn't have
-	 * that, but it usually does have _configthreadlocale().  In some versions
-	 * of MinGW, _configthreadlocale() exists but always returns -1 --- so
-	 * treat that situation as if the function doesn't exist.
-	 */
-#ifdef HAVE_USELOCALE
-
-	/*
-	 * Since ecpg_init() succeeded, we have a connection.  Any successful
-	 * connection initializes ecpg_clocale.
-	 */
-	Assert(ecpg_clocale);
-	stmt->oldlocale = uselocale(ecpg_clocale);
-	if (stmt->oldlocale == (locale_t) 0)
-	{
-		ecpg_do_epilogue(stmt);
-		return false;
-	}
-#else
-#ifdef HAVE__CONFIGTHREADLOCALE
-	stmt->oldthreadlocale = _configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
-#endif
-	stmt->oldlocale = ecpg_strdup(setlocale(LC_NUMERIC, NULL), lineno);
-	if (stmt->oldlocale == NULL)
-	{
-		ecpg_do_epilogue(stmt);
-		return false;
-	}
-	setlocale(LC_NUMERIC, "C");
-#endif
-
 	/*
 	 * If statement type is ECPGst_prepnormal we are supposed to prepare the
 	 * statement before executing them
@@ -2213,24 +2176,6 @@ ecpg_do_epilogue(struct statement *stmt)
 	if (stmt == NULL)
 		return;
 
-#ifdef HAVE_USELOCALE
-	if (stmt->oldlocale != (locale_t) 0)
-		uselocale(stmt->oldlocale);
-#else
-	if (stmt->oldlocale)
-		setlocale(LC_NUMERIC, stmt->oldlocale);
-#ifdef HAVE__CONFIGTHREADLOCALE
-
-	/*
-	 * This is a bit trickier than it looks: if we failed partway through
-	 * statement initialization, oldthreadlocale could still be 0.  But that's
-	 * okay because a call with 0 is defined to be a no-op.
-	 */
-	if (stmt->oldthreadlocale != -1)
-		(void) _configthreadlocale(stmt->oldthreadlocale);
-#endif
-#endif
-
 	free_statement(stmt);
 }
 
diff --git a/src/interfaces/ecpg/pgtypeslib/dt_common.c b/src/interfaces/ecpg/pgtypeslib/dt_common.c
index ed08088bfe..6aff989e0f 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt_common.c
+++ b/src/interfaces/ecpg/pgtypeslib/dt_common.c
@@ -1216,7 +1216,7 @@ DecodeNumber(int flen, char *str, int fmask,
 			return DecodeNumberField(flen, str, (fmask | DTK_DATE_M),
 									 tmask, tm, fsec, is2digits);
 
-		*fsec = strtod(cp, &cp);
+		*fsec = strtod_l(cp, &cp, pg_get_c_locale());
 		if (*cp != '\0')
 			return -1;
 	}
@@ -2028,7 +2028,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		frac;
 
-								frac = strtod(cp, &cp);
+								frac = strtod_l(cp, &cp, pg_get_c_locale());
 								if (*cp != '\0')
 									return -1;
 								*fsec = frac * 1000000;
@@ -2052,7 +2052,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		time;
 
-								time = strtod(cp, &cp);
+								time = strtod_l(cp, &cp, pg_get_c_locale());
 								if (*cp != '\0')
 									return -1;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c
index 936a688381..8fc34acf68 100644
--- a/src/interfaces/ecpg/pgtypeslib/interval.c
+++ b/src/interfaces/ecpg/pgtypeslib/interval.c
@@ -60,7 +60,7 @@ ParseISO8601Number(const char *str, char **endptr, int *ipart, double *fpart)
 	if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.'))
 		return DTERR_BAD_FORMAT;
 	errno = 0;
-	val = strtod(str, endptr);
+	val = strtod_l(str, endptr, pg_get_c_locale());
 	/* did we not see anything that looks like a double? */
 	if (*endptr == str || errno != 0)
 		return DTERR_BAD_FORMAT;
@@ -455,7 +455,7 @@ DecodeInterval(char **field, int *ftype, int nf,	/* int range, */
 				else if (*cp == '.')
 				{
 					errno = 0;
-					fval = strtod(cp, &cp);
+					fval = strtod_l(cp, &cp, pg_get_c_locale());
 					if (*cp != '\0' || errno != 0)
 						return DTERR_BAD_FORMAT;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/numeric.c b/src/interfaces/ecpg/pgtypeslib/numeric.c
index 35e7b92da4..f53fdb9f10 100644
--- a/src/interfaces/ecpg/pgtypeslib/numeric.c
+++ b/src/interfaces/ecpg/pgtypeslib/numeric.c
@@ -1455,7 +1455,7 @@ numericvar_to_double(numeric *var, double *dp)
 	 * strtod does not reset errno to 0 in case of success.
 	 */
 	errno = 0;
-	val = strtod(tmp, &endptr);
+	val = strtod_l(tmp, &endptr, pg_get_c_locale());
 	if (errno == ERANGE)
 	{
 		free(tmp);
diff --git a/src/port/Makefile b/src/port/Makefile
index db7c02117b..03b55bd26b 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	bsearch_arg.o \
 	chklocale.o \
 	inet_net_ntop.o \
+	locale.o \
 	noblock.o \
 	path.o \
 	pg_bitutils.o \
diff --git a/src/port/locale.c b/src/port/locale.c
new file mode 100644
index 0000000000..ca463257ef
--- /dev/null
+++ b/src/port/locale.c
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * locale.c
+ *		Helper routines for thread-safe system locale usage.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/port/locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "c.h"
+
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
+#ifndef WIN32
+#include <pthread.h>
+#else
+#include <synchapi.h>
+#endif
+
+static locale_t c_locale;
+
+#ifndef WIN32
+static void
+init_c_locale_once(void)
+{
+	c_locale = newlocale(LC_ALL, "C", NULL);
+}
+#else
+static BOOL
+init_c_locale_once(PINIT_ONCE once, PVOID parameter, PVOID *context)
+{
+	c_locale = _create_locale(LC_ALL, "C");
+	return true;
+}
+#endif
+
+locale_t
+pg_get_c_locale(void)
+{
+	/*
+	 * Fast path if already initialized.  This assumes that we can read a
+	 * locale_t (in practice, a pointer) without tearing in a multi-threaded
+	 * program.
+	 */
+	if (c_locale != (locale_t) 0)
+		return c_locale;
+
+	/* Make a locale_t.  It will live until process exit. */
+	{
+#ifndef WIN32
+		static pthread_once_t once = PTHREAD_ONCE_INIT;
+
+		pthread_once(&once, init_c_locale_once);
+#else
+		static INIT_ONCE once;
+		InitOnceExecuteOnce(&once, init_c_locale_once, NULL, NULL);
+#endif
+	}
+
+	Assert(c_locale != (locale_t) 0);
+
+	return c_locale;
+}
diff --git a/src/port/meson.build b/src/port/meson.build
index ff54b7b53e..e21961fd73 100644
--- a/src/port/meson.build
+++ b/src/port/meson.build
@@ -5,6 +5,7 @@ pgport_sources = [
   'chklocale.c',
   'inet_net_ntop.c',
   'noblock.c',
+  'locale.c',
   'path.c',
   'pg_bitutils.c',
   'pg_strong_random.c',
diff --git a/src/port/snprintf.c b/src/port/snprintf.c
index 884f0262dd..c122e0deb7 100644
--- a/src/port/snprintf.c
+++ b/src/port/snprintf.c
@@ -35,6 +35,7 @@
 
 #include <math.h>
 
+
 /*
  * We used to use the platform's NL_ARGMAX here, but that's a bad idea,
  * first because the point of this module is to remove platform dependencies
@@ -109,6 +110,15 @@
 #undef	vprintf
 #undef	printf
 
+#ifdef WIN32
+/*
+ * This is only good enough to claim support for the usage we make in this
+ * module, so keep this claim of HAVE_SNPRINTF_L local.
+ */
+#define snprintf_l(str, size, loc, format, ...) _snprintf_l(str, size, format, loc, __VA_ARGS__)
+#define HAVE_SNPRINTF_L
+#endif
+
 /*
  * Info about where the formatted output is going.
  *
@@ -1184,6 +1194,9 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 		 * according to == but not according to memcmp.
 		 */
 		static const double dzero = 0.0;
+#ifndef HAVE_SNPRINTF_L
+		locale_t	save_locale = uselocale(pg_get_c_locale());
+#endif
 
 		if (adjust_sign((value < 0.0 ||
 						 (value == 0.0 &&
@@ -1205,7 +1218,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '*';
 			fmt[3] = type;
 			fmt[4] = '\0';
+#ifdef HAVE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), pg_get_c_locale(), fmt, prec, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, prec, value);
+#endif
 		}
 		else
 		{
@@ -1214,6 +1231,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '\0';
 			vallen = snprintf(convert, sizeof(convert), fmt, value);
 		}
+
+#ifndef HAVE_SNPRINTF_L
+		uselocale(save_locale);
+#endif
+
 		if (vallen < 0)
 			goto fail;
 
@@ -1336,12 +1358,23 @@ pg_strfromd(char *str, size_t count, int precision, double value)
 		}
 		else
 		{
+#ifndef HAVE_SNPRINTF_L
+			locale_t	save_locale;
+#endif
+
 			fmt[0] = '%';
 			fmt[1] = '.';
 			fmt[2] = '*';
 			fmt[3] = 'g';
 			fmt[4] = '\0';
+#ifdef HAVE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), pg_get_c_locale(), fmt, precision, value);
+#else
+			save_locale = uselocale(pg_get_c_locale());
 			vallen = snprintf(convert, sizeof(convert), fmt, precision, value);
+			uselocale(save_locale);
+#endif
+
 			if (vallen < 0)
 			{
 				target.failed = true;
-- 
2.39.2

---

From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 15:48:45 +1200

On Sat, Aug 10, 2024 at 1:29 PM Thomas Munro <[email-masked]> wrote:
> Here is a new attempt at this can of portability worms.

Slightly better version:

* it's OK to keep relying on the global locale in the backend; for
now, we know that LC_NUMERIC is set in main(), and in the
multi-threaded future calling setlocale() even transiently will be
banned, so it seems it'll be OK to just keep doing that, right?

* we could use LC_C_LOCALE to get a "C" locale slightly more
efficiently on those; we could define it ourselves for other systems,
using pg_get_c_locale()

---

From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 15:52:23 +1200

On Sat, Aug 10, 2024 at 3:48 PM Thomas Munro <[email-masked]> wrote:
> efficiently on those

Oops, lost some words, I meant "on those systems that have them (macOS
and NetBSD AFAIK)"

---

From: Thomas Munro <[email-masked]>
Date: Sun, 11 Aug 2024 10:11:00 +1200

v4 adds error handling, in case newlocale("C") fails.  I created CF
entry #5166 for this.
From 3543cc04b0d66c7f83bdccbb247eedd93cdea4af Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 11:01:21 +1200
Subject: [PATCH v4] Improve locale thread safety of ECPG.

Instead, use the C locale explicitly.  Provide the name PG_C_LOCALE for
this purpose.  On a couple of systems this maps to a system-provided
LC_C_LOCALE (a non-standard extension to POSIX), and otherwise we
provide a thread-safe singleton constructor that allocates a locale_t
for (LC_ALL, "C") for the lifetime of the process.

1.  Use strtod_l(..., PG_C_LOCALE) for parsing floats, and supply an
implementation of that common but non-standard extension with
standard uselocale() + strtod() if it is missing.

2.  Inside our own snprintf.c, in front-end code only, use non-standard
snprintf_l() where we punt floating point numbers to the system
snprintf(), or wrap it in uselocale() if not available.

Discussion: https://postgr.es/m/CWZBBRR6YA8D.8EHMDRGLCKCD%40neon.tech
---
 config/c-library.m4                          |  55 ---------
 configure                                    | 115 +------------------
 configure.ac                                 |   6 +-
 meson.build                                  |  37 +-----
 src/include/pg_config.h.in                   |  15 +--
 src/include/port.h                           |  42 +++++++
 src/include/utils/pg_locale.h                |   2 +-
 src/interfaces/ecpg/ecpglib/connect.c        |  39 +------
 src/interfaces/ecpg/ecpglib/data.c           |   2 +-
 src/interfaces/ecpg/ecpglib/descriptor.c     |  37 ------
 src/interfaces/ecpg/ecpglib/ecpglib_extern.h |  15 ---
 src/interfaces/ecpg/ecpglib/execute.c        |  55 ---------
 src/interfaces/ecpg/pgtypeslib/dt_common.c   |   6 +-
 src/interfaces/ecpg/pgtypeslib/interval.c    |   4 +-
 src/interfaces/ecpg/pgtypeslib/numeric.c     |   2 +-
 src/port/Makefile                            |   1 +
 src/port/locale.c                            |  78 +++++++++++++
 src/port/meson.build                         |   1 +
 src/port/snprintf.c                          |  53 +++++++++
 19 files changed, 196 insertions(+), 369 deletions(-)
 create mode 100644 src/port/locale.c

index aa8223d2ef0..421bc612b27 100644
index 4f3aa447566..298492ec251 100755

 
-fi

 
index 049bc014911..302aadd427d 100644
 
-PGAC_TYPE_LOCALE_T
-
 
 PGAC_VAR_INT_TIMEZONE
-PGAC_FUNC_WCSTOMBS_L

index cc176f11b5d..932334ed634 100644

 
-
index 0e9b108e667..c0586edab13 100644

@@ -421,6 +424,9 @@

@@ -556,9 +562,6 @@

@@ -577,9 +580,6 @@

@@ -766,9 +766,6 @@
 
index c7400052675..8e814700e7f 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -15,6 +15,11 @@
 
 #include <ctype.h>
 
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
 /*
  * Windows has enough specialized port stuff that we push most of it off
  * into another file.
@@ -217,6 +222,43 @@ extern int	pg_fprintf(FILE *stream, const char *fmt,...) pg_attribute_printf(2,
 extern int	pg_vprintf(const char *fmt, va_list args) pg_attribute_printf(1, 0);
 extern int	pg_printf(const char *fmt,...) pg_attribute_printf(1, 2);
 
+/* Thread-safe singleton constructor for process-lifetime "C" locale. */
+extern locale_t pg_get_c_locale(void);
+
+/*
+ * A couple of systems offer a pre-defined locale_t value for the "C" locale.
+ * Otherwise fall back to the above function.  Provide a way to check that a
+ * "C" locale has been allocated at a convenient time for error reporting.
+ */
+#ifdef LC_C_LOCALE
+#define PG_C_LOCALE LC_C_LOCALE
+#define pg_ensure_c_locale() true
+#else
+#define PG_C_LOCALE pg_get_c_locale()
+#define pg_ensure_c_locale() (pg_get_c_locale() != (locale_t) 0)
+#endif
+
+#ifndef HAVE_STRTOD_L
+/*
+ * POSIX doesn't define this function, but we can implement it with thread-safe
+ * save-and-restore.  Not all systems have uselocale(), but the ones that don't
+ * have strtod_l().
+ */
+static inline double
+strtod_l(const char *nptr, char **endptr, locale_t loc)
+{
+#ifdef WIN32
+	return _strtod_l(nptr, endptr, loc);
+#else
+	locale_t	save = uselocale(loc);
+	double		result = strtod(nptr, endptr);
+
+	uselocale(save);
+	return result;
+#endif
+}
+#endif
+
 #ifndef WIN32
 /*
  * We add a pg_ prefix as a warning that the Windows implementations have the
diff --git a/src/include/utils/pg_locale.h b/src/include/utils/pg_locale.h
index f41d33975be..9651f3c3ab5 100644
--- a/src/include/utils/pg_locale.h
+++ b/src/include/utils/pg_locale.h
@@ -12,7 +12,7 @@
 #ifndef _PG_LOCALE_
 #define _PG_LOCALE_
 
index 8afb1f0a26f..f4ba5b722eb 100644
 
-#ifdef HAVE_USELOCALE
-locale_t	ecpg_clocale = (locale_t) 0;
-#endif
-
 static pthread_mutex_t connections_mutex = PTHREAD_MUTEX_INITIALIZER;
 static pthread_key_t actual_connection_key;
 static pthread_once_t actual_connection_key_once = PTHREAD_ONCE_INIT;
@@ -268,7 +264,7 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
 	const char **conn_keywords;
 	const char **conn_values;
 
-	if (sqlca == NULL)
+	if (sqlca == NULL || !pg_ensure_c_locale())
 	{
 		ecpg_raise(lineno, ECPG_OUT_OF_MEMORY,
 				   ECPG_SQLSTATE_ECPG_OUT_OF_MEMORY, NULL);
@@ -483,39 +479,6 @@ ECPGconnect(int lineno, int c, const char *name, const char *user, const char *p
 	/* add connection to our list */
 	pthread_mutex_lock(&connections_mutex);
 
-	/*
index fa562767585..856f4c9472d 100644
 
 					if (!check_special_value(pval, &dres, &scan_length))
-						dres = strtod(pval, &scan_length);
+						dres = strtod_l(pval, &scan_length, PG_C_LOCALE);
 
index ad279e245c4..855322e037f 100644

index 01b4309a710..40988d53575 100644
 
 enum COMPAT_MODE
 {
 
-#ifdef HAVE_USELOCALE
index 04d0b40c537..806d4779fa9 100644

-	/*
 
-#ifdef HAVE_USELOCALE
 
index ed08088bfe1..92459728bf4 100644
 
-		*fsec = strtod(cp, &cp);
+		*fsec = strtod_l(cp, &cp, PG_C_LOCALE);
 		if (*cp != '\0')
 			return -1;
 	}
@@ -2028,7 +2028,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		frac;
 
-								frac = strtod(cp, &cp);
+								frac = strtod_l(cp, &cp, PG_C_LOCALE);
 								if (*cp != '\0')
 									return -1;
 								*fsec = frac * 1000000;
@@ -2052,7 +2052,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		time;
 
-								time = strtod(cp, &cp);
+								time = strtod_l(cp, &cp, PG_C_LOCALE);
 								if (*cp != '\0')
 									return -1;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c
index 936a6883816..155c6cc7770 100644
--- a/src/interfaces/ecpg/pgtypeslib/interval.c
+++ b/src/interfaces/ecpg/pgtypeslib/interval.c
@@ -60,7 +60,7 @@ ParseISO8601Number(const char *str, char **endptr, int *ipart, double *fpart)
 	if (!(isdigit((unsigned char) *str) || *str == '-' || *str == '.'))
 		return DTERR_BAD_FORMAT;
 	errno = 0;
-	val = strtod(str, endptr);
+	val = strtod_l(str, endptr, PG_C_LOCALE);
 	/* did we not see anything that looks like a double? */
 	if (*endptr == str || errno != 0)
 		return DTERR_BAD_FORMAT;
@@ -455,7 +455,7 @@ DecodeInterval(char **field, int *ftype, int nf,	/* int range, */
 				else if (*cp == '.')
 				{
 					errno = 0;
-					fval = strtod(cp, &cp);
+					fval = strtod_l(cp, &cp, PG_C_LOCALE);
 					if (*cp != '\0' || errno != 0)
 						return DTERR_BAD_FORMAT;
 
diff --git a/src/interfaces/ecpg/pgtypeslib/numeric.c b/src/interfaces/ecpg/pgtypeslib/numeric.c
index 35e7b92da40..49938543d03 100644
--- a/src/interfaces/ecpg/pgtypeslib/numeric.c
+++ b/src/interfaces/ecpg/pgtypeslib/numeric.c
@@ -1455,7 +1455,7 @@ numericvar_to_double(numeric *var, double *dp)
 	 * strtod does not reset errno to 0 in case of success.
 	 */
 	errno = 0;
-	val = strtod(tmp, &endptr);
+	val = strtod_l(tmp, &endptr, PG_C_LOCALE);
 	if (errno == ERANGE)
 	{
 		free(tmp);
diff --git a/src/port/Makefile b/src/port/Makefile
index db7c02117b0..03b55bd26b0 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	bsearch_arg.o \
 	chklocale.o \
 	inet_net_ntop.o \
+	locale.o \
 	noblock.o \
 	path.o \
 	pg_bitutils.o \
diff --git a/src/port/locale.c b/src/port/locale.c
new file mode 100644
index 00000000000..a5b0648bb8b
--- /dev/null
+++ b/src/port/locale.c
@@ -0,0 +1,78 @@
+/*-------------------------------------------------------------------------
+ *
+ * locale.c
+ *		Helper routines for thread-safe system locale usage.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/port/locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "c.h"
+
+#include <locale.h>
+#ifdef HAVE_XLOCALE_H
+#include <xlocale.h>
+#endif
+
+#ifndef WIN32
+#include <pthread.h>
+#else
+#include <synchapi.h>
+#endif
+
+static locale_t c_locale;
+
+#ifndef WIN32
+static void
+init_c_locale_once(void)
+{
+	c_locale = newlocale(LC_ALL, "C", NULL);
+}
+#else
+static BOOL
+init_c_locale_once(PINIT_ONCE once, PVOID parameter, PVOID *context)
+{
+	c_locale = _create_locale(LC_ALL, "C");
+	return true;
+}
+#endif
+
+locale_t
+pg_get_c_locale(void)
+{
+	/*
+	 * Fast path if already initialized.  This assumes that we can read a
+	 * locale_t (in practice, a pointer) without tearing in a multi-threaded
+	 * program.
+	 */
+	if (c_locale != (locale_t) 0)
+		return c_locale;
+
+	/* Make a locale_t.  It will live until process exit. */
+	{
+#ifndef WIN32
+		static pthread_once_t once = PTHREAD_ONCE_INIT;
+
+		pthread_once(&once, init_c_locale_once);
+#else
+		static INIT_ONCE once;
+		InitOnceExecuteOnce(&once, init_c_locale_once, NULL, NULL);
+#endif
+	}
+
+	/*
+	 * It is remotely possible that the locale couldn't be allocated, due to
+	 * lack of memory.  In that case (locale_t) 0 will be returned.  A caller
+	 * can test for that condition with pg_ensure_c_locale() at a convenient
+	 * time for error reporting.
+	 */
+
+	return c_locale;
+}
diff --git a/src/port/meson.build b/src/port/meson.build
index ff54b7b53e9..e21961fd739 100644
--- a/src/port/meson.build
+++ b/src/port/meson.build
@@ -5,6 +5,7 @@ pgport_sources = [
   'chklocale.c',
   'inet_net_ntop.c',
   'noblock.c',
+  'locale.c',
   'path.c',
   'pg_bitutils.c',
   'pg_strong_random.c',
diff --git a/src/port/snprintf.c b/src/port/snprintf.c
index 884f0262dd1..983d39a7729 100644
--- a/src/port/snprintf.c
+++ b/src/port/snprintf.c
@@ -109,6 +109,34 @@
 #undef	vprintf
 #undef	printf
 
+#if defined(FRONTEND)
+/*
+ * Frontend code might be multi-threaded.  When calling the system snprintf()
+ * for floats, we have to use either the snprintf_l() variant, or
+ * save-and-restore the calling thread's locale using uselocale().
+ */
+#if defined(HAVE_SNPRINTF_L)
+/* At least macOS and the BSDs have the snprintf_l() extension. */
+#define USE_SNPRINTF_L
+#elif defined(WIN32)
+/* Windows has a version with a different name and argument order. */
+#define snprintf_l(str, size, loc, format, ...) _snprintf_l(str, size, format, loc, __VA_ARGS__)
+#define USE_SNPRINTF_L
+#else
+/* We have to do a thread-safe save-and-restore around snprintf(). */
+#define NEED_USE_LOCALE
+#endif
+#else
+/*
+ * Backend code doesn't need to worry about the locales, because LC_NUMERIC is
+ * set to "C" in main() and doesn't change.  Plain old snprintf() is always OK.
+ *
+ * XXX If the backend were multithreaded, it would have to be 100% certain that
+ * no one is calling setlocale() concurrently, even transiently, to be able to
+ * keep this small optimization.
+ */
+#endif
+
 /*
  * Info about where the formatted output is going.
  *
@@ -1184,6 +1212,9 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 		 * according to == but not according to memcmp.
 		 */
 		static const double dzero = 0.0;
+#ifdef NEED_USE_LOCALE
+		locale_t	save_locale = uselocale(PG_C_LOCALE);
+#endif
 
 		if (adjust_sign((value < 0.0 ||
 						 (value == 0.0 &&
@@ -1205,7 +1236,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '*';
 			fmt[3] = type;
 			fmt[4] = '\0';
+#ifdef USE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), PG_C_LOCALE, fmt, prec, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, prec, value);
+#endif
 		}
 		else
 		{
@@ -1214,6 +1249,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '\0';
 			vallen = snprintf(convert, sizeof(convert), fmt, value);
 		}
+
+#ifdef NEED_USE_LOCALE
+		uselocale(save_locale);
+#endif
+
 		if (vallen < 0)
 			goto fail;
 
@@ -1336,12 +1376,25 @@ pg_strfromd(char *str, size_t count, int precision, double value)
 		}
 		else
 		{
+#ifdef NEED_USE_LOCALE
+			locale_t	save_locale = uselocale(PG_C_LOCALE);
+#endif
+
 			fmt[0] = '%';
 			fmt[1] = '.';
 			fmt[2] = '*';
 			fmt[3] = 'g';
 			fmt[4] = '\0';
+#ifdef USE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), PG_C_LOCALE, fmt, precision, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, precision, value);
+#endif
+
+#ifdef NEED_USE_LOCALE
+			uselocale(save_locale);
+#endif
+
 			if (vallen < 0)
 			{
 				target.failed = true;
-- 
2.46.0

---

From: "Tristan Partin" <[email-masked]>
Date: Tue, 13 Aug 2024 18:17:31 -0500

Hey Thomas,

Thanks for picking this up. I think your patch looks really good. Are 
you familiar with gcc's function poisoning?

	#include <stdio.h>
	#pragma GCC poison puts
	
	int main(){
	#pragma GCC bless begin puts
	    puts("a");
	#pragma GCC bless end puts
	}

I wonder if we could use function poisoning to our advantage. For 
instance in ecpg, it looks like you got all of the strtod() invocations 
and replaced them with strtod_l(). Here is a patch with an example of 
what I'm talking about.

-- 
Tristan Partin
Neon (https://neon.tech)
From a76a3f2577d098f7ce5a3ed1768aca4293aedfcc Mon Sep 17 00:00:00 2001
From: Tristan Partin <[email-masked]>
Date: Tue, 13 Aug 2024 18:06:20 -0500
Subject: [PATCH v1] Poison strtod in ecpg

ecpg exclusively uses strtod_l now.

Signed-off-by: Tristan Partin <[email-masked]>
---
 src/interfaces/ecpg/ecpglib/data.c         | 1 +
 src/interfaces/ecpg/include/ecpg_poison.h  | 6 ++++++
 src/interfaces/ecpg/pgtypeslib/dt_common.c | 1 +
 src/interfaces/ecpg/pgtypeslib/interval.c  | 1 +
 src/interfaces/ecpg/pgtypeslib/numeric.c   | 1 +
 5 files changed, 10 insertions(+)
 create mode 100644 src/interfaces/ecpg/include/ecpg_poison.h

diff --git a/src/interfaces/ecpg/ecpglib/data.c b/src/interfaces/ecpg/ecpglib/data.c
index 856f4c9472d..13023fc21eb 100644
--- a/src/interfaces/ecpg/ecpglib/data.c
+++ b/src/interfaces/ecpg/ecpglib/data.c
@@ -6,6 +6,7 @@
 #include <math.h>
 
 #include "ecpgerrno.h"
+#include "ecpg_poison.h"
 #include "ecpglib.h"
 #include "ecpglib_extern.h"
 #include "ecpgtype.h"
diff --git a/src/interfaces/ecpg/include/ecpg_poison.h b/src/interfaces/ecpg/include/ecpg_poison.h
new file mode 100644
index 00000000000..97ae9d7a143
--- /dev/null
+++ b/src/interfaces/ecpg/include/ecpg_poison.h
@@ -0,0 +1,6 @@
+#ifndef _ECPG_POISON_H
+#define _ECPG_POISON_H
+
+#pragma GCC poison strtod
+
+#endif							/* !_ECPG_POISON_H */
diff --git a/src/interfaces/ecpg/pgtypeslib/dt_common.c b/src/interfaces/ecpg/pgtypeslib/dt_common.c
index 92459728bf4..2a9c5ec32c0 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt_common.c
+++ b/src/interfaces/ecpg/pgtypeslib/dt_common.c
@@ -8,6 +8,7 @@
 
 #include "common/string.h"
 #include "dt.h"
+#include "ecpg_poison.h"
 #include "pgtypes_timestamp.h"
 #include "pgtypeslib_extern.h"
 
diff --git a/src/interfaces/ecpg/pgtypeslib/interval.c b/src/interfaces/ecpg/pgtypeslib/interval.c
index 155c6cc7770..bfcc7233999 100644
--- a/src/interfaces/ecpg/pgtypeslib/interval.c
+++ b/src/interfaces/ecpg/pgtypeslib/interval.c
@@ -12,6 +12,7 @@
 
 #include "common/string.h"
 #include "dt.h"
+#include "ecpg_poison.h"
 #include "pgtypes_error.h"
 #include "pgtypes_interval.h"
 #include "pgtypeslib_extern.h"
diff --git a/src/interfaces/ecpg/pgtypeslib/numeric.c b/src/interfaces/ecpg/pgtypeslib/numeric.c
index 49938543d03..2623f362565 100644
--- a/src/interfaces/ecpg/pgtypeslib/numeric.c
+++ b/src/interfaces/ecpg/pgtypeslib/numeric.c
@@ -6,6 +6,7 @@
 #include <float.h>
 #include <limits.h>
 
+#include "ecpg_poison.h"
 #include "pgtypes_error.h"
 #include "pgtypes_numeric.h"
 #include "pgtypeslib_extern.h"
-- 
Tristan Partin
https://tristan.partin.io

---

From: Thomas Munro <[email-masked]>
Date: Thu, 15 Aug 2024 20:49:11 +1200

On Wed, Aug 14, 2024 at 11:17 AM Tristan Partin <[email-masked]> wrote:
> Thanks for picking this up. I think your patch looks really good.

Thanks for looking!

> Are

Thanks, this looks very useful.

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 28 Aug 2024 20:50:31 +0200

> v4 adds error handling, in case newlocale("C") fails.  I created CF

I took a look at this.  It was quite a complicated discussion that led 
to this, but I agree with the solution that was arrived at.

I suggest that the simplification of the xlocale.h configure tests could 
be committed separately.  This would also be useful independent of this, 
and it's a sizeable chunk of this patch.

Also, you're removing the configure test for _configthreadlocale(). 
Presumably because you're removing all the uses.  But wouldn't we need 
that back later in the backend maybe?  Or is that test even relevant 
anymore, that is, are there Windows versions that don't have it?

Adding global includes to port.h doesn't seem great.  That's not a place 
one would normally look.  We already include <locale.h> in c.h anyway, 
so it would probably be even better overall if you just added a 
conditional #include <xlocale.h> to c.h as well.

For Windows, we already have things like

#define strcoll_l _strcoll_l

in src/include/port/win32_port.h, so it would seem more sensible to add 
strtod_l to that list, instead of in port.h.

The error handling with pg_ensure_c_locale(), that's the sort of thing 
I'm afraid will be hard to test or even know how it will behave.  And it 
creates this weird coupling between pgtypeslib and ecpglib that you 
mentioned earlier.  And if there are other users of PG_C_LOCALE in the 
future, there will be similar questions about the proper initialization 
and error handling sequence.

I would consider instead making a local static variable in each function 
that needs this.  For example, numericvar_to_double() might do

{
     static locale_t c_locale;

     if (!c_locale)
     {
         c_locale = pg_get_c_locale();
         if (!c_locale)
             return -1;  /* local error reporting convention */
     }

     ...
}

This is a bit more code in total, but then you only initialize what you 
need and you can handle errors locally.

---

From: Peter Eisentraut <[email-masked]>
Date: Tue, 1 Oct 2024 08:04:07 -0400

> and it's a sizeable chunk of this patch.

To keep this moving along a bit, I have extracted this part and 
committed it separately.  I had to make a few small tweaks, e.g., there 
was no check for xlocale.h in configure.ac, and the old 
xlocale.h-including stanza could be removed from chklocale.h.  Let's see 
how this goes.

---

From: Thomas Munro <[email-masked]>
Date: Thu, 14 Nov 2024 14:54:06 +1300

> xlocale.h-including stanza could be removed from chklocale.h.  Let's see

Thanks, Peter.  That seemed to work fine, and we shouldn't have to
think about xlocale.h as a special case too much again.

I am back to working on all these cleanup patches.  Here is a rebase
of this one, with a few minor style adjustments and a new attempt to
explain the overall approach in the commit message.
From a5eebfdd0b946f64310f60fab68e67b84bb474ae Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 11:01:21 +1200
Subject: [PATCH v5] Tidy up locale thread safety in ECPG library.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Remove setlocale() and _configthreadlocal() as fallback strategy on
systems that don't have uselocale(), where ECPG tries to control
LC_NUMERIC formatting on input and output of floating point numbers.  It
was probably broken on some systems (NetBSD), and the code was also
quite messy and complicated, with obsolete configure tests (Windows).
It was also arguably broken, or at least had unstated environmental
requirements, if pgtypeslib code was called directly.

Instead, introduce PG_C_LOCALE to refer to the "C" locale as a locale_t
value.  It maps to the special constant LC_C_LOCALE when defined by libc
(macOS, NetBSD), or otherwise uses a process-lifetime locale_t that is
allocated on first use, just as ECPG previously did itself.  The new
replacement might be more widely useful.  Then change the float parsing
and printing code to pass that to _l() functions where appropriate.

Unfortunately the portability of those functions is a bit complicated.
First, many obvious and useful _l() functions are missing from POSIX,
though most standard libraries define some of them anyway.  Second,
although the thread-safe save/restore technique can be used to replace
the missing ones, Windows and NetBSD refused to implement standard
uselocale().  They might have a point: "wide scope" uselocale() is hard
to combine with other code and error-prone, especially in library code.
Luckily they have the  _l() functions we want so far anyway.  So we have
to be prepared for both ways of doing things:

1.  In ECPG, use strtod_l() for parsing, and supply a port.h replacement
using uselocale() over a limited scope if missing.

2.  Inside our own snprintf.c, use three different approaches to format
floats.  For frontend code, call libc's snprintf_l(), or wrap libc's
snprintf() in uselocale() if it's missing.  For backend code, snprintf.c
can keep assuming that the global locale's LC_NUMERIC is "C" and call
libc's snprintf() without change, for now.

(It might eventually be possible to call our in-tree Ryū routines to
display floats in snprintf.c, given the C-locale-always remit of our
in-tree snprintf(), but this patch doesn't risk changing anything that
complicated.)

Reviewed-by: Peter Eisentraut <[email-masked]>
Reviewed-by: Tristan Partin <[email-masked]>
Discussion: https://postgr.es/m/CWZBBRR6YA8D.8EHMDRGLCKCD%40neon.tech
---
 configure                                    | 13 +---
 configure.ac                                 |  3 +-
 meson.build                                  |  3 +-
 src/include/pg_config.h.in                   |  9 ++-
 src/include/port.h                           | 36 +++++++++
 src/interfaces/ecpg/ecpglib/connect.c        | 39 +---------
 src/interfaces/ecpg/ecpglib/data.c           |  2 +-
 src/interfaces/ecpg/ecpglib/descriptor.c     | 37 ---------
 src/interfaces/ecpg/ecpglib/ecpglib_extern.h | 12 ---
 src/interfaces/ecpg/ecpglib/execute.c        | 55 --------------
 src/interfaces/ecpg/pgtypeslib/dt_common.c   |  6 +-
 src/interfaces/ecpg/pgtypeslib/interval.c    |  4 +-
 src/interfaces/ecpg/pgtypeslib/numeric.c     |  2 +-
 src/port/Makefile                            |  1 +
 src/port/locale.c                            | 80 ++++++++++++++++++++
 src/port/meson.build                         |  1 +
 src/port/snprintf.c                          | 55 ++++++++++++++
 17 files changed, 192 insertions(+), 166 deletions(-)
 create mode 100644 src/port/locale.c

diff --git a/configure b/configure
index f58eae1baa8..6a133c09d3b 100755
--- a/configure
+++ b/configure
@@ -15039,7 +15039,7 @@ fi
 LIBS_including_readline="$LIBS"
 LIBS=`echo "$LIBS" | sed -e 's/-ledit//g' -e 's/-lreadline//g'`
 
@@ -15812,17 +15812,6 @@ fi
 
index 82c5009e3e8..e73e0985862 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1727,6 +1727,8 @@ AC_CHECK_FUNCS(m4_normalize([
 	pthread_is_threaded_np
 	setproctitle
 	setproctitle_fast
+	snprintf_l
+	strtod_l
 	strchrnul
 	strsignal
 	syncfs
@@ -1816,7 +1818,6 @@ fi
 
index 5b0510cef78..dc557a317df 100644
@@ -2614,7 +2614,6 @@ endif
@@ -2646,6 +2645,8 @@ func_checks = [
index cdd9a6e9355..10c05775ff2 100644
@@ -349,6 +349,9 @@

@@ -397,6 +400,9 @@

@@ -529,9 +535,6 @@

diff --git a/src/include/port.h b/src/include/port.h
index ba9ab0d34f4..f8344a1eb3d 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -217,6 +217,42 @@ extern int	pg_fprintf(FILE *stream, const char *fmt,...) pg_attribute_printf(2,
 extern int	pg_vprintf(const char *fmt, va_list args) pg_attribute_printf(1, 0);
 extern int	pg_printf(const char *fmt,...) pg_attribute_printf(1, 2);
 
+/*
+ * A couple of systems offer a fast constant locale_t value representing the
+ * "C" locale.  We use that if possible, but fall back to creating a singleton
+ * object otherwise.  To check that it is available, call pg_ensure_c_locale()
+ * and assume out of memory if it returns false.
+ */
+#ifdef LC_C_LOCALE
+#define PG_C_LOCALE LC_C_LOCALE
+#define pg_ensure_c_locale() true
+#else
+extern locale_t pg_get_c_locale(void);
+#define PG_C_LOCALE pg_get_c_locale()
+#define pg_ensure_c_locale() (PG_C_LOCALE != 0)
+#endif
+
+#ifndef HAVE_STRTOD_L
+/*
+ * POSIX doesn't define this function, but we can implement it with thread-safe
+ * save-and-restore.  Not all systems have uselocale(), but the ones that don't
+ * have strtod_l().
+ */
+static inline double
+strtod_l(const char *nptr, char **endptr, locale_t loc)
+{
+#ifdef WIN32
+	return _strtod_l(nptr, endptr, loc);
+#else
+	locale_t	save = uselocale(loc);
+	double		result = strtod(nptr, endptr);
+
+	uselocale(save);
+	return result;
+#endif
+}
+#endif
+
 #ifndef WIN32
 /*
  * We add a pg_ prefix as a warning that the Windows implementations have the
diff --git a/src/interfaces/ecpg/ecpglib/connect.c b/src/interfaces/ecpg/ecpglib/connect.c
index b24b310ce59..78e501a2656 100644
--- a/src/interfaces/ecpg/ecpglib/connect.c
+++ b/src/interfaces/ecpg/ecpglib/connect.c
@@ -10,10 +10,6 @@
 #include "ecpgtype.h"
 #include "sqlca.h"
 
-#ifdef HAVE_USELOCALE
 
-	if (sqlca == NULL)
 
-	/*

 
 
diff --git a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
index bad3cd9920b..40988d53575 100644
--- a/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
+++ b/src/interfaces/ecpg/ecpglib/ecpglib_extern.h
@@ -56,10 +56,6 @@ struct ECPGtype_information_cache
 	enum ARRAY_TYPE isarray;
 };
 
-#ifdef HAVE_USELOCALE
@@ -73,14 +69,6 @@ struct statement
index c578c21cf66..ec939d0f63a 100644

-	/*
 
-#ifdef HAVE_USELOCALE
 
diff --git a/src/interfaces/ecpg/pgtypeslib/dt_common.c b/src/interfaces/ecpg/pgtypeslib/dt_common.c
index c4119ab7932..f8349b66ce9 100644
--- a/src/interfaces/ecpg/pgtypeslib/dt_common.c
+++ b/src/interfaces/ecpg/pgtypeslib/dt_common.c
@@ -1218,7 +1218,7 @@ DecodeNumber(int flen, char *str, int fmask,
 			return DecodeNumberField(flen, str, (fmask | DTK_DATE_M),
 									 tmask, tm, fsec, is2digits);
 
-		*fsec = strtod(cp, &cp);
+		*fsec = strtod_l(cp, &cp, PG_C_LOCALE);
 		if (*cp != '\0')
 			return -1;
 	}
@@ -2030,7 +2030,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		frac;
 
-								frac = strtod(cp, &cp);
+								frac = strtod_l(cp, &cp, PG_C_LOCALE);
 								if (*cp != '\0')
 									return -1;
 								*fsec = frac * 1000000;
@@ -2054,7 +2054,7 @@ DecodeDateTime(char **field, int *ftype, int nf,
 							{
 								double		time;

 
diff --git a/src/interfaces/ecpg/pgtypeslib/numeric.c b/src/interfaces/ecpg/pgtypeslib/numeric.c
index 35e7b92da40..49938543d03 100644
--- a/src/interfaces/ecpg/pgtypeslib/numeric.c
+++ b/src/interfaces/ecpg/pgtypeslib/numeric.c
@@ -1455,7 +1455,7 @@ numericvar_to_double(numeric *var, double *dp)
 	 * strtod does not reset errno to 0 in case of success.
 	 */
 	errno = 0;
-	val = strtod(tmp, &endptr);
+	val = strtod_l(tmp, &endptr, PG_C_LOCALE);
 	if (errno == ERANGE)
 	{
 		free(tmp);
diff --git a/src/port/Makefile b/src/port/Makefile
index 366c814bd92..70d63963611 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -41,6 +41,7 @@ OBJS = \
 	bsearch_arg.o \
 	chklocale.o \
 	inet_net_ntop.o \
+	locale.o \
 	noblock.o \
 	path.o \
 	pg_bitutils.o \
diff --git a/src/port/locale.c b/src/port/locale.c
new file mode 100644
index 00000000000..387fb4a107a
--- /dev/null
+++ b/src/port/locale.c
@@ -0,0 +1,80 @@
+/*-------------------------------------------------------------------------
+ *
+ * locale.c
+ *		Helper routines for thread-safe system locale usage.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/port/locale.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "c.h"
+
+#ifndef WIN32
+#include <pthread.h>
+#else
+#include <synchapi.h>
+#endif
+
+/* A process-lifetime singleton, allocated on first need. */
+static locale_t c_locale;
+
+#ifndef WIN32
+static void
+init_c_locale_once(void)
+{
+	c_locale = newlocale(LC_ALL, "C", NULL);
+}
+#else
+static BOOL
+init_c_locale_once(PINIT_ONCE once, PVOID parameter, PVOID *context)
+{
+	c_locale = _create_locale(LC_ALL, "C");
+	return true;
+}
+#endif
+
+/*
+ * Access a process-lifetime singleton locale_t object.  Use the macro
+ * PG_C_LOCALE instead of calling this directly, as it can skip the function
+ * call on some systems.
+ */
+locale_t
+pg_get_c_locale(void)
+{
+	/*
+	 * Fast path if already initialized.  This assumes that we can read a
+	 * locale_t (in practice, a pointer) without tearing in a multi-threaded
+	 * program.
+	 */
+	if (c_locale != (locale_t) 0)
+		return c_locale;
+
+	/* Make a locale_t.  It will live until process exit. */
+	{
+#ifndef WIN32
+		static pthread_once_t once = PTHREAD_ONCE_INIT;
+
+		pthread_once(&once, init_c_locale_once);
+#else
+		static INIT_ONCE once;
+		InitOnceExecuteOnce(&once, init_c_locale_once, NULL, NULL);
+#endif
+	}
+
+	/*
+	 * It's possible that the allocation of the locale failed due to low
+	 * memory, and then (locale_t) 0 will be returned.  Users of PG_C_LOCALE
+	 * should defend against that by checking pg_ensure_c_locale() at a
+	 * convenient time, so that they can treat it as a simple constant after
+	 * that.
+	 */
+
+	return c_locale;
+}
diff --git a/src/port/meson.build b/src/port/meson.build
index 83a06325209..93f6158c307 100644
--- a/src/port/meson.build
+++ b/src/port/meson.build
@@ -5,6 +5,7 @@ pgport_sources = [
   'chklocale.c',
   'inet_net_ntop.c',
   'noblock.c',
+  'locale.c',
   'path.c',
   'pg_bitutils.c',
   'pg_popcount_avx512.c',
diff --git a/src/port/snprintf.c b/src/port/snprintf.c
index 884f0262dd1..3cc49f01ea6 100644
--- a/src/port/snprintf.c
+++ b/src/port/snprintf.c
@@ -109,6 +109,36 @@
 #undef	vprintf
 #undef	printf
 
+#if defined(FRONTEND)
+/*
+ * Frontend code might be multi-threaded.  When calling the system snprintf()
+ * for floats, we have to use either the non-standard snprintf_l() variant, or
+ * save-and-restore the calling thread's locale using uselocale(), depending on
+ * availability.
+ */
+#if defined(HAVE_SNPRINTF_L)
+/* At least macOS and the BSDs have the snprintf_l() extension. */
+#define USE_SNPRINTF_L
+#elif defined(WIN32)
+/* Windows has a version with a different name and argument order. */
+#define snprintf_l(str, size, loc, format, ...) _snprintf_l(str, size, format, loc, __VA_ARGS__)
+#define USE_SNPRINTF_L
+#else
+/* We have to do a thread-safe save-and-restore around snprintf(). */
+#define NEED_USE_LOCALE
+#endif
+#else
+/*
+ * Backend code doesn't need to worry about locales here, because LC_NUMERIC is
+ * set to "C" in main() and doesn't change.  Plain old snprintf() is always OK
+ * without uselocale().
+ *
+ * XXX If the backend were multithreaded, we would have to be 100% certain that
+ * no one is calling setlocale() concurrently, even transiently, to be able to
+ * keep this small optimization.
+ */
+#endif
+
 /*
  * Info about where the formatted output is going.
  *
@@ -1184,6 +1214,9 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 		 * according to == but not according to memcmp.
 		 */
 		static const double dzero = 0.0;
+#ifdef NEED_USE_LOCALE
+		locale_t	save_locale = uselocale(PG_C_LOCALE);
+#endif
 
 		if (adjust_sign((value < 0.0 ||
 						 (value == 0.0 &&
@@ -1205,7 +1238,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '*';
 			fmt[3] = type;
 			fmt[4] = '\0';
+#ifdef USE_SNPRINTF_L
+			vallen = snprintf_l(convert, sizeof(convert), PG_C_LOCALE, fmt, prec, value);
+#else
 			vallen = snprintf(convert, sizeof(convert), fmt, prec, value);
+#endif
 		}
 		else
 		{
@@ -1214,6 +1251,11 @@ fmtfloat(double value, char type, int forcesign, int leftjust,
 			fmt[2] = '\0';
 			vallen = snprintf(convert, sizeof(convert), fmt, value);
 		}
+
+#ifdef NEED_USE_LOCALE
+		uselocale(save_locale);
+#endif
+
 		if (vallen < 0)
 			goto fail;
 
@@ -1336,12 +1378,25 @@ pg_strfromd(char *str, size_t count, int precision, double value)
 		}
 		else
 		{
+#ifdef NEED_USE_LOCALE
2.47.0

---

From: Thomas Munro <[email-masked]>
Date: Thu, 14 Nov 2024 20:48:03 +1300

> to this, but I agree with the solution that was arrived at.

Sorry I missed/forgot this prior feedback when posting earlier today.

> anymore, that is, are there Windows versions that don't have it?

Yeah, this removes the uses.  As it happens, in the thread about
localeconv() I am also proposing to add a new user of it[1], but even
then I don't think we'd need the configure test, because all relevant
systems have it.  It's in ucrt, the modern C runtime that replaced the
old msvcrt, as used by Visual Studio 2015+ and Windows 10+, and also
by MinGW in recent years.  I think it was also in some versions of
msvcrt, but MinGW had issues with that apparently.

It's pretty confusing, so I went looking for proof and clues about the
history.  Here's a thread about 2019 animal jacana lacking the
function:

https://www.postgresql.org/message-id/flat/31420.1547783697%40sss.pgh.pa.us

Once jacana was upgraded, it started finding the function, but next
failed with -1 at runtime, cf 2cf91ccb.  Grovelling through
MinGW-w64's github, it looks like they added a dummy function that
fails with -1 in 2016:

https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-crt/misc/_configthreadlocale.c#L11

They only seem to interpose the dummy function when building against msvcrt.

https://github.com/mingw-w64/mingw-w64/blob/master/mingw-w64-crt/Makefile.am#L298

The default and recommended runtime library as of a few years ago is ucrt:

https://www.msys2.org/docs/environments/#msvcrt-vs-ucrt

The three MinGW environments we test today are using ucrt, and
configure detects the symbol on all.  Namely: fairwren
(msys2/mingw64), the CI mingw64 task and the mingw cross-build that
runs on Linux in the CI CompilerWarnings task.  As far as I know these
are the reasons for, and mechanism by which, we keep MinGW support
working.  We have no policy requiring arbitrary old MinGW systems
work, and we wouldn't know anyway.

I was paranoid that it might still be a dummy function, so I tried
running a simple program on our CI with the MinGW compiler to confirm
it's reaching the real library:

int
main()
{
    printf("hello world\n");
    printf("got %d\n", _configthreadlocale(_ENABLE_PER_THREAD_LOCALE));
    printf("got %d\n", _configthreadlocale(_DISABLE_PER_THREAD_LOCALE));
}

[06:16:39.580] c:\cirrus>call C:\msys64\usr\bin\bash.exe -l -c "gcc
doit.c -o c:/doit"
...
[06:16:41.474] c:\cirrus>call c:\doit.exe
[06:16:41.482] hello world
[06:16:41.482] got 2
[06:16:41.482] got 1

It's not clear to me whether or when we might have made any changes
already that prevented PostgreSQL from working with msvcrt, of course,
but I'm pretty sure if someone proposed that we *should* support it,
we'd reject the proposal.  That'd effectively be a different, obsolete
one corresponding to Windows versions we've dropped, which wouldn't
make any sense.

Other mentions of msvcrt in win32env.c and pg_locale.c may also be
effectively obsolete now too.

> strtod_l to that list, instead of in port.h.

OK, I moved that line into that list.  I left the implementation for
Unixen (at a guess: probably just Solaris?) there.

> and error handling sequence.

Ack.  The coupling does become at least less weird (currently it must
be capable of giving the wrong answers reliably if called directly I
think, no?) and weaker, but I acknowledge that it's still non-ideal
that out-of-memory would be handled nicely only if you used ecpg
first, and subtle misbehaviour would ensure otherwise, and future
users face the same sort of problem unless they design in a reasonable
initialisation place that could check pg_ensure_c_locale() nicely.
Classic retro-fitting problem, though.

> need and you can handle errors locally.

Hmm.  Seems like quite a lot of duplication, and also assumes that we
can actually unwind and handle the error in a reasonable way at all
callsites.  Let me think about that some more...

[1] https://www.postgresql.org/message-id/flat/[email-masked]
From 2965979a7180265a8df91adb45c1ce32f05b190f Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Sat, 10 Aug 2024 11:01:21 +1200
Subject: [PATCH v6] Tidy up locale thread safety in ECPG library.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

 src/include/port.h                           | 31 ++++++++
 src/include/port/win32_port.h                |  1 +
 18 files changed, 188 insertions(+), 166 deletions(-)

 

 
@@ -397,6 +400,9 @@

@@ -529,9 +535,6 @@

diff --git a/src/include/port.h b/src/include/port.h
index ba9ab0d34f4..48292cb28f8 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -217,6 +217,37 @@ extern int	pg_fprintf(FILE *stream, const char *fmt,...) pg_attribute_printf(2,
 extern int	pg_vprintf(const char *fmt, va_list args) pg_attribute_printf(1, 0);
 extern int	pg_printf(const char *fmt,...) pg_attribute_printf(1, 2);
 
+/*
+ * A couple of systems offer a fast constant locale_t value representing the
+ * "C" locale.  We use that if possible, but fall back to creating a singleton
+ * object otherwise.  To check that it is available, call pg_ensure_c_locale()
+ * and assume out of memory if it returns false.
+ */
+#ifdef LC_C_LOCALE
+#define PG_C_LOCALE LC_C_LOCALE
+#define pg_ensure_c_locale() true
+#else
+extern locale_t pg_get_c_locale(void);
+#define PG_C_LOCALE pg_get_c_locale()
+#define pg_ensure_c_locale() (PG_C_LOCALE != 0)
+#endif
+
+#if !defined(HAVE_STRTOD_L) && !defined(WIN32)
+/*
+ * POSIX doesn't define this function, but we can implement it with thread-safe
+ * save-and-restore.
+ */
+static inline double
+strtod_l(const char *nptr, char **endptr, locale_t loc)
+{
+	locale_t	save = uselocale(loc);
+	double		result = strtod(nptr, endptr);
+
+	uselocale(save);
+	return result;
+}
+#endif
+
 #ifndef WIN32
 /*
  * We add a pg_ prefix as a warning that the Windows implementations have the
diff --git a/src/include/port/win32_port.h b/src/include/port/win32_port.h
index 7789e0431aa..8dbde7da954 100644
--- a/src/include/port/win32_port.h
+++ b/src/include/port/win32_port.h
@@ -463,6 +463,7 @@ extern int	_pglstat64(const char *name, struct stat *buf);
 #define isspace_l _isspace_l
 #define iswspace_l _iswspace_l
 #define strcoll_l _strcoll_l
+#define strtod_l _strtod_l
 #define strxfrm_l _strxfrm_l
 #define wcscoll_l _wcscoll_l

-#ifdef HAVE_USELOCALE
 
-	if (sqlca == NULL)
 
-	/*

 

-#ifdef HAVE_USELOCALE

-	/*
 
-#ifdef HAVE_USELOCALE

 

 
 
+#if defined(FRONTEND)
+/*

---

From: Heikki Linnakangas <[email-masked]>
Date: Thu, 14 Nov 2024 11:54:29 +0200

> Classic retro-fitting problem, though.

Hmm, so should we add calls to pg_ensure_c_locale() in pgtypeslib too, 
before each call to strtod_l()?

Looking at the callers of strtod() in ecpg, all but one of them actually 
readily convert the returned value to integer, with some multiplication 
or division with constants. It would be nice to replace those with a 
function that would avoid going through double in the first place. That 
still leaves one caller in numericvar_to_double() which really wants a 
double, though

Overall, the patch looks good to me.

-- 
Heikki Linnakangas
Neon (https://neon.tech)

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 14 Nov 2024 13:53:47 +0100

> work, and we wouldn't know anyway.

Right.  So I think we could unwind this in steps.  First, remove the 
configure test for _configthreadlocale() and all the associated #ifdefs 
in the existing ecpg code.  This seems totally safe, it would just leave 
behind MinGW older than 2016 and MSVC older than 2015, the latter of 
which is already the current threshold.

Then the question whether we want to re-enable the error checking on 
_configthreadlocale() that was reverted by 2cf91ccb, or at least 
something similar.  This should also be okay based on your description 
of the different Windows runtimes.  I think it would also be good to do 
this to make sure this works before we employ _configthreadlocale() in 
higher-stakes situations.

I suggest doing these two steps as separate patches, so this doesn't get 
confused between the various thread-related threads that want to 
variously add or remove uses of this function.

---

From: Tom Lane <[email-masked]>
Date: Fri, 12 Dec 2025 13:07:08 -0500

> xlocale.h-including stanza could be removed from chklocale.h.  Let's see 

For the archives' sake --- I discovered today during a "git bisect"
session that commits between 35eeea623 ("Use thread-safe
nl_langinfo_l(), not nl_langinfo()") and 9c2a6c5a5 ("Simplify checking
for xlocale.h", the commit Peter refers to here) fail to build on
current macOS (26/Tahoe):

chklocale.c:330:8: error: call to undeclared function 'nl_langinfo_l'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
  330 |         sys = nl_langinfo_l(CODESET, loc);
      |               ^
chklocale.c:330:8: note: did you mean 'nl_langinfo'?
/Library/Developer/CommandLineTools/SDKs/MacOSX26.1.sdk/usr/include/_langinfo.h:116:20: note: 'nl_langinfo' declared here
  116 | char    *_LIBC_CSTR   nl_langinfo(nl_item);
      |                       ^
chklocale.c:330:6: error: incompatible integer to pointer conversion assigning to 'char *' from 'int' [-Wint-conversion]
  330 |         sys = nl_langinfo_l(CODESET, loc);
      |             ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 errors generated.

This happens because nl_langinfo_l() is declared in xlocale.h,
but chklocale.c elects not to #include that, evidently because
it's no longer needed to obtain typedef locale_t.

AFAICS there's nothing we can do about this retroactively;
it'll be a more or less permanent landmine for bisecting on macOS.
Fortunately it's not too difficult to work around, you can just do

diff --git a/src/port/chklocale.c b/src/port/chklocale.c
index 9506cd87ed8..0e35e0cf55f 100644
--- a/src/port/chklocale.c
+++ b/src/port/chklocale.c
@@ -23,9 +23,7 @@
 #include <langinfo.h>
 #endif

 #include "mb/pg_wchar.h"
 
when trying to build one of the affected commits.  Another option
that might fit into a bisecting workflow more easily is to inject
-DLOCALE_T_IN_XLOCALE into CPPFLAGS.

			regards, tom lane