From: Julien Marrec <[email protected]>
On targets without POSIX 2008 locales (e.g. MinGW-w64), formatted I/O saves the
pointer returned by setlocale (LC_NUMERIC, NULL) and restores through it after
setlocale (LC_NUMERIC, "C"). C11 allows that second call to overwrite the
string, and both Windows C runtimes invalidate it. msvcrt overwrites its static
buffer with "C", so a non-"C" LC_NUMERIC set by the program is reset to "C" by
the first formatted I/O statement. UCRT frees the buffer, so the restore reads
freed memory and leaves the process in an arbitrary locale or fails, and
formatted I/O becomes several times slower.
Save a copy of the name instead, and skip the switch entirely when LC_NUMERIC
is already "C", which avoids two setlocale calls per data transfer statement in
the common case.
PR libfortran/128183
libgfortran/ChangeLog:
* io/transfer.c (data_transfer_init_worker): Save a copy of the
LC_NUMERIC name, and only switch to "C" if it is not already "C".
(finalize_transfer): Only restore if a name was saved, and free it.
* io/unit.c (old_locale): Update comment.
gcc/testsuite/ChangeLog:
* gfortran.dg/pr128183.f90: New test.
* gfortran.dg/pr128183_aux.c: New file.
Signed-off-by: Julien Marrec <[email protected]>
---
Testing:
- x86_64-pc-linux-gnu, non-bootstrap C/Fortran build, with libgfortran
configured with `ac_cv_func_uselocale=no` so that it uses the setlocale
fallback. `make check-fortran` with and without the fix: no new failures.
- The new test fails without the fix on that build (glibc frees the old name
too; valgrind reports an invalid read in finalize_transfer) and passes with it.
- x86_64-w64-mingw32: transfer.c and unit.c rebuilt with the fix and swapped
into the libgfortran.a of a GCC 16.2.0 UCRT toolchain. The reproducer passes
(it fails with the stock library), and an internal WRITE goes from 6.8 us to
1.8 us. I have not run the testsuite on MinGW-w64.
Bug and reproducers: https://gcc.gnu.org/PR128183,
https://github.com/jmarrec/libgfortran-ucrt-setlocale
I don't have write access, so please push it for me if approved. Ok for trunk?
This is a forge pull request published on the gcc-patches mailing list mailing
list
as requested by jmarrec via Sourceware Forge
<[email protected]>.
Forge discussion: https://forge.sourceware.org/gcc/gcc/pulls/258
Get it locally using:
```
git fetch forge-upstream "+refs/versioned_pull/258/*:refs/versioned_pull/258/*"
git switch -c "pr-258-v1" "refs/versioned_pull/258/1/head"
```
Or, download the patch at: https://forge.sourceware.org/gcc/gcc/pulls/258.diff
Created on: 2026-10-26 17:03:38+00:00
Latest update: 2026-10-26 17:08:42+00:00
Changes: 4 changed files, 85 additions, 5 deletions
Head revision: jmarrec/gcc ref libfortran-setlocale-copy commit
7e7185cd2c0708c4cc36fe9992195af4f17bec6b
Base revision: gcc/gcc ref trunk commit
8158ffe53bc70511246588be6efe5e98431e55d9 r17-5585-g8158ffe53bc705
Merge base: 8158ffe53bc70511246588be6efe5e98431e55d9
Requested Reviewers:
Changed files:
- A: gcc/testsuite/gfortran.dg/pr128183.f90
- A: gcc/testsuite/gfortran.dg/pr128183_aux.c
- M: libgfortran/io/transfer.c
- M: libgfortran/io/unit.c
gcc/testsuite/gfortran.dg/pr128183.f90 | 34 +++++++++++++++++++++
gcc/testsuite/gfortran.dg/pr128183_aux.c | 39 ++++++++++++++++++++++++
libgfortran/io/transfer.c | 12 ++++++--
libgfortran/io/unit.c | 5 +--
4 files changed, 85 insertions(+), 5 deletions(-)
create mode 100644 gcc/testsuite/gfortran.dg/pr128183.f90
create mode 100644 gcc/testsuite/gfortran.dg/pr128183_aux.c
diff --git a/gcc/testsuite/gfortran.dg/pr128183.f90
b/gcc/testsuite/gfortran.dg/pr128183.f90
new file mode 100644
index 0000000000000..e663206936caa
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/pr128183.f90
@@ -0,0 +1,34 @@
+! { dg-do run }
+! { dg-additional-sources pr128183_aux.c }
+!
+! PR libfortran/128183
+! Formatted I/O must leave a non-"C" LC_NUMERIC unchanged. On targets without
+! uselocale (e.g. MinGW-w64), libgfortran used to restore LC_NUMERIC through a
+! pointer invalidated by its own setlocale (LC_NUMERIC, "C") call. Targets
+! with uselocale never call setlocale during I/O, so on those targets this test
+! cannot fail. The ' 1.5' check also catches a missing switch to "C" when the
+! chosen locale uses a decimal comma.
+
+program pr128183
+ use iso_c_binding, only: c_int
+ implicit none
+ interface
+ integer(c_int) function set_non_c_locale () bind(C)
+ import :: c_int
+ end function set_non_c_locale
+ integer(c_int) function locale_unchanged () bind(C)
+ import :: c_int
+ end function locale_unchanged
+ end interface
+ character(len=8) :: buf
+ integer :: i
+
+ ! Nothing to test if no locale other than "C" is installed.
+ if (set_non_c_locale () == 0) stop
+
+ do i = 1, 3
+ write (buf, '(F4.1)') 1.5
+ if (buf /= ' 1.5') stop 1
+ if (locale_unchanged () == 0) stop 2
+ end do
+end program pr128183
diff --git a/gcc/testsuite/gfortran.dg/pr128183_aux.c
b/gcc/testsuite/gfortran.dg/pr128183_aux.c
new file mode 100644
index 0000000000000..f35935d64a28a
--- /dev/null
+++ b/gcc/testsuite/gfortran.dg/pr128183_aux.c
@@ -0,0 +1,39 @@
+/* Helper for pr128183.f90. */
+
+#include <locale.h>
+#include <string.h>
+
+static char saved_name[256];
+
+/* Set LC_NUMERIC to an installed locale whose name is not "C". Windows names
+ come first, since targets without uselocale are mostly MinGW-w64. Return 0
+ if there is none. */
+
+int
+set_non_c_locale (void)
+{
+ static const char *const names[] = {
+ "French", "German", "English", "C.UTF-8", "C.utf8", NULL
+ };
+
+ for (int i = 0; names[i] != NULL; i++)
+ {
+ const char *name = setlocale (LC_NUMERIC, names[i]);
+ if (name != NULL && strcmp (name, "C") != 0
+ && strlen (name) < sizeof saved_name)
+ {
+ strcpy (saved_name, name);
+ return 1;
+ }
+ }
+ return 0;
+}
+
+/* Return 1 if LC_NUMERIC is still the locale set by set_non_c_locale. */
+
+int
+locale_unchanged (void)
+{
+ const char *name = setlocale (LC_NUMERIC, NULL);
+ return name != NULL && strcmp (name, saved_name) == 0;
+}
diff --git a/libgfortran/io/transfer.c b/libgfortran/io/transfer.c
index 95ead6936f201..5e485b4dae816 100644
--- a/libgfortran/io/transfer.c
+++ b/libgfortran/io/transfer.c
@@ -3757,8 +3757,13 @@ data_transfer_init_worker (st_parameter_dt *dtp, int
read_flag)
__gthread_mutex_lock (&old_locale_lock);
if (!old_locale_ctr++)
{
- old_locale = setlocale (LC_NUMERIC, NULL);
- setlocale (LC_NUMERIC, "C");
+ /* The next setlocale call may free this string; keep a copy. */
+ const char *cur_locale = setlocale (LC_NUMERIC, NULL);
+ if (cur_locale != NULL && strcmp (cur_locale, "C") != 0)
+ {
+ old_locale = strdup (cur_locale);
+ setlocale (LC_NUMERIC, "C");
+ }
}
__gthread_mutex_unlock (&old_locale_lock);
#endif
@@ -4598,9 +4603,10 @@ finalize_transfer (st_parameter_dt *dtp)
}
#else
__gthread_mutex_lock (&old_locale_lock);
- if (!--old_locale_ctr)
+ if (!--old_locale_ctr && old_locale != NULL)
{
setlocale (LC_NUMERIC, old_locale);
+ free (old_locale);
old_locale = NULL;
}
__gthread_mutex_unlock (&old_locale_lock);
diff --git a/libgfortran/io/unit.c b/libgfortran/io/unit.c
index ed40fd58b80e5..766628c6a39b5 100644
--- a/libgfortran/io/unit.c
+++ b/libgfortran/io/unit.c
@@ -127,8 +127,9 @@ locale_t c_locale;
traditional setlocale(). To prevent multiple concurrent threads
doing formatted I/O from messing up the locale, we need to store a
global old_locale, and a counter keeping track of how many threads
- are currently doing formatted I/O. The first thread saves the old
- locale, and the last one restores it. */
+ are currently doing formatted I/O. The first thread saves a copy of
+ the old locale name (NULL if it was already "C"), and the last one
+ restores it. */
char *old_locale;
int old_locale_ctr;
#ifdef __GTHREAD_MUTEX_INIT
--
2.54.0