diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..deee94494f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -837,5 +837,14 @@ if(MINGW) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:" FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran") + add_test(NAME windows_crash_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") + add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005" + FAIL_REGULAR_EXPRESSION "stack words from rsp;data at stack words") endif() endif() diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 47cfaed9fc..e7c1694e1a 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -256,6 +256,57 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) { return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); } +std::uint64_t EnvironmentNumber(const char* name) { + char text[32]; + const DWORD length = GetEnvironmentVariableA(name, text, sizeof(text)); + if (length == 0 || length >= sizeof(text)) return 0; + return std::strtoull(text, nullptr, 0); +} + +void ReportStackWords(std::uint64_t rsp) { + const auto bytes = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); + if (bytes == 0) return; + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto stackLow = reinterpret_cast(tib->StackLimit); + const auto stackHigh = reinterpret_cast(tib->StackBase); + const auto first = rsp & ~std::uint64_t{7}; + Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast(rsp)); + std::uint64_t end = first; + std::uint64_t readablePage = 0; + for (std::uint64_t at = first; at < first + bytes; at += 32) { + std::uint64_t words[4] = {}; + int count = 0; + for (; count < 4; ++count) { + const auto slot = at + static_cast(count) * 8; + const auto page = slot & ~std::uint64_t{0xfff}; + if (page != readablePage) { + if (!IsReadable(slot)) break; + readablePage = page; + } + words[count] = *reinterpret_cast(slot); + } + if (count == 0) break; + char text[192]; + int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast(at - first)); + for (int k = 0; k < count && used > 0 && used < static_cast(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast(used), " %016llx", static_cast(words[k])); + Report("%s\n", text); + end = at + static_cast(count) * 8; + if (count < 4) break; + } + std::uint64_t seen[256]; + std::size_t seenCount = 0; + Report(" data at stack words that point outside the stack (8 words each):\n"); + for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) { + const auto value = *reinterpret_cast(slot); + if (value < 0x10000 || (value & 7) != 0 || value > 0x7fffffffffffull || (value >= stackLow && value < stackHigh)) continue; + if (std::find(seen, seen + seenCount, value) != seen + seenCount) continue; + if (IsExecutable(value) || !IsReadable(value) || !IsReadable(value + 0x38)) continue; + seen[seenCount++] = value; + const auto* words = reinterpret_cast(value); + Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast(slot - first), static_cast(value), words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + } +} + void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { const auto* record = info->ExceptionRecord; char type[256] = "of unknown type"; @@ -377,7 +428,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { frame = next; } ReportStackCandidates(context->Rsp, 16, 0x2000); - if (context->Rip == 0 || !IsExecutable(context->Rip)) { + ReportStackWords(context->Rsp); + if (context->Rip == 0|| !IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. Report(" recently popped return address candidates:\n"); diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index 32871b4f63..d16340e2b3 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" #include "../prx/libc/include/FailureReport.hpp" @@ -16,10 +18,26 @@ void DroppedReport() noexcept { std::fputs("dropped failure report ran\n", stderr); } +LONG WINAPI EndAfterReport(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_CONTINUE_SEARCH; +} + +[[gnu::noinline]] void FaultHoldingObject() { + AddVectoredExceptionHandler(0, EndAfterReport); + auto* object = static_cast(VirtualAlloc(nullptr, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + if (object == nullptr) std::exit(1); + for (std::uint64_t i = 0; i < 8; ++i) object[i] = 0xc0ffee0000000000ull + i; + std::uint64_t* volatile held = object; + volatile int* volatile target = nullptr; + *target = static_cast(reinterpret_cast(held)); +} + } int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "fault") == 0) FaultHoldingObject(); if (argc == 2 && std::strcmp(argv[1], "report") == 0) { LibcRegisterFailureReport_nid_no_patch(DroppedReport); LibcRegisterFailureReport_nid_no_patch(KeptReport); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 27b1862066..de73e72082 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -242,6 +242,8 @@ When a register points at readable memory that starts with at least four printab Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. +Set `APS5_CRASH_STACK_BYTES=` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. + ### GPU submission rings and stall dumps Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`.