From 8b6fa1e8601482a36c75b21c46e6e6df00bf698e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 8 Oct 2026 14:03:36 -0500 Subject: [PATCH 1/2] implement lua traceback on crash, when possible added a WIndows crash handler (loaded in front of bay12's) that will attempt to produce a lua traceback if a crash occurs while running a lua script. this is also added to the linux crashlogger in addition, a small addition is made to the Lua engine itself to attempt to catch a possible cause of misoperation that some recent crash logs indicated as a possibility that we want to protect against --- depends/lua/src/ldo.c | 39 +++++ docs/changelog.txt | 2 + library/CMakeLists.txt | 2 + library/Core.cpp | 19 +- library/Crashlog-windows.cpp | 328 +++++++++++++++++++++++++++++++++++ library/Crashlog.cpp | 258 ++++++++++++++++++++++++++- library/LuaTools.cpp | 19 ++ library/include/Crashlog.h | 53 ++++++ 8 files changed, 707 insertions(+), 13 deletions(-) create mode 100644 library/Crashlog-windows.cpp create mode 100644 library/include/Crashlog.h diff --git a/depends/lua/src/ldo.c b/depends/lua/src/ldo.c index 65158df0b7..cef04888dc 100644 --- a/depends/lua/src/ldo.c +++ b/depends/lua/src/ldo.c @@ -38,6 +38,33 @@ #define errorstatus(s) ((s) > LUA_YIELD) +/* +** {====================================================== +** DFHACK: runtime sanity checks around C function results +** ======================================================= +*/ + +/* +** api_checknelems() compiles to nothing in release builds, so a C +** function (or a continuation function) that returns a result count +** inconsistent with the number of values actually on the stack would +** make luaD_poscall()/moveresults() read and write outside the stack, +** hard-crashing the host process. Convert that condition into a +** catchable Lua error instead. This check is two comparisons on a path +** already dominated by the call itself, so it is cheap enough to run on +** every call, including hot paths like the overlay render loop. +*/ +#define checkcresults(L,ci,n,f) \ + if ((n) < 0 || (n) >= (L)->top - (ci)->func) \ + luaG_runerror(L, \ + "lua_CFunction %p reported invalid result count %d", (void *)(f), (n)); + + +/* +** }====================================================== +*/ + + /* ** {====================================================== ** Error-recovery functions @@ -386,6 +413,15 @@ int luaD_poscall (lua_State *L, CallInfo *ci, StkId firstResult, int nres) { } res = ci->func; /* res == final position of 1st result */ L->ci = ci->previous; /* back to caller */ + /* DFHACK: sanity-check the pointers moveresults() is about to use. A + corrupted pointer here would otherwise read or write outside the + stack and hard-crash (or silently corrupt) the host process. */ + if (firstResult < L->stack || firstResult > L->top || + res < L->stack || res >= L->stack_last) { + if (L->top > L->ci->top) + L->top = L->ci->top; /* make sure the error value has a safe home */ + luaG_runerror(L, "corrupted Lua call frame"); + } /* move results to proper place */ return moveresults(L, firstResult, res, nres, wanted); } @@ -434,6 +470,7 @@ int luaD_precall (lua_State *L, StkId func, int nresults) { n = (*f)(L); /* do the actual call */ lua_lock(L); api_checknelems(L, n); + checkcresults(L, ci, n, f); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); return 1; } @@ -533,6 +570,7 @@ static void finishCcall (lua_State *L, int status) { n = (*ci->u.c.k)(L, status, ci->u.c.ctx); /* call continuation function */ lua_lock(L); api_checknelems(L, n); + checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); /* finish 'luaD_precall' */ } @@ -636,6 +674,7 @@ static void resume (lua_State *L, void *ud) { n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */ lua_lock(L); api_checknelems(L, n); + checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ firstArg = L->top - n; /* yield results come from continuation */ } luaD_poscall(L, ci, firstArg, n); /* finish 'luaD_precall' */ diff --git a/docs/changelog.txt b/docs/changelog.txt index 1e8961c7be..5da526f353 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,8 @@ Template for new versions: ## Fixes ## Misc Improvements +- `overlay`: errors raised by a bad C function result count or corrupted Lua call stack are now reported as Lua errors instead of crashing the game +- crash reports now include a Lua stack traceback when the game crashes while Lua code is executing (``crashlog/crash_*.txt`` on Linux, ``stderr.log`` on Windows) ## Documentation diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index f4c5c08d0b..117c0f5e8e 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -53,6 +53,7 @@ set(MAIN_HEADERS include/CoordTemplate.h include/Core.h include/CoreDefs.h + include/Crashlog.h include/DataDefs.h include/DataFuncs.h include/DataIdentity.h @@ -131,6 +132,7 @@ endif() set(MAIN_SOURCES_WINDOWS ${CONSOLE_SOURCES} + Crashlog-windows.cpp ) if(WIN32) diff --git a/library/Core.cpp b/library/Core.cpp index a17e5bf33c..09b9660345 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -23,6 +23,7 @@ distribution. */ #include "Core.h" +#include "Crashlog.h" #include "Internal.h" @@ -1096,8 +1097,7 @@ bool Core::InitMainThread(std::filesystem::path path) { Filesystem::init(); - #ifdef LINUX_BUILD - extern void dfhack_crashlog_init(); + #if defined(LINUX_BUILD) || defined(WIN32) dfhack_crashlog_init(); #endif @@ -1517,6 +1517,18 @@ bool Core::isSuspended(void) void Core::doUpdate(color_ostream &out) { + #ifdef WIN32 + // Re-chain the top-level exception filter once DF is fully running, + // in case DF installed its own crash filter after our init. If ours + // is still installed, this is a no-op. + static bool crashlog_rearmed = false; + if (!crashlog_rearmed) + { + crashlog_rearmed = true; + dfhack_crashlog_init(); + } + #endif + Lua::Core::Reset(out, "DF code execution"); // find the current viewscreen @@ -1914,8 +1926,7 @@ void Core::onStateChange(color_ostream &out, state_change_event event) int Core::Shutdown ( void ) { - #ifdef LINUX_BUILD - extern void dfhack_crashlog_shutdown(); + #if defined(LINUX_BUILD) || defined(WIN32) dfhack_crashlog_shutdown(); #endif diff --git a/library/Crashlog-windows.cpp b/library/Crashlog-windows.cpp new file mode 100644 index 0000000000..6e54cf2e49 --- /dev/null +++ b/library/Crashlog-windows.cpp @@ -0,0 +1,328 @@ +/* + * DFHack crash reporting for Windows. + * + * Dwarf Fortress installs its own top-level unhandled exception filter, + * which produces crashlog.txt (a native stack trace with module/offset + * annotations). That trace is enough to identify that a crash happened + * inside the Lua VM, but not what Lua code was running. + * + * This module chains an exception filter ahead of DF's (and additionally + * registers a vectored continue handler in case our filter is itself + * replaced later). When a fatal exception occurs and the Lua VM was + * involved - lua53.dll on the native stack, or a DFHack Lua call active + * on the faulting thread - we write a Lua stack traceback to stderr.log + * before chaining to DF's filter so that its crashlog is still produced. + * + * Everything in the crash path runs in the context of a crashed process, + * so it uses bare Win32 IO only and is wrapped in SEH so that a secondary + * fault while reporting simply falls through to DF's handler. + */ + +#define WIN32_LEAN_AND_MEAN +#include + +#include +#include +#include +#include +#include + +#include "Core.h" +#include "Crashlog.h" +#include "DFHackVersion.h" + +#include "lua.h" +#include "lauxlib.h" +#include "lstate.h" + +namespace { + +std::atomic handler_active{false}; +LPTOP_LEVEL_EXCEPTION_FILTER previous_filter = nullptr; +PVOID continue_handler = nullptr; + +// image bounds of interest, resolved once at init (before anyone could crash) +struct ImageRange { const char *name; uintptr_t base, end; }; +ImageRange df_image = {"Dwarf Fortress.exe", 0, 0}; +ImageRange dfhack_image = {"dfhack.dll", 0, 0}; +ImageRange lua_image = {"lua53.dll", 0, 0}; + +void cache_image_range(ImageRange &range, const wchar_t *module_name) +{ + HMODULE m = GetModuleHandleW(module_name); + if (!m) + return; + auto dos = reinterpret_cast(m); + auto nt = reinterpret_cast( + reinterpret_cast(m) + dos->e_lfanew); + range.base = reinterpret_cast(m); + range.end = range.base + nt->OptionalHeader.SizeOfImage; +} + +const char *image_name_for(uintptr_t addr) +{ + for (const auto &r : {df_image, dfhack_image, lua_image}) + if (r.base && addr >= r.base && addr < r.end) + return r.name; + return nullptr; +} + +// Minimal output: append to stderr.log with raw Win32 IO so we depend on +// as little process state as possible. +void log_write(HANDLE log, const char *fmt, ...) +{ + char buf[8192]; + va_list args; + va_start(args, fmt); + int len = _vsnprintf(buf, sizeof(buf) - 1, fmt, args); + va_end(args); + if (len == 0) + return; + if (len < 0 || len >= (int)sizeof(buf)) + len = (int)sizeof(buf) - 1; // truncated output is still worth having + buf[sizeof(buf) - 1] = '\0'; + DWORD written; + WriteFile(log, buf, len, &written, nullptr); +} + +void log_write_str(HANDLE log, const char *str) +{ + DWORD written; + WriteFile(log, str, (DWORD)strlen(str), &written, nullptr); +} + +// Walk the faulting thread's stack looking for instruction pointers inside +// lua53.dll. Uses RtlVirtualUnwind (driven by .pdata unwind info), which does +// not need dbghelp or symbol files. +bool lua_frames_on_stack(EXCEPTION_POINTERS *ep) +{ + CONTEXT ctx = *ep->ContextRecord; + for (int i = 0; i < 200; i++) + { + if (ctx.Rip >= lua_image.base && ctx.Rip < lua_image.end) + return true; + if (!ctx.Rip || (ctx.Rip & 7)) + break; + + DWORD64 image_base = 0; + PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(ctx.Rip, &image_base, nullptr); + if (fn) + { + PVOID handler_data = nullptr; + DWORD64 establisher = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, ctx.Rip, fn, + &ctx, &handler_data, &establisher, nullptr); + } + else + { + // leaf function: return address is at [rsp] + ctx.Rip = *reinterpret_cast(ctx.Rsp); + ctx.Rsp += 8; + } + } + return false; +} + +bool state_has_active_call(lua_State *L) +{ + return L && L->ci != &L->base_ci; +} + +void write_lua_traceback(HANDLE log, lua_State *L) +{ + if (!L) + return; + + // Mirror of struct lua_extra_state in depends/lua/include/dfhack_llimits.h: + // the first extraspace slot holds a pointer to the state's recursive + // critical section. If the faulting thread holds it already (e.g. the + // crash is inside a Lua call on this thread), TryEnterCriticalSection + // recurses and succeeds. If another thread is inside the VM, we note it + // and walk the state anyway - it is read-only and SEH-guarded, and an + // approximate trace is better than none. + bool locked = false; + CRITICAL_SECTION *cs = nullptr; + __try + { + cs = *reinterpret_cast(lua_getextraspace(L)); + if (cs) + locked = TryEnterCriticalSection(cs); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + cs = nullptr; + } + + __try + { + if (!locked) + log_write(log, "(could not acquire Lua state lock; " + "traceback may be inaccurate)\n"); + if (!state_has_active_call(L)) + { + log_write(log, "main lua_State %p: no active call\n", (void*)L); + } + else + { + luaL_traceback(L, L, "crash while Lua was executing", 0); + const char *tb = lua_tostring(L, -1); + if (tb) + { + log_write(log, "lua_State %p traceback:\n", (void*)L); + log_write_str(log, tb); + log_write(log, "\n"); + } + else + { + log_write(log, "lua_State %p: traceback not a string\n", (void*)L); + } + lua_pop(L, 1); + } + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + log_write(log, "(lua traceback attempt faulted)\n"); + } + + if (locked) + LeaveCriticalSection(cs); +} + +void report_crash(EXCEPTION_POINTERS *ep) +{ + HANDLE log = CreateFileA("stderr.log", FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (log == INVALID_HANDLE_VALUE) + return; + + EXCEPTION_RECORD *er = ep->ExceptionRecord; + const char *fault_image = image_name_for( + reinterpret_cast(er->ExceptionAddress)); + bool in_lua = lua_frames_on_stack(ep); + + lua_State *active = DFHack::Crashlog::active_state(); + lua_State *main_state = nullptr; + __try + { + if (!DFHack::Core::noInstance()) + main_state = DFHack::Core::getInstance().getLuaState(true); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + + log_write(log, + "\n===== DFHack crash report =====\n" + "DFHack %s, DF %s\n" + "Exception 0x%08lx at %p (module: %s), thread %lu\n", + DFHack::Version::dfhack_version(), DFHack::Version::df_version(), + er->ExceptionCode, er->ExceptionAddress, + fault_image ? fault_image : "?", + GetCurrentThreadId()); + + if (er->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && + er->NumberParameters >= 2) + log_write(log, "access violation %s address %p\n", + er->ExceptionInformation[0] ? "writing" : "reading", + (void*)er->ExceptionInformation[1]); + + log_write(log, + "lua53.dll on native stack: %s\n" + "thread-local active lua_State: %p\n", + in_lua ? "yes" : "no", + (void*)active); + + if (in_lua || active || state_has_active_call(main_state)) + { + write_lua_traceback(log, active ? active : main_state); + if (active && main_state && active != main_state && + state_has_active_call(main_state)) + write_lua_traceback(log, main_state); + } + else + { + log_write(log, "no Lua involvement detected\n"); + } + + log_write(log, "===== end DFHack crash report =====\n\n"); + CloseHandle(log); +} + +LONG WINAPI dfhack_unhandled_exception(EXCEPTION_POINTERS *ep) +{ + if (!handler_active.exchange(true)) + { + __try + { + report_crash(ep); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + } + + if (previous_filter) + return previous_filter(ep); + return EXCEPTION_CONTINUE_SEARCH; +} + +// Runs only if the exception was never handled and DF's own filter (or +// whoever replaced us) declined to handle it either. Produces the same +// report; the handler_active flag prevents double-reporting. +LONG WINAPI dfhack_vectored_continue(EXCEPTION_POINTERS *ep) +{ + if (!handler_active.exchange(true)) + { + __try + { + report_crash(ep); + } + __except (EXCEPTION_EXECUTE_HANDLER) {} + } + return EXCEPTION_CONTINUE_SEARCH; +} + +void chain_top_level_filter() +{ + LPTOP_LEVEL_EXCEPTION_FILTER prev = + SetUnhandledExceptionFilter(dfhack_unhandled_exception); + if (prev && prev != dfhack_unhandled_exception) + previous_filter = prev; +} + +} + +namespace DFHack { + +void dfhack_crashlog_init() +{ + cache_image_range(df_image, nullptr); + cache_image_range(dfhack_image, L"dfhack.dll"); + cache_image_range(lua_image, L"lua53.dll"); + + // Install our top-level filter, chaining to whoever was there before + // (normally DF's own crash handler). This function is also called again + // on the first update tick so that a filter DF installs later in + // startup still ends up behind us in the chain. + chain_top_level_filter(); + + // Fallback path: runs after the top-level filter if the exception was + // never handled, catching the case where our filter was overwritten. + if (!continue_handler) + continue_handler = AddVectoredContinueHandler(0, dfhack_vectored_continue); +} + +void dfhack_crashlog_shutdown() +{ + // Probe the current top-level filter by installing ours and looking at + // what was there. If ours was current, the chain tail is what should be + // restored; otherwise put back whatever we just displaced. + LPTOP_LEVEL_EXCEPTION_FILTER cur = + SetUnhandledExceptionFilter(dfhack_unhandled_exception); + SetUnhandledExceptionFilter(cur == dfhack_unhandled_exception + ? previous_filter : cur); + if (continue_handler) + { + RemoveVectoredContinueHandler(continue_handler); + continue_handler = nullptr; + } +} + +} diff --git a/library/Crashlog.cpp b/library/Crashlog.cpp index 3860e0106e..fdf20150d2 100644 --- a/library/Crashlog.cpp +++ b/library/Crashlog.cpp @@ -1,20 +1,43 @@ +// Dl_info and the SA_SIGINFO handler form need the GNU/POSIX extensions. +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + #include "DFHackVersion.h" +#include "Crashlog.h" #include +#include #include #include #include #include #include +#include #include #include +#include +#include +#include +#include + +#include "Core.h" +#include "lua.h" +#include "lauxlib.h" +#include "lstate.h" const int BT_ENTRY_MAX = 25; struct CrashInfo { int backtrace_entries = 0; void* backtrace[BT_ENTRY_MAX]; int signal = 0; + int sig_code = 0; + void* fault_address = nullptr; + // The lua_State the faulting thread was executing in, if it was inside + // a DFHack Lua entry point when it crashed. + lua_State* active_lua_state = nullptr; + pid_t thread_id = 0; }; CrashInfo crash_info; @@ -23,6 +46,10 @@ std::atomic crashed = false; std::atomic crashlog_ready = false; std::atomic shutdown = false; +// Load base of liblua53, resolved at init so the crashlog thread can +// recognize Lua frames without depending on anything else being intact. +void* lua_image_base = nullptr; + // Use eventfd for async-signal safe waiting int crashlog_complete = -1; @@ -44,13 +71,19 @@ void signal_crashlog_complete() { std::thread crashlog_thread; // Force this method to be inlined so that it doesn't create a stack frame -[[gnu::always_inline]] inline void handle_signal_internal(int sig) { +[[gnu::always_inline]] inline void handle_signal_internal(int sig, siginfo_t *si) { if (shutdown.load() || crashed.exchange(true) || crashlog_ready.load()) { // Ensure the signal handler doesn't try to write a crashlog // whilst the crashlog thread is unavailable. std::quick_exit(1); } crash_info.signal = sig; + if (si) { + crash_info.sig_code = si->si_code; + crash_info.fault_address = si->si_addr; + } + crash_info.thread_id = (pid_t)syscall(SYS_gettid); + crash_info.active_lua_state = DFHack::Crashlog::active_state(); crash_info.backtrace_entries = backtrace(crash_info.backtrace, BT_ENTRY_MAX); // Signal saving of crashlog @@ -63,12 +96,12 @@ std::thread crashlog_thread; std::quick_exit(1); } -extern "C" void dfhack_crashlog_handle_signal(int sig) { - handle_signal_internal(sig); +extern "C" void dfhack_crashlog_handle_signal(int sig, siginfo_t *si, void *) { + handle_signal_internal(sig, si); } void dfhack_crashlog_handle_terminate() { - handle_signal_internal(0); + handle_signal_internal(0, nullptr); } std::string signal_name(int sig) { @@ -83,12 +116,46 @@ std::string signal_name(int sig) { return "SIGFPE"; case SIGSEGV: return "SIGSEGV"; + case SIGBUS: + return "SIGBUS"; case SIGTERM: return "SIGTERM"; } return ""; } +std::string signal_code_name(int sig, int code) { + // si_code values are only unique within their signal, so key on both. + switch (sig) { + case SIGSEGV: + switch (code) { + case SEGV_MAPERR: return "SEGV_MAPERR"; + case SEGV_ACCERR: return "SEGV_ACCERR"; + case SEGV_BNDERR: return "SEGV_BNDERR"; + } + break; + case SIGBUS: + switch (code) { + case BUS_ADRALN: return "BUS_ADRALN"; + case BUS_ADRERR: return "BUS_ADRERR"; + } + break; + case SIGILL: + switch (code) { + case ILL_ILLOPC: return "ILL_ILLOPC"; + case ILL_PRVOPC: return "ILL_PRVOPC"; + } + break; + case SIGFPE: + switch (code) { + case FPE_INTDIV: return "FPE_INTDIV"; + case FPE_FLTDIV: return "FPE_FLTDIV"; + } + break; + } + return ""; +} + std::filesystem::path get_crashlog_path() { std::time_t time = std::time(nullptr); std::tm* tm = std::localtime(&time); @@ -109,6 +176,152 @@ std::filesystem::path get_crashlog_path() { return log_path; } +// --------------------------------------------------------------------------- +// Lua crash diagnostics +// +// These run on the dedicated crashlog thread in normal (non-signal) context, +// after the native backtrace has already been written and flushed. The Lua +// state may itself be corrupt - that is often *why* we crashed - so the +// inspection is bounded by a fault guard: a segfault while tracing only +// loses the Lua section of the report. +// --------------------------------------------------------------------------- + +volatile sig_atomic_t lua_guard_armed = 0; +sigjmp_buf lua_guard_jmp; +pid_t crashlog_thread_id = 0; +const int guarded_signals[3] = {SIGSEGV, SIGBUS, SIGILL}; +struct sigaction saved_actions[3]; + +// Runs as a temporary signal disposition while the Lua state is inspected. +// Faults on the crashlog thread jump back to the guard; a fault on any other +// thread re-raises with the default disposition so the process still dies +// with its original signal. +extern "C" void lua_fault_guard(int sig, siginfo_t *, void *) { + if (lua_guard_armed && (pid_t)syscall(SYS_gettid) == crashlog_thread_id) + siglongjmp(lua_guard_jmp, sig); + signal(sig, SIG_DFL); + raise(sig); +} + +bool in_lua_image(void *addr) { + Dl_info di; + if (!dladdr(addr, &di)) + return false; + if (lua_image_base && di.dli_fbase == lua_image_base) + return true; + if (di.dli_fname) { + const char *base = std::strrchr(di.dli_fname, '/'); + base = base ? base + 1 : di.dli_fname; + if (std::strstr(base, "lua53") || std::strstr(base, "liblua")) + return true; + } + return false; +} + +bool state_has_active_call(lua_State *L) { + return L && L->ci != &L->base_ci; +} + +// Emits the Lua section of the crashlog. Must only be called between +// arming and disarming the fault guard. +void inspect_lua_state(std::ofstream &crashlog, lua_State *L) { + if (!L) + return; + + // Best-effort lock. The faulting thread is frozen inside its signal + // handler and usually still holds the state mutex, so trylock typically + // fails and we walk the state read-only - its only writer is suspended + // anyway. If we do acquire it, no other thread is inside the VM. + // Mirror of struct lua_extra_state in depends/lua/include/dfhack_llimits.h: + // the extraspace slot holds a pointer to the state's recursive mutex. + bool locked = false; + auto *mutex = *reinterpret_cast(lua_getextraspace(L)); + if (mutex) + locked = pthread_mutex_trylock(mutex) == 0; + if (!locked) + crashlog << "could not acquire Lua state lock; " + "the following is a best-effort snapshot\n"; + + if (!state_has_active_call(L)) { + crashlog << "lua_State " << (const void*)L << ": no active call\n"; + } else { + // A corrupted CallInfo chain (e.g. a cycle) would send + // luaL_traceback into an unbounded walk, which the fault guard + // cannot interrupt. Bound the chain first. + const CallInfo *ci = L->ci; + int depth = 0; + while (ci && ci != &L->base_ci && ++depth <= 65536) + ci = ci->previous; + if (ci != &L->base_ci) { + crashlog << "lua_State " << (const void*)L + << ": corrupted CallInfo chain; not safe to trace\n"; + } else { + crashlog << "lua_State " << (const void*)L << " traceback:\n"; + luaL_traceback(L, L, "crash while Lua was executing", 0); + const char *tb = lua_tostring(L, -1); + crashlog << (tb ? tb : "(traceback unavailable)") << "\n"; + lua_pop(L, 1); + } + } + + if (locked) + pthread_mutex_unlock(mutex); +} + +// Writes the Lua diagnostics section, guarded against faults in corrupted +// interpreter metadata. Fault guard handlers are installed for the duration +// of the inspection and the original dispositions are restored afterwards. +void write_lua_context(std::ofstream &crashlog) { + bool lua_on_stack = false; + for (int i = 0; i < crash_info.backtrace_entries; i++) { + if (in_lua_image(crash_info.backtrace[i])) { + lua_on_stack = true; + break; + } + } + + crashlog << "\nliblua53 on native stack: " << (lua_on_stack ? "yes" : "no") << "\n"; + crashlog << "thread-local active lua_State: " + << (const void*)crash_info.active_lua_state << "\n"; + + struct sigaction sa; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = lua_fault_guard; + sa.sa_flags = SA_SIGINFO | SA_NODEFER; + sigemptyset(&sa.sa_mask); + for (int i = 0; i < 3; i++) + sigaction(guarded_signals[i], &sa, &saved_actions[i]); + crashlog_thread_id = (pid_t)syscall(SYS_gettid); + + // Everything that dereferences the Lua state or Core singleton lives + // inside the guard: either may be pointing at corrupt memory. + int fault = sigsetjmp(lua_guard_jmp, 1); + if (fault == 0) { + lua_guard_armed = 1; + + lua_State *active = crash_info.active_lua_state; + lua_State *main_state = nullptr; + if (!DFHack::Core::noInstance()) + main_state = DFHack::Core::getInstance().getLuaState(true); + + if (!lua_on_stack && !active && !state_has_active_call(main_state)) { + crashlog << "no Lua involvement detected\n"; + } else { + inspect_lua_state(crashlog, active ? active : main_state); + if (active && main_state && active != main_state && + state_has_active_call(main_state)) + inspect_lua_state(crashlog, main_state); + } + lua_guard_armed = 0; + } else { + crashlog << "(inspecting the Lua state faulted: " + << signal_name(fault) << ")\n"; + } + + for (int i = 0; i < 3; i++) + sigaction(guarded_signals[i], &saved_actions[i], nullptr); +} + void dfhack_save_crashlog() { char** backtrace_strings = backtrace_symbols(crash_info.backtrace, crash_info.backtrace_entries); try { @@ -121,7 +334,18 @@ void dfhack_save_crashlog() { std::string signal = signal_name(crash_info.signal); if (!signal.empty()) { - crashlog << "Signal " << signal << "\n"; + crashlog << "Signal " << signal; + std::string code = signal_code_name(crash_info.signal, crash_info.sig_code); + if (!code.empty()) + crashlog << " (" << code << ")"; + else if (crash_info.sig_code) + crashlog << " (code " << crash_info.sig_code << ")"; + // si_addr is only meaningful for faults (a null deref reads as + // (nil), which is itself diagnostic). + if (crash_info.signal == SIGSEGV || crash_info.signal == SIGBUS || + crash_info.signal == SIGILL || crash_info.signal == SIGFPE) + crashlog << " at address " << crash_info.fault_address; + crashlog << " on thread " << crash_info.thread_id << "\n"; } if (crash_info.backtrace_entries >= 2 && backtrace_strings != nullptr) { @@ -133,6 +357,11 @@ void dfhack_save_crashlog() { // Make it clear if no relevant backtrace was able to be obtained crashlog << "Failed to obtain relevant backtrace\n"; } + + // Make sure the native report is on disk before touching the Lua + // state, which may itself be corrupt. + crashlog.flush(); + write_lua_context(crashlog); } catch (...) {} free(backtrace_strings); @@ -151,7 +380,7 @@ void dfhack_crashlog_thread() { std::terminate_handler term_handler = nullptr; -const int desired_signals[3] = {SIGSEGV,SIGILL,SIGABRT}; +const int desired_signals[4] = {SIGSEGV,SIGBUS,SIGILL,SIGABRT}; namespace DFHack { void dfhack_crashlog_init() { // Initialize eventfd flag @@ -159,15 +388,26 @@ namespace DFHack { crashlog_thread = std::thread(dfhack_crashlog_thread); + struct sigaction sa; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = dfhack_crashlog_handle_signal; + sa.sa_flags = SA_SIGINFO; + sigemptyset(&sa.sa_mask); for (int signal : desired_signals) { - std::signal(signal, dfhack_crashlog_handle_signal); + sigaction(signal, &sa, nullptr); } term_handler = std::set_terminate(dfhack_crashlog_handle_terminate); - // https://sourceware.org/glibc/manual/latest/html_mono/libc.html#index-backtrace-1 - // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s + // Symbols used by the signal handler and the crashlog thread must be + // resolved ahead of time - lazy binding cannot run in the crash path. + // backtrace is AsyncSignal-Unsafe due to dynamic loading of libgcc_s. // Using it here ensures it is loaded before use in the signal handler. [[maybe_unused]] int _ = backtrace(crash_info.backtrace, 1); + [[maybe_unused]] lua_State *s = DFHack::Crashlog::active_state(); + [[maybe_unused]] pid_t t = (pid_t)syscall(SYS_gettid); + Dl_info di; + if (dladdr((void*)&lua_close, &di)) + lua_image_base = di.dli_fbase; } void dfhack_crashlog_shutdown() { diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 56c5079838..ea356bc473 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -33,6 +33,7 @@ distribution. #include "DataFuncs.h" #include "LuaWrapper.h" #include "LuaTools.h" +#include "Crashlog.h" #include "MiscUtils.h" #include "DFHackVersion.h" #include "PluginManager.h" @@ -810,10 +811,26 @@ static int dfhack_safecall (lua_State *L) return Lua::TailPCallK(L, lua_gettop(L) - 2, LUA_MULTRET, 1, 0); } +// The lua_State through which this thread is currently executing a DFHack +// Lua entry point, or nullptr. Consulted by the crash reporter. +static thread_local lua_State *t_active_state = nullptr; + +lua_State *DFHack::Crashlog::active_state() { + return t_active_state; +} + +lua_State *DFHack::Crashlog::set_active_state(lua_State *state) { + lua_State *old = t_active_state; + t_active_state = state; + return old; +} + bool DFHack::Lua::SafeCall(color_ostream &out, lua_State *L, int nargs, int nres, bool perr) { AssertCoreSuspend(L); + Crashlog::ActiveStateGuard lua_guard(L); + int base = lua_gettop(L) - nargs; color_ostream *cur_out = Lua::GetOutput(L); @@ -975,6 +992,8 @@ int DFHack::Lua::SafeResume(color_ostream &out, lua_State *from, lua_State *thre { AssertCoreSuspend(from); + Crashlog::ActiveStateGuard lua_guard(thread ? thread : from); + color_ostream *cur_out = Lua::GetOutput(from); set_dfhack_output(from, &out); diff --git a/library/include/Crashlog.h b/library/include/Crashlog.h new file mode 100644 index 0000000000..c3d1f6aabd --- /dev/null +++ b/library/include/Crashlog.h @@ -0,0 +1,53 @@ +#pragma once + +struct lua_State; + +namespace DFHack { + +/** + * Installs/shuts down the platform crash reporter. Implemented + * per-platform (Crashlog.cpp on Linux, Crashlog-windows.cpp on Windows). + */ +void dfhack_crashlog_init(); +void dfhack_crashlog_shutdown(); + +/** + * Support for post-mortem crash reporting. + * + * The platform crash reporter (see Crashlog.cpp / Crashlog-windows.cpp) + * consults this to produce a Lua stack traceback when a fatal exception + * occurs while Lua code is executing. + */ +namespace Crashlog { + + /** + * The lua_State currently executing a DFHack Lua entry point + * (Lua::SafeCall, Lua::SafeResume, ...) on the calling thread, or + * nullptr if this thread is not inside a Lua call. + * + * Note that a thread's active state may be a coroutine rather than the + * main state, which is exactly what a traceback wants. + */ + lua_State *active_state(); + + /** + * Sets the calling thread's active lua_State, returning the previous + * value. Callers should generally prefer ActiveStateGuard. + */ + lua_State *set_active_state(lua_State *state); + + /** + * RAII helper to maintain active_state() around a Lua entry point. + */ + class ActiveStateGuard { + lua_State *previous; + public: + explicit ActiveStateGuard(lua_State *state) : + previous(set_active_state(state)) {} + ~ActiveStateGuard() { set_active_state(previous); } + + ActiveStateGuard(const ActiveStateGuard&) = delete; + ActiveStateGuard& operator=(const ActiveStateGuard&) = delete; + }; +} +} From 0f4a4372d6a568aa4dda1a5d810e4a6e0b4c2674 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 8 Oct 2026 17:15:26 -0500 Subject: [PATCH 2/2] address review comments --- depends/lua/src/ldo.c | 8 ++++---- library/Crashlog-windows.cpp | 27 ++++++++++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/depends/lua/src/ldo.c b/depends/lua/src/ldo.c index cef04888dc..6325ebf468 100644 --- a/depends/lua/src/ldo.c +++ b/depends/lua/src/ldo.c @@ -54,7 +54,7 @@ ** already dominated by the call itself, so it is cheap enough to run on ** every call, including hot paths like the overlay render loop. */ -#define checkcresults(L,ci,n,f) \ +#define DFHack_checkcresults(L,ci,n,f) \ if ((n) < 0 || (n) >= (L)->top - (ci)->func) \ luaG_runerror(L, \ "lua_CFunction %p reported invalid result count %d", (void *)(f), (n)); @@ -470,7 +470,7 @@ int luaD_precall (lua_State *L, StkId func, int nresults) { n = (*f)(L); /* do the actual call */ lua_lock(L); api_checknelems(L, n); - checkcresults(L, ci, n, f); /* DFHACK */ + DFHack_checkcresults(L, ci, n, f); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); return 1; } @@ -570,7 +570,7 @@ static void finishCcall (lua_State *L, int status) { n = (*ci->u.c.k)(L, status, ci->u.c.ctx); /* call continuation function */ lua_lock(L); api_checknelems(L, n); - checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ + DFHack_checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ luaD_poscall(L, ci, L->top - n, n); /* finish 'luaD_precall' */ } @@ -674,7 +674,7 @@ static void resume (lua_State *L, void *ud) { n = (*ci->u.c.k)(L, LUA_YIELD, ci->u.c.ctx); /* call continuation */ lua_lock(L); api_checknelems(L, n); - checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ + DFHack_checkcresults(L, ci, n, ci->u.c.k); /* DFHACK */ firstArg = L->top - n; /* yield results come from continuation */ } luaD_poscall(L, ci, firstArg, n); /* finish 'luaD_precall' */ diff --git a/library/Crashlog-windows.cpp b/library/Crashlog-windows.cpp index 6e54cf2e49..16b32fed56 100644 --- a/library/Crashlog-windows.cpp +++ b/library/Crashlog-windows.cpp @@ -67,6 +67,12 @@ const char *image_name_for(uintptr_t addr) return nullptr; } +void log_write_str(HANDLE log, const char *str) +{ + DWORD written; + WriteFile(log, str, (DWORD)strlen(str), &written, nullptr); +} + // Minimal output: append to stderr.log with raw Win32 IO so we depend on // as little process state as possible. void log_write(HANDLE log, const char *fmt, ...) @@ -81,14 +87,8 @@ void log_write(HANDLE log, const char *fmt, ...) if (len < 0 || len >= (int)sizeof(buf)) len = (int)sizeof(buf) - 1; // truncated output is still worth having buf[sizeof(buf) - 1] = '\0'; - DWORD written; - WriteFile(log, buf, len, &written, nullptr); -} + log_write_str(log, buf); -void log_write_str(HANDLE log, const char *str) -{ - DWORD written; - WriteFile(log, str, (DWORD)strlen(str), &written, nullptr); } // Walk the faulting thread's stack looking for instruction pointers inside @@ -101,7 +101,12 @@ bool lua_frames_on_stack(EXCEPTION_POINTERS *ep) { if (ctx.Rip >= lua_image.base && ctx.Rip < lua_image.end) return true; - if (!ctx.Rip || (ctx.Rip & 7)) + // Return addresses land on the instruction after a call and are + // normally unaligned, so the only sane early-out here is a null + // ip. Garbage values are harmless: RtlLookupFunctionEntry returns + // NULL for them, and the iteration bound plus the caller's SEH + // guard cap the damage from a corrupt stack. + if (!ctx.Rip) break; DWORD64 image_base = 0; @@ -115,7 +120,11 @@ bool lua_frames_on_stack(EXCEPTION_POINTERS *ep) } else { - // leaf function: return address is at [rsp] + // Leaf function: return address is at [rsp]. Unlike rip, rsp + // is architecturally required to stay 8-aligned, so a + // misaligned value here means the context is corrupt. + if (ctx.Rsp & 7) + break; ctx.Rip = *reinterpret_cast(ctx.Rsp); ctx.Rsp += 8; }