https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125506
Bug ID: 125506
Summary: libcpp/libiberty: MinGW-hosted GCC fails to open
system headers containing uppercase letters in Windows
case-sensitive directories (`lrealpath`
force-lowercases the path)
Product: gcc
Version: 12.5.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: preprocessor
Assignee: unassigned at gcc dot gnu.org
Reporter: luoyonggang at gmail dot com
Target Milestone: ---
# libcpp/libiberty: MinGW-hosted GCC fails to open system headers containing
uppercase letters in Windows case-sensitive directories (`lrealpath`
force-lowercases the path)
**Product / Component:** gcc - preprocessor (libcpp) / libiberty
**Host:** `x86_64-w64-mingw32` (MinGW, Win32)
**Target:** `x86_64-pc-linux-gnu` (Canadian cross)
**GCC (affected):** 6.5.0; the offending code is present in all of GCC 9-12
**GCC (fixed):** 13 and newer (see "Upstream status" below)
**OS:** Windows 11 with per-directory case sensitivity enabled
(`fsutil file setCaseSensitiveInfo <dir> enable` - the WSL/NTFS feature)
## Upstream status
This is **already fixed in GCC 13+**. The Windows branch of
`libiberty/lrealpath.c`
was rewritten in commit `e2bb55ec3b70cf45088bb73ba9ca990129328d60`
("libiberty: fix lrealpath on Windows NTFS symlinks", PR 108350, 2023-02-11) to
use
`GetFinalPathNameByHandle`/`GetFullPathName`. That rewrite removed the
`CharLowerBuff()` call, so the returned path now preserves (and even resolves
to)
the true on-disk case. The change was motivated by NTFS symlink handling, not
case
sensitivity, so this particular symptom appears to have been fixed only
incidentally and was never tracked as its own report. GCC 9, 10, 11, and 12
still
contain the lowercasing and are affected.
## Summary
A cross-compiler hosted on MinGW cannot find a system header whose on-disk name
contains an uppercase letter when the header's directory has the Windows
per-directory case-sensitive attribute set - even though the file exists with
the
exact requested case. glibc's `libio.h` does `#include <_G_config.h>`, so a
trivial `#include <stdio.h>` fails:
```
In file included from .../sysroot/usr/include/stdio.h:74:0,
from hello.c:1:
.../sysroot/usr/include/libio.h:32:23: fatal error: _G_config.h: No such file
or directory
#include <_G_config.h>
^
compilation terminated.
```
## Steps to reproduce
1. Build/obtain a GCC cross-compiler hosted on `x86_64-w64-mingw32` with a
glibc
sysroot (e.g. crosstool-NG), where `gcc -print-sysroot` resolves through a
relative component, e.g. `<prefix>\bin\..\<target>\sysroot` (i.e. the path
contains a `..` segment).
2. On Windows 11, place/extract the toolchain in a directory tree that has case
sensitivity enabled:
```
fsutil file setCaseSensitiveInfo "<...>\sysroot\usr\include" enable
```
3. Compile a file that includes `<stdio.h>`:
```c
#include <stdio.h>
int main(void) { return 0; }
```
## Actual result
```
fatal error: _G_config.h: No such file or directory
```
...although `usr\include\_G_config.h` exists with exactly that case. Verified
on
disk via PowerShell (character codes of the name):
```
[_G_config.h]
95 71 95 99 111 110 102 105 103 46 104 ( = _ G _ c o n f i g . h )
```
`dir`/`FindFirstFile` (case-insensitive wildcard) lists the file fine; it is
the
exact-name `open()` from the compiler that fails. Renaming the file to a
lowercase name (`_g_config.h`) makes compilation succeed - a strong hint at the
cause below.
## Expected result
The header is found and the file compiles, exactly as it does on a normal
case-insensitive Windows volume.
## Root cause
`libiberty/lrealpath.c` (the `_WIN32` branch) force-lowercases the whole path,
on the assumption that Windows filesystems are always case-insensitive:
```c
#if defined (_WIN32)
{
char buf[MAX_PATH];
char* basename;
DWORD len = GetFullPathName (filename, MAX_PATH, buf, &basename);
if (len == 0 || len > MAX_PATH - 1)
return strdup (filename);
else
{
/* The file system is case-preserving but case-insensitive,
Canonicalize to lowercase, using the codepage associated
with the process locale. */
CharLowerBuff (buf, len);
return strdup (buf);
}
}
#endif
```
`libcpp/files.c:find_file_in_dir()` canonicalizes system-header paths (enabled
by
default via `-fcanonical-system-headers`) through
`maybe_shorter_path() -> lrealpath()`, and adopts the result whenever it is
*shorter* than the original:
```c
static char *
maybe_shorter_path (const char * file)
{
char * file2 = lrealpath (file);
if (file2 && strlen (file2) < strlen (file))
return file2;
else { free (file2); return NULL; }
}
```
```c
if ((CPP_OPTION (pfile, canonical_system_headers) && file->dir->sysp)
#ifdef HAVE_DOS_BASED_FILE_SYSTEM
|| !file->dir->sysp
#endif
)
{
char * canonical_path = maybe_shorter_path (path);
if (canonical_path) { free (path); path = canonical_path; }
}
```
When the include search path contains a `..` segment (common with relocatable /
relative sysroots, e.g. `<prefix>\bin\..\<target>\sysroot\usr\include`),
`GetFullPathName` collapses the `..`, so the canonicalized path is genuinely
shorter and gets adopted - and it has been lowercased. On a case-sensitive
directory the lowercased name (`_g_config.h`) no longer matches the real
on-disk
entry (`_G_config.h`), so `open()` fails with `ENOENT`.
All-lowercase headers (`stdio.h`, `libio.h`, `bits/types.h`, etc.) are
unaffected
because lowercasing them is a no-op, which is why only the rare
uppercase-bearing
header (`_G_config.h`) fails and why the error surfaces specifically there.
## Suggested fix
`lrealpath()` must not destroy filename case in the path it returns. Options:
- Remove the `CharLowerBuff()` call (`GetFullPathName` already preserves the
case as given); or
- If a canonical on-disk case is desired, obtain the *real* case via
`GetLongPathNameA`/`GetFinalPathNameByHandle` instead of forcing lowercase;
or
- Confine any case-folding to a comparison/hash key only, and never use the
lowercased form as the path passed to `open()`.
(GCC 13 took the second approach as part of commit e2bb55ec3b70.)
## Workarounds
- Keep the toolchain on a case-insensitive directory (do not enable
per-directory
case sensitivity on the sysroot), or
- Compile with `-fno-canonical-system-headers`, or
- Use GCC 13 or newer.