From 74392900d5f891ca38d2670e5cdf181ecec4201f Mon Sep 17 00:00:00 2001 From: Reagan Little <198160383+rebelittle@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:12:07 -0400 Subject: [PATCH] fix(libc): unwind guest exceptions through Windows DLL frames The Windows unwinder stopped at the first frame of a module without the project's .ehmeta or .ehfram tables, so an exception could not cross libstdc++-6.dll and libwinpthread-1.dll (std::call_once through pthread_once) or system DLLs. For such a frame it now looks up the function's PE unwind data with RtlLookupFunctionEntry and takes the caller's registers, including the nonvolatile rsi, rdi and xmm6-xmm15, from RtlVirtualUnwind. A frame whose unwind data has an exception or termination handler still stops the walk, since those handlers are not run. windows_exceptions gets back the std::call_once case of #1423. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/exception/Unwind.cpp | 71 ++++++++++++++++++--- core/libs/tests/WindowsExceptions.cpp | 23 +++++++ docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 87 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index e38aa9fb1..a05a32369 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -90,6 +90,10 @@ struct Frame { std::intptr_t dataAlign {}; unsigned returnRegister {}; bool signal {}; +#ifdef _WIN32 + bool foreign {}; + _Unwind_Context caller {}; +#endif }; bool DecodeRange(Frame& frame, const Lookup& query, const Byte*& p, const char*& augmentation, Byte& lsdaEncoding) { @@ -192,6 +196,42 @@ const FrameIndex& IndexFrames(const ImageKey& key, const Byte* begin, const Byte std::sort(index.begin(), index.end()); return index; } + +const IMAGE_NT_HEADERS64* ImageHeaders(Word pc, const Byte*& base) { + void* image = nullptr; + if (!RtlPcToFileHeader(reinterpret_cast(pc), &image) || !image) return nullptr; + base = static_cast(image); + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr; + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + return nt->Signature == IMAGE_NT_SIGNATURE ? nt : nullptr; +} + +bool VirtualUnwind(const _Unwind_Context& context, _Unwind_Context& caller) { + const Word pc = context.registers[16]; + const Byte* base = nullptr; + const auto* nt = ImageHeaders(pc, base); + if (!nt) return false; + const auto* sections = IMAGE_FIRST_SECTION(nt); + for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) + if (std::memcmp(sections[i].Name, ".ehmeta", 8) == 0 || std::memcmp(sections[i].Name, ".ehfram", 8) == 0) return false; + DWORD64 imageBase = 0; + auto* function = RtlLookupFunctionEntry(pc, &imageBase, nullptr); + if (!function) return false; + CONTEXT state {}; + DWORD64* const registers[17] {&state.Rax, &state.Rdx, &state.Rcx, &state.Rbx, &state.Rsi, &state.Rdi, &state.Rbp, &state.Rsp, + &state.R8, &state.R9, &state.R10, &state.R11, &state.R12, &state.R13, &state.R14, &state.R15, &state.Rip}; + for (unsigned i = 0; i < 17; ++i) *registers[i] = context.registers[i]; + for (unsigned i = 0; i < 16; ++i) std::memcpy(&state.FltSave.XmmRegisters[i], context.vectorRegisters[i], 16); + void* handlerData = nullptr; + DWORD64 establisher = 0; + if (RtlVirtualUnwind(UNW_FLAG_EHANDLER | UNW_FLAG_UHANDLER, imageBase, pc, function, &state, &handlerData, &establisher, nullptr)) return false; + caller = context; + for (unsigned i = 0; i < 17; ++i) caller.registers[i] = *registers[i]; + for (unsigned i = 0; i < 16; ++i) std::memcpy(caller.vectorRegisters[i], &state.FltSave.XmmRegisters[i], 16); + caller.signalFrame = false; + return true; +} #endif bool DecodeFrame(_Unwind_Context& context, Frame& frame) { @@ -224,13 +264,9 @@ bool DecodeFrame(_Unwind_Context& context, Frame& frame) { #endif return DecodeCandidate(context, frame, query); #else - void* image = nullptr; - if (!RtlPcToFileHeader(reinterpret_cast(query.pc), &image) || !image) return false; - const auto* base = static_cast(image); - const auto* dos = reinterpret_cast(base); - if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; - const auto* nt = reinterpret_cast(base + dos->e_lfanew); - if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + const Byte* base = nullptr; + const auto* nt = ImageHeaders(query.pc, base); + if (!nt) return false; const auto* sections = IMAGE_FIRST_SECTION(nt); for (unsigned i = 0; i < nt->FileHeader.NumberOfSections; ++i) { const auto& section = sections[i]; @@ -385,7 +421,18 @@ bool Expression(const Byte* p, const _Unwind_Context& context, Word cfa, Word& r } bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { - if (!DecodeFrame(context, frame)) return false; + if (!DecodeFrame(context, frame)) { +#ifdef _WIN32 + frame = {}; + if (!VirtualUnwind(context, frame.caller)) return false; + frame.foreign = true; + context.cfa = frame.caller.registers[7]; + context.region = context.lsda = context.personality = context.textBase = context.dataBase = 0; + return true; +#else + return false; +#endif + } Rules initial; if (!Instructions(frame.cieBegin, frame.cieEnd, frame, ~Word(0), initial, {})) return false; #ifdef _WIN32 @@ -406,6 +453,14 @@ bool GetRules(_Unwind_Context& context, Frame& frame, Rules& rules) { bool Step(_Unwind_Context& context) { Frame frame; Rules rules; if (!GetRules(context, frame, rules)) return false; +#ifdef _WIN32 + if (frame.foreign) { + const auto& next = frame.caller; + if (!next.registers[16] || (next.registers[7] == context.registers[7] && next.registers[16] == context.registers[16])) return false; + context = next; + return true; + } +#endif _Unwind_Context next = context; for (unsigned i = 0; i < 17; ++i) { const auto& rule = rules.registers[i]; diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index d214000e5..99a3ca928 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -248,6 +248,28 @@ static void testUnwindThroughGuestFrames() { } } +static int onceDestroyed; +struct OnceGuard { ~OnceGuard() { ++onceDestroyed; } }; +static void ThrowThroughOnce(int depth) { + OnceGuard guard; + if (depth) { ThrowThroughOnce(depth - 1); return; } + std::once_flag flag; + std::call_once(flag, [] { throw std::runtime_error("through runtime frames"); }); +} + +static void testUnwindThroughRuntimeModules() { + for (int depth : {0, 1, 7, 64}) { + onceDestroyed = 0; + try { + ThrowThroughOnce(depth); + throw std::logic_error("call_once did not propagate"); + } catch (const std::runtime_error& error) { + if (std::strcmp(error.what(), "through runtime frames")) throw; + } + if (onceDestroyed != depth + 1) throw std::runtime_error("unwinding through runtime DLL frames skipped a destructor"); + } +} + int main(int argc, char** argv) { if (argc > 1) { if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); @@ -260,6 +282,7 @@ int main(int argc, char** argv) { testMadeExceptionPointer(); testUncaughtExceptions(); testUnwindThroughGuestFrames(); + testUnwindThroughRuntimeModules(); try { Rethrow(); return 1; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index e05b39391..60c94db38 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -475,7 +475,7 @@ Throughout the project, every function at every stage either **does exactly what - [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection -- The Windows libc exception unwinder reads the project's `.ehmeta` and `.ehfram` tables, not PE `.pdata`/`.xdata`. WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll` have empty `.eh_frame` sections, so guest exceptions cannot currently cross their frames (for example, the `std::call_once` path through `pthread_once`); `RtlVirtualUnwind` stepping and Windows cleanup handlers are not implemented +- The Windows libc [exception unwinder](../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask