https://gcc.gnu.org/bugzilla/show_bug.cgi?id=128183

            Bug ID: 128183
           Summary: libgfortran: use-after-free of saved LC_NUMERIC name
                    in formatted I/O on UCRT (no POSIX 2008 locales)
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libfortran
          Assignee: unassigned at gcc dot gnu.org
          Reporter: julien.marrec at gmail dot com
  Target Milestone: ---

Created attachment 65969
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65969&action=edit
Fortran MCVE

On targets without HAVE_POSIX_2008_LOCALE (e.g. x86_64-w64-mingw32),
libgfortran wraps every formatted data transfer in a global LC_NUMERIC switch
(libgfortran/io/transfer.c):

    /* data_transfer_init_worker */
    old_locale = setlocale (LC_NUMERIC, NULL);   /* pointer kept, no copy */
    setlocale (LC_NUMERIC, "C");
    ...
    /* finalize_transfer */
    setlocale (LC_NUMERIC, old_locale);          /* restore through it */

C11 7.11.1.1p8 (https://port70.net/~nsz/c/c11/n1570.html#7.11.1.1p8):

    The pointer to string returned by the setlocale function is such that a
    subsequent call with that string value and its associated category will
    restore that part of the program's locale. The string pointed to shall not
    be modified by the program, but may be overwritten by a subsequent call to
    the setlocale function.

So the string returned by setlocale (cat, NULL) is only valid until the next
setlocale call, and libgfortran restores through it after one. The Universal
CRT (UCRT) frees it when the category changes, so every formatted I/O statement
restores LC_NUMERIC through freed memory. msvcrt keeps a static buffer, which
is likely why this went unnoticed. The code is unchanged on trunk and has been
there since 2014, so any UCRT-based MinGW-w64 toolchain should be affected.

Consequences with UCRT-based MinGW-w64 toolchains:

- Depending on what the freed memory holds, either the process LC_NUMERIC ends
up as the user locale (seen locally: "English_United States.1252", although the
program never called setlocale), or the restore fails (returns NULL) and
LC_NUMERIC stays "C" (seen on GitHub windows-latest runners).
- Either way, every statement still makes two setlocale calls, and UCRT's
setlocale is far more expensive than msvcrt's. Internal WRITE, GCC 16.2.0, per
statement: 2.5 us (msvcrt) vs 6.8 us (UCRT) locally (user-locale case), 0.9 us
vs 2.2 us on GitHub runners (failed-restore case). A real-world I/O-heavy
program runs about 3x slower after moving from a msvcrt to a UCRT toolchain
(GCC 14.2).
- With MSVC, /MTd makes the restore fail deterministically, and
/fsanitize=address reports a heap-use-after-free (freed in
ucrt\src\appcrt\locale\setlocale.cpp).

A minimal C demo comparing MinGW UCRT, glibc and MSVC /MTd side by side:
https://godbolt.org/z/xsWGjcMzq

I have also created an MCVE repo at
https://github.com/jmarrec/libgfortran-ucrt-setlocale including actual CI runs
that exhibit the bug. CI runs mcve.f90 and mcve.c against niXman mingw-builds
GCC 16.2.0 (posix-seh-ucrt and posix-seh-msvcrt), and mcve.c against MSVC /MT,
/MTd and /fsanitize=address.

mcve.f90 and mcve.c from that repo are attached for convenience. Both exit with
code 1 when the bug is detected. They check whether the buffer returned by
setlocale (LC_NUMERIC, NULL) was replaced, so the result does not depend on
what the freed memory contains.

    gfortran -O2 -static mcve.f90 -o mcve_f.exe && ./mcve_f.exe
    gcc -O2 -static mcve.c -o mcve_c.exe && ./mcve_c.exe

Proposed fix: save a copy of the name before switching to "C", and skip the
switch entirely when LC_NUMERIC is already "C". Since that is the default for
programs that never call setlocale, this removes both setlocale calls per
statement in the common case.

Reply via email to