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