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

            Bug ID: 128486
           Summary: [mingw-w64, UCRT] libgfortran passes fd -1 to C
                    runtime in a process without console: OPEN or PRINT
                    aborts it (0xC0000409)
           Product: gcc
           Version: 16.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libfortran
          Assignee: unassigned at gcc dot gnu.org
          Reporter: federico.perini at gmail dot com
  Target Milestone: ---

Created attachment 66095
  --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=66095&action=edit
Sample Fortran DLL with bind(C) procedures

In a process that starts without a console and without standard handles
(pythonw.exe, a GUI-subsystem program, a child created with
STARTF_USESTDHANDLES and NULL handles), a DLL that links libgfortran aborts the
whole process with exit code 0xC0000409 (__fastfail) on its first OPEN or
INQUIRE(FILE=) of an existing file, or on its first PRINT. Seen with MSYS2
UCRT64 gfortran 16.2.0, with a static or a shared libgfortran. The code
involved is unchanged since at least GCC 4.9.

Cause (libgfortran/io/unix.c):

1. At start-up, fd_to_stream builds the preconnected units 5, 6 and 0 on
   descriptors 0, 1 and 2. Without a standard handle, fstat fails with EBADF
   and fd_to_stream stores s->fd = -1.

2. Later operations on these units pass that -1 to the C runtime:
   - find_file0 calls id_from_fd (mingw only), i.e. _get_osfhandle (-1), for
     every unit, units 5, 6 and 0 included. find_file runs on every
     INQUIRE(FILE=) of an existing file, and from new_unit on every OPEN of an
     existing file when F2018 is not allowed. In a DLL that is always the
     case: without a Fortran main program, _gfortran_set_options never runs.
   - raw_write calls write (-1, ...) for a PRINT or a WRITE to unit 0.
   - raw_read, raw_seek, raw_tell, raw_size, raw_truncate and stream_isatty
     do the same for READ(5), REWIND, BACKSPACE, ENDFILE, INQUIRE(SIZE=) and
     ISATTY.

3. POSIX and msvcrt.dll fail these calls with EBADF: the output is lost and
   the program goes on. The Universal CRT treats a negative descriptor as an
   invalid parameter: unless the process has installed an invalid-parameter
   handler, it calls __fastfail and the process exits with 0xC0000409.

A gfortran executable survives, because the mingw-w64 startup code installs
a handler. A DLL loaded by a host that installs none (python.exe,
pythonw.exe, any MSVC-built program) kills its host.

Reproducer (attached: mini.f90, host.c, launch.c). mini.f90 is a DLL with
two bind(C) entry points:

   open(newunit=iunit, file=path, action='read', iostat=ios)   ! mini_open
   print *, 'mini_print: a line on unit 6'                     ! mini_print

host.c loads the DLL and calls one entry point after
_set_invalid_parameter_handler (NULL), the state of an MSVC-built host.
launch.c starts a program with no console and NULL standard handles, and
prints its exit code.

   gfortran -shared -static -static-libgfortran -static-libgcc -o mini.dll
mini.f90
   gcc -o host.exe host.c
   gcc -o launch.exe launch.c
   ./launch.exe ./host.exe mini.dll open mini.f90
   ./launch.exe ./host.exe mini.dll print
   ./launch.exe C:/Python39/pythonw.exe -c "import ctypes;
ctypes.CDLL(r'C:/full/path/to/mini.dll').mini_print()"

Observed: child exit 0xC0000409, three times.
Expected: child exit 0x00000000: the OPEN succeeds and the PRINT output is
discarded, as on POSIX when descriptor 1 is closed.

The same commands with the MINGW64 (msvcrt.dll) toolchain exit 0. Run from a
shell, with standard handles, the host prints the line and exits 0.

Environment: MSYS2 UCRT64, gcc version 16.2.0 (Rev6, Built by MSYS2
project), target x86_64-w64-mingw32, thread model posix; Windows 10 22H2,
ucrtbase.dll 10.0.19041.3636.

One CRT call on fd -1 per process, no standard handles:

   call on fd -1                    msvcrt.dll   UCRT, no handler   UCRT,
mingw-w64 handler
   _get_osfhandle                   -1, EBADF    0xC0000409         -1, EBADF
   _write _read _lseeki64 _fstat64  -1, EBADF    0xC0000409         -1, EBADF
   _close _setmode _dup _commit     -1, EBADF    0xC0000409         -1, EBADF
   _chsize                          -1, EBADF    0xC0000409         -1, EBADF
   _isatty                          0, EBADF     0xC0000409         0, EBADF

The same calls on descriptor 1 without an OS handle (_get_osfhandle (1)
returns -2) never abort, under either CRT: only the -1 that fd_to_stream
stores is fatal.

Fortran statements on a preconnected unit (UCRT64 DLL, host with no handler,
no standard handles). The function column comes from a logging handler that
returns:

   statement                 function passing fd -1             UCRT64 DLL  
MINGW64 DLL
   OPEN, existing file       find_file0 (id_from_fd), 5, 6, 0   0xC0000409   0,
IOSTAT 0
   INQUIRE(FILE=), existing  find_file0 (id_from_fd)            0xC0000409   0,
IOSTAT 0
   PRINT; WRITE(0,...)       raw_write                          0xC0000409   0,
IOSTAT 0
   READ(5,...)               raw_read                           0xC0000409   0,
IOSTAT -1
   REWIND(6)                 raw_seek                           0xC0000409   0,
IOSTAT 9
   BACKSPACE(6)              raw_tell, raw_seek                 0xC0000409   0,
IOSTAT 9
   ENDFILE(6)                raw_tell, raw_truncate             0xC0000409   0,
IOSTAT 9
   INQUIRE(UNIT=6, SIZE=)    raw_size                           0xC0000409   0,
IOSTAT 0
   ISATTY(6)                 stream_isatty                      0xC0000409   0

Proposed fix (patch attached): never pass a negative descriptor to the C
library. id_from_fd returns 0 for fd < 0, the value it already returns for
an invalid handle; raw_read, raw_write, raw_seek, raw_tell, raw_size,
raw_truncate (mingw branch) and stream_isatty fail with EBADF for fd < 0.
The guards are unconditional: POSIX already fails these calls with EBADF,
so nothing changes there. The patch applies to master and to releases/gcc-13
through releases/gcc-16.

Tested on x86_64-w64-mingw32 (MSYS2 UCRT64, GCC 16.2.0): the patched
io/unix.c of the 16.2.0 release, compiled with the flags of MSYS2's
libgfortran build, replaced unix.o in MSYS2's libgfortran.a (the unpatched
file compiled the same way matches the shipped unix.o in objdump -d -r).
With no standard handles, from the C host and from pythonw.exe, every
statement above exits 0 with the msvcrt.dll IOSTAT values, and no call
reaches the invalid-parameter handler. With standard handles redirected to
files, the results are unchanged. Not bootstrapped; the testsuite was not
run.

Workaround until a fixed libgfortran is available: before loading the DLL,
point each standard descriptor that has no handle at NUL (_open ("NUL", ...)
and _dup2 on descriptors 0, 1 and 2).

Thank you,
Federico

Reply via email to