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

Reply via email to