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.

Reply via email to