feat(libc): dump the faulting thread's stack words in Windows crash reports

APS5_CRASH_STACK_BYTES=<n> prints n bytes of stack words from rsp after the
stack return address candidates, then eight words at every distinct stack word
that points at readable, non-executable memory outside the thread's stack.
Off when the variable is unset, empty or 0.
This commit is contained in:
AlxcBTW committed 2026-10-18 12:20:10 +02:00
1 parent 9caecfd2d5
commit 127ee82f02
4 files changed
+82 -1

No files matched your search

+9
View File
@@ -837,5 +837,14 @@ if(MINGW)
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>"
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:$<TARGET_FILE_DIR:libc>;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:$<TARGET_FILE_DIR:libc>;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()
@@ -256,6 +256,57 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) {
return ReadCode(address, reinterpret_cast<std::uint8_t*>(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<std::uint64_t>(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20);
if (bytes == 0) return;
const auto* tib = reinterpret_cast<const NT_TIB*>(NtCurrentTeb());
const auto stackLow = reinterpret_cast<std::uint64_t>(tib->StackLimit);
const auto stackHigh = reinterpret_cast<std::uint64_t>(tib->StackBase);
const auto first = rsp & ~std::uint64_t{7};
Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast<unsigned long long>(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<std::uint64_t>(count) * 8;
const auto page = slot & ~std::uint64_t{0xfff};
if (page != readablePage) {
if (!IsReadable(slot)) break;
readablePage = page;
}
words[count] = *reinterpret_cast<const std::uint64_t*>(slot);
}
if (count == 0) break;
char text[192];
int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast<unsigned long long>(at - first));
for (int k = 0; k < count && used > 0 && used < static_cast<int>(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast<std::size_t>(used), " %016llx", static_cast<unsigned long long>(words[k]));
Report("%s\n", text);
end = at + static_cast<std::uint64_t>(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<const std::uint64_t*>(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<const std::uint64_t*>(value);
Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast<unsigned long long>(slot - first), static_cast<unsigned long long>(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");
@@ -1,6 +1,8 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <windows.h>
#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<std::uint64_t*>(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<int>(reinterpret_cast<std::uintptr_t>(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);
+2
View File
@@ -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=<n>` 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=<n>` 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`.