ITADN

DynamicLibrary DLSym resolves CRT symbols from msvcrt.dll instead of ucrtbase on MS-Windows

#13092Openikappaki 创建于 2026-05-30
I
ikappakicommented
Hi, I hit this while working on ORC JIT support for [jank](https://github.com/jank-lang/jank) on Windows. When the host binary is linked against ucrt, `DynamicLibrary::getAddressOfSymbol` returns addresses from `msvcrt.dll` for standard C functions like `tmpfile` and `fwrite`. This causes crashes when a `FILE*` created by JIT'd code (msvcrt layout) is passed to host code (ucrtbase layout). **Reproducer** (requires linking against LLVMSupport): ``` clang++ -o repro.exe repro_llvm_api.cpp $(llvm-config --cxxflags --ldflags --libs support) ``` ```cpp #include "llvm/Support/DynamicLibrary.h" #include <cstdio> #include <cstring> #include <windows.h> static bool icontains(const char *h, const char *n) { for (; *h; ++h) { const char *a=h, *b=n; while (*a && *b && tolower(*a)==tolower(*b)) { ++a; ++b; } if (!*b) return true; } return false; } static const char *getModuleFor(void *addr) { static char buf[MAX_PATH]; HMODULE mod = nullptr; GetModuleHandleExA(6, (LPCSTR)addr, &mod); if (mod) GetModuleFileNameA(mod, buf, MAX_PATH); else strcpy(buf, "<unknown>"); return buf; } int main() { // msvcrt.dll is already loaded - the MinGW toolchain links advapi32/shell32/ // user32 by default, and those system DLLs depend on msvcrt at runtime. auto DL = llvm::sys::DynamicLibrary::getPermanentLibrary(nullptr); void *llvm_tmpfile = DL.getAddressOfSymbol("tmpfile"); void *host_tmpfile = (void*)&tmpfile; fprintf(stderr, "LLVM DynamicLibrary resolved tmpfile to: %p (%s)\n", llvm_tmpfile, getModuleFor(llvm_tmpfile)); fprintf(stderr, "Host binary uses tmpfile at: %p (%s)\n", host_tmpfile, getModuleFor(host_tmpfile)); if (llvm_tmpfile == host_tmpfile) return 0; // OK const char *mod = getModuleFor(llvm_tmpfile); if (!icontains(mod, "msvcrt")) return 0; fprintf(stderr, "\nBUG: LLVM resolved tmpfile from msvcrt.dll, not the host CRT.\n"); FILE *f = ((FILE*(*)(void))llvm_tmpfile)(); fwrite("x", 1, 1, f); // crash: mismatched CRT return 1; } ``` Output: ``` LLVM DynamicLibrary resolved tmpfile to: 00007FFFF6DE39E0 (C:\WINDOWS\System32\msvcrt.dll) Host binary uses tmpfile at: 00007FF732D12B10 (repro_llvm_api.exe) BUG: LLVM resolved tmpfile from msvcrt.dll, not the host CRT. Segmentation fault ``` The host binary is linked against ucrt (via `api-ms-win-crt-stdio`), so its `fwrite` expects ucrtbase's `FILE` layout. The LLVM-resolved `tmpfile` creates a msvcrt `FILE*` with a different layout -> crash. What's happening: the problem is the reverse iteration in [`HandleSet::DLSym`](https://github.com/llvm/llvm-project/blob/d659364295f6/llvm/lib/Support/Windows/DynamicLibrary.inc#L86-L143). The reverse order was introduced in [c1db8cf9c1dd](https://github.com/llvm/llvm-project/commit/c1db8cf9c1dd) (2017) to prefer ucrt over msvcrt, but the assumption about load order doesn't hold. For ucrt linked binaries, `ucrtbase.dll` loads before `msvcrt.dll` - ucrtbase is a direct dependency of the EXE, while msvcrt is a transitive dependency pulled in by system DLLs like advapi32. So reverse iteration finds msvcrt first. This standalone reproducer (no LLVM dependency, just Windows APIs) shows the mechanism: ``` clang++ -o repro_nodep.exe repro_nodep.cpp -lpsapi # psapi provides EnumProcessModulesEx ``` ```cpp #include <cstdio> #include <vector> #include <windows.h> #include <psapi.h> static bool icontains(const char *h, const char *n) { for (; *h; ++h) { const char *a=h, *b=n; while (*a && *b && tolower(*a)==tolower(*b)) { ++a; ++b; } if (!*b) return true; } return false; } int main() { // msvcrt.dll is already loaded - MinGW always links advapi32/shell32/user32, // which depend on msvcrt at runtime. HANDLE proc = GetCurrentProcess(); DWORD bytes = 0; EnumProcessModulesEx(proc, nullptr, 0, &bytes, LIST_MODULES_64BIT); std::vector<HMODULE> mods(bytes / sizeof(HMODULE)); EnumProcessModulesEx(proc, mods.data(), bytes, &bytes, LIST_MODULES_64BIT); int count = bytes / sizeof(HMODULE); // Print which modules export tmpfile fprintf(stderr, "Module order (modules exporting tmpfile):\n"); for (int i = 0; i < count; ++i) { if (GetProcAddress(mods[i], "tmpfile")) { char n[MAX_PATH] = {}; GetModuleFileNameA(mods[i], n, MAX_PATH); fprintf(stderr, " [%d] %s\n", i, n); } } // Same algorithm as DynamicLibrary.inc DLSym: EXE first, then reverse FARPROC found = GetProcAddress(mods[0], "tmpfile"); int idx = 0; if (!found) for (int i = count-1; i >= 1; --i) if ((found = GetProcAddress(mods[i], "tmpfile"))) { idx = i; break; } char name[MAX_PATH] = {}; GetModuleFileNameA(mods[idx], name, MAX_PATH); fprintf(stderr, "Reverse iteration picked: [%d] %s\n", idx, name); if (!icontains(name, "msvcrt")) return 0; FILE *f = ((FILE*(*)(void))found)(); fwrite("x", 1, 1, f); // crashes here return 1; } ``` Output: ``` Module order (modules exporting tmpfile): [5] C:\WINDOWS\System32\ucrtbase.dll [8] C:\WINDOWS\System32\msvcrt.dll Reverse iteration picked: [8] C:\WINDOWS\System32\msvcrt.dll Segmentation fault ``` ucrtbase is at a lower index (loaded first, direct dep), msvcrt at a higher index (loaded later, transitive dep via advapi32). Reverse iteration hits msvcrt before ucrtbase. One solution could be to skip `msvcrt.dll` in the DLSym loop when `ucrtbase.dll` is also present, happy to hear alternatives. Tested on Windows 11 x64, Clang 22 (MSYS2 CLANG64, ucrt target). Should reproduce on any ucrt linked Windows binary where msvcrt.dll is also loaded (which is the common case - the MinGW toolchain's default libraries pull it in via system DLL dependencies). Thanks
0 条评论