/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see .
*/
#include "crashhandler.h"
#include "logring.h"
#include "../qetversion.h"
#include
#include
#include
#include
#ifdef Q_OS_WIN
#include
#include
#include
#include
#include
#include
#else
#include
#include
#include
#include
// QET_CRASH_BACKTRACE is defined by CMake, via find_package(Backtrace),
// not by probing for the header here. exists on FreeBSD as
// well, but backtrace() is in a separate libexecinfo there, so a header
// probe compiles and then fails to link.
#ifdef QET_CRASH_BACKTRACE
#include
#endif
#endif
namespace {
// Everything the handler touches is preallocated here and filled in by
// install() (normal context, runs once at startup) -- nothing under the
// actual signal/exception path may allocate or touch QString/Qt.
const LogRing *g_ring = nullptr;
char g_dump_path[1024] = {};
char g_header[1024] = {};
int g_header_len = 0;
// Guards against two threads crashing at once, or the handler itself
// faulting while dumping: only the first crash writes a dump. See
// crashhandler.h invariant 4.
std::atomic g_already_dumped{false};
#ifndef Q_OS_WIN
// A stack-overflow SIGSEGV leaves no usable stack for a handler to run
// on at all, hence the alternate signal stack (invariant: sized well
// above any known SIGSTKSZ so this doesn't depend on
// sysconf(_SC_SIGSTKSZ), which some libc versions require at runtime
// rather than offering as a compile-time constant).
char g_altstack[65536];
const int kHandledSignals[] = {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL};
#ifdef QET_CRASH_BACKTRACE
// Preallocated here for the same reason as everything else in this block:
// backtrace() fills a caller-supplied array, so it needs no heap of its
// own, and backtrace_symbols_fd() writes straight to the fd (unlike
// backtrace_symbols(), which mallocs and is therefore unusable here).
void *g_backtrace_frames[64];
#endif
void restoreDefaultAndReraise(int sig)
{
struct sigaction sa {};
sa.sa_handler = SIG_DFL;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(sig, &sa, nullptr);
raise(sig);
}
void signalHandler(int sig)
{
if (g_already_dumped.exchange(true, std::memory_order_acq_rel)) {
// Not the first crash (concurrent fault on another thread, or
// this handler faulting while dumping): skip straight to
// restore-and-re-raise rather than risk a second, interleaved
// write to the same file.
restoreDefaultAndReraise(sig);
return;
}
// open/write/close, and backtrace_symbols_fd, are all on the POSIX
// async-signal-safe function list; nothing else is called here.
//
// Async-signal-safe is not the same as lock-free, which is why the
// order below matters. backtrace() unwinds through libgcc, which calls
// dl_iterate_phdr and takes the loader lock. Warming it in install()
// removes the allocation, not the lock -- so a crash that happens
// inside dlopen() (Qt plugin loading), or on a corrupted heap or
// stack, can leave this handler deadlocked or faulting a second time
// at the backtrace. Everything cheaper and more valuable is therefore
// written and flushed first: header, signal, then the log ring. If the
// backtrace never completes, the dump is still there and still useful.
const int fd = ::open(g_dump_path, O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (fd >= 0) {
if (g_header_len > 0) {
::write(fd, g_header, static_cast(g_header_len));
}
// Which signal killed it. The header is built once at install()
// and is therefore identical for every crash, so without this the
// dump never said what actually happened -- SIGSEGV and SIGABRT
// point at very different bugs.
char line[64];
int len = 0;
const char kSignalLabel[] = "Signal: ";
for (unsigned i = 0 ; i < sizeof(kSignalLabel) - 1 ; ++i) {
line[len++] = kSignalLabel[i];
}
len += CrashHandler::formatInt(line + len, static_cast(sizeof(line)) - len - 1, sig);
line[len++] = '\n';
::write(fd, line, static_cast(len));
// The ring first: it is the part that says what the program was
// doing, it costs one write, and it takes no lock.
const char kRingLabel[] = "--- log ---\n";
::write(fd, kRingLabel, sizeof(kRingLabel) - 1);
if (g_ring) {
g_ring->dumpToFd(fd);
}
#ifdef QET_CRASH_BACKTRACE
// Then where it was when it died. Last, deliberately: see the
// note above about the loader lock. backtrace() is warmed in
// install() so its first-call lazy resolution cannot allocate
// here, and backtrace_symbols_fd() writes to the fd without
// allocating -- unlike backtrace_symbols(), which mallocs and
// must not be used.
const char kBacktraceLabel[] = "--- backtrace ---\n";
::write(fd, kBacktraceLabel, sizeof(kBacktraceLabel) - 1);
const int frames = ::backtrace(g_backtrace_frames,
static_cast(sizeof(g_backtrace_frames)
/ sizeof(g_backtrace_frames[0])));
if (frames > 0) {
::backtrace_symbols_fd(g_backtrace_frames, frames, fd);
}
#endif
::close(fd);
}
restoreDefaultAndReraise(sig);
}
#else // Q_OS_WIN
// The dump is not written on the crashing thread but on a reporter thread
// started by install(). The crashing thread may have no stack left at all
// (a stack overflow, EXCEPTION_STACK_OVERFLOW), and even when it has some,
// the module lookup below takes the loader lock, which the crashing thread
// may be holding. The exception filter therefore only records what
// happened, wakes the reporter and waits for it, for a bounded time: a
// reporter that blocks costs the dump, never a hung process.
HANDLE g_reporter_go = nullptr;
HANDLE g_reporter_done = nullptr;
const DWORD kReporterTimeoutMs = 10000;
// What the crashing thread hands over. Filled in before g_reporter_go is
// signalled and only read after it, so no lock is needed.
enum class CrashKind { Exception, Abort, Fatal };
CrashKind g_crash_kind = CrashKind::Exception;
DWORD g_crash_code = 0; // the exception code, for CrashKind::Exception
const EXCEPTION_RECORD *g_crash_record = nullptr;
CONTEXT g_crash_context; // copied: the walk below modifies it
DWORD64 g_crash_stack_low = 0; // the crashing thread's stack, from its TIB,
DWORD64 g_crash_stack_high = 0; // so the walk never reads outside it
void writeText(int fd, const char *text)
{
_write(fd, text, static_cast(std::strlen(text)));
}
void writeHex(int fd, unsigned long long value)
{
char buffer[24];
_write(fd, buffer, static_cast(
CrashHandler::formatHex(buffer, sizeof(buffer), value)));
}
/**
The exception codes worth a name, so a report reads "access violation"
rather than a number to look up. Anything else is still printed as
its code.
*/
const char *exceptionName(DWORD code)
{
switch (code) {
case EXCEPTION_ACCESS_VIOLATION: return "access violation";
case EXCEPTION_STACK_OVERFLOW: return "stack overflow";
case EXCEPTION_ILLEGAL_INSTRUCTION: return "illegal instruction";
case EXCEPTION_PRIV_INSTRUCTION: return "privileged instruction";
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "integer divide by zero";
case EXCEPTION_INT_OVERFLOW: return "integer overflow";
case EXCEPTION_IN_PAGE_ERROR: return "in-page error";
case EXCEPTION_DATATYPE_MISALIGNMENT: return "datatype misalignment";
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "array bounds exceeded";
case EXCEPTION_BREAKPOINT: return "breakpoint";
case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "floating-point divide by zero";
case EXCEPTION_FLT_INVALID_OPERATION: return "floating-point invalid operation";
case 0xC0000374: return "heap corruption";
case 0xC0000409: return "stack buffer overrun";
case 0x20474343: return "uncaught C++ exception (GCC)";
case 0xE06D7363: return "uncaught C++ exception (MSVC)";
default: return nullptr;
}
}
/**
Writes @p address as "module.dll+0x1234", the form a symbolizer needs:
modules load at a different address on every run, the offset inside
the module does not. Returns false, writing nothing, when the address
is in no loaded module.
*/
bool writeModuleOffset(int fd, DWORD64 address)
{
PVOID base = nullptr;
if (!RtlPcToFileHeader(reinterpret_cast(address), &base) || !base) {
return false;
}
wchar_t path[MAX_PATH];
const DWORD length = GetModuleFileNameW(static_cast(base), path, MAX_PATH);
const wchar_t *name = path;
for (DWORD i = 0 ; i < length ; ++i) {
if (path[i] == L'\\' || path[i] == L'/') {
name = path + i + 1;
}
}
char name_utf8[MAX_PATH * 3];
const int written = length
? WideCharToMultiByte(CP_UTF8, 0, name, -1,
name_utf8, sizeof(name_utf8), nullptr, nullptr)
: 0;
writeText(fd, written > 0 ? name_utf8 : "?");
writeText(fd, "+");
writeHex(fd, address - reinterpret_cast(base));
return true;
}
#if defined(_M_X64) || defined(__x86_64__)
/**
Walks the crashed thread's stack from @p context, one "#NN module+offset"
line per frame. Uses the unwind tables every x64 image carries (GCC
emits them too), through RtlLookupFunctionEntry/RtlVirtualUnwind: no
dbghelp, no symbols, no allocation. Turning the offsets into function
names and lines is left to a symbolizer run later against the same
build, e.g. addr2line.
*/
void writeBacktrace(int fd, CONTEXT *context)
{
const DWORD64 low = g_crash_stack_low;
const DWORD64 high = g_crash_stack_high;
int printed = 0;
for (int i = 0 ; i < 64 ; ++i)
{
const DWORD64 pc = context->Rip;
//Frame 0 is always written, even outside any module: a call
//through a null pointer shows up exactly that way. Deeper
//frames outside every module are values the leaf rule below
//picked up from a helper such as __chkstk_ms that pushed
//registers without unwind data; skipping them keeps the list
//readable, and the walk re-synchronises on its own.
PVOID module_base = nullptr;
const bool in_module = RtlPcToFileHeader(reinterpret_cast(pc), &module_base)
&& module_base;
if (i == 0 || in_module) {
char frame[8] = {'#', 0, 0, ' ', 0};
frame[1] = static_cast('0' + (printed / 10) % 10);
frame[2] = static_cast('0' + printed % 10);
writeText(fd, frame);
if (!writeModuleOffset(fd, pc)) {
writeHex(fd, pc);
}
writeText(fd, "\n");
++printed;
}
DWORD64 image_base = 0;
PRUNTIME_FUNCTION function = pc
? RtlLookupFunctionEntry(pc, &image_base, nullptr)
: nullptr;
if (function) {
PVOID handler_data = nullptr;
DWORD64 establisher_frame = 0;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, image_base, pc, function,
context, &handler_data, &establisher_frame, nullptr);
} else {
//A leaf function (no unwind entry), or a call to a bad
//address: the return address is on top of the stack.
if (context->Rsp < low || context->Rsp + 8 > high) {
break;
}
context->Rip = *reinterpret_cast(context->Rsp);
context->Rsp += 8;
}
if (!context->Rip || context->Rsp < low || context->Rsp >= high) {
break;
}
}
}
#endif
/**
Runs on the reporter thread. Same order as the POSIX handler, and for
the same reason: what crashed, then the log ring, then the backtrace,
so that whatever the walk runs into, the cheaper parts are already on
disk.
*/
void writeWindowsDump()
{
int fd = -1;
const errno_t err = _sopen_s(&fd, g_dump_path,
_O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY,
_SH_DENYWR, _S_IREAD | _S_IWRITE);
if (err != 0 || fd < 0) {
return;
}
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_crash_kind == CrashKind::Exception && g_crash_record) {
writeText(fd, "Exception: ");
writeHex(fd, g_crash_code);
if (const char *name = exceptionName(g_crash_code)) {
writeText(fd, " (");
writeText(fd, name);
writeText(fd, ")");
}
writeText(fd, "\nAt: ");
const DWORD64 address = reinterpret_cast(g_crash_record->ExceptionAddress);
if (!writeModuleOffset(fd, address)) {
writeHex(fd, address);
}
writeText(fd, "\n");
//For an access violation, what was accessed: a small address
//such as 0x10 is a null pointer's member, a large one a
//dangling or corrupted pointer.
if (g_crash_code == EXCEPTION_ACCESS_VIOLATION
&& g_crash_record->NumberParameters >= 2) {
const ULONG_PTR kind = g_crash_record->ExceptionInformation[0];
writeText(fd, kind == 0 ? "Reading: "
: kind == 1 ? "Writing: "
: "Executing: ");
writeHex(fd, g_crash_record->ExceptionInformation[1]);
writeText(fd, "\n");
}
} else if (g_crash_kind == CrashKind::Fatal) {
writeText(fd, "Fatal: qFatal() (its message is the last Fatal line of the log)\n");
} else {
writeText(fd, "Signal: SIGABRT (abort)\n");
}
writeText(fd, "--- log ---\n");
if (g_ring) {
g_ring->dumpToFd(fd);
}
#if defined(_M_X64) || defined(__x86_64__)
writeText(fd, "--- backtrace ---\n");
writeBacktrace(fd, &g_crash_context);
#endif
_close(fd);
}
DWORD WINAPI reporterThread(LPVOID)
{
WaitForSingleObject(g_reporter_go, INFINITE);
writeWindowsDump();
SetEvent(g_reporter_done);
return 0;
}
/**
Hands the crash to the reporter thread and waits for it. Uses almost no
stack of its own, which is what lets a stack overflow be reported.
*/
void reportFromCrashingThread()
{
const NT_TIB *tib = reinterpret_cast(NtCurrentTeb());
g_crash_stack_low = reinterpret_cast(tib->StackLimit);
g_crash_stack_high = reinterpret_cast(tib->StackBase);
if (g_reporter_go && g_reporter_done) {
SetEvent(g_reporter_go);
WaitForSingleObject(g_reporter_done, kReporterTimeoutMs);
}
}
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *pointers)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
g_crash_kind = CrashKind::Exception;
g_crash_record = pointers->ExceptionRecord;
g_crash_code = pointers->ExceptionRecord->ExceptionCode;
g_crash_context = *pointers->ContextRecord;
reportFromCrashingThread();
// Do not suppress Windows Error Reporting / an attached debugger --
// same invariant as re-raising on POSIX (see crashhandler.h,
// invariant 3).
return EXCEPTION_CONTINUE_SEARCH;
}
/**
abort() -- which std::terminate() and a failed assert end in -- does
not raise an SEH exception on Windows, so the filter above never sees
it. The C runtime raises SIGABRT first, though. (qFatal() does not
get here: see CrashHandler::reportFatal().)
*/
void windowsAbortHandler(int)
{
bool expected = false;
if (g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
g_crash_kind = CrashKind::Abort;
RtlCaptureContext(&g_crash_context);
reportFromCrashingThread();
}
//Let abort() carry on to its default end, so Windows Error
//Reporting still sees the crash (invariant 3).
signal(SIGABRT, SIG_DFL);
}
#endif
} // namespace
// Async-signal-safe decimal formatting: write() takes a buffer, and there
// is no snprintf on the POSIX async-signal-safe list. Writes into a
// caller-owned buffer (stack, not heap) and returns the length used.
//
// Defined as CrashHandler::formatInt rather than a file-local helper only
// so tst_crashhandler can reach it; it is not called anywhere else.
int CrashHandler::formatInt(char *buffer, int size, int value)
{
if (size <= 0) return 0;
if (value == 0) {
buffer[0] = '0';
return 1;
}
char scratch[16];
int n = 0;
bool negative = value < 0;
unsigned int v = negative ? static_cast(-(value + 1)) + 1u
: static_cast(value);
while (v > 0 && n < static_cast(sizeof(scratch))) {
scratch[n++] = static_cast('0' + (v % 10));
v /= 10;
}
int len = 0;
if (negative && len < size) buffer[len++] = '-';
while (n > 0 && len < size) buffer[len++] = scratch[--n];
return len;
}
void CrashHandler::reportFatal()
{
#ifdef Q_OS_WIN
bool expected = false;
if (g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
g_crash_kind = CrashKind::Fatal;
RtlCaptureContext(&g_crash_context);
reportFromCrashingThread();
}
#endif
}
// Same constraints as formatInt(): caller-owned buffer, no allocation.
// Always writes the "0x" prefix and at least one digit when there is room.
int CrashHandler::formatHex(char *buffer, int size, unsigned long long value)
{
if (size <= 0) return 0;
char scratch[16];
int n = 0;
do {
scratch[n++] = "0123456789abcdef"[value & 0xf];
value >>= 4;
} while (value != 0 && n < static_cast(sizeof(scratch)));
int len = 0;
const char kPrefix[] = "0x";
for (unsigned i = 0 ; i < sizeof(kPrefix) - 1 && len < size ; ++i) {
buffer[len++] = kPrefix[i];
}
while (n > 0 && len < size) buffer[len++] = scratch[--n];
return len;
}
void CrashHandler::install(const LogRing *ring, const QString &dump_path)
{
g_ring = ring;
const QByteArray path_utf8 = dump_path.toUtf8();
std::strncpy(g_dump_path, path_utf8.constData(), sizeof(g_dump_path) - 1);
const QByteArray header = QByteArray("QET crash dump\n")
+ "Version: " + QetVersion::displayedVersion().toUtf8() + "\n"
+ "Git: " GIT_COMMIT_SHA "\n"
+ "OS: " + QSysInfo::prettyProductName().toUtf8() + " (" + QSysInfo::currentCpuArchitecture().toUtf8() + ")\n"
+ "Qt: " QT_VERSION_STR "\n"
+ "---\n";
g_header_len = qMin(header.size(), static_cast(sizeof(g_header)) - 1);
std::memcpy(g_header, header.constData(), static_cast(g_header_len));
#ifdef Q_OS_WIN
//The reporter thread and its two events are made here, in normal
//context, because nothing can be created once the crash happens.
//The thread only waits; its 64 KiB is reserved, not committed.
g_reporter_go = CreateEventW(nullptr, FALSE, FALSE, nullptr);
g_reporter_done = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (g_reporter_go && g_reporter_done) {
HANDLE thread = CreateThread(nullptr, 65536, reporterThread, nullptr,
STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr);
if (thread) {
CloseHandle(thread);
} else {
CloseHandle(g_reporter_go);
CloseHandle(g_reporter_done);
g_reporter_go = g_reporter_done = nullptr;
}
}
//After a stack overflow the filter still needs a little stack to
//hand over to the reporter. Keep some in reserve on the main
//thread, where the GUI's deep recursions happen. install() runs on
//it, and the guarantee only applies to the calling thread.
ULONG stack_guarantee = 32768;
SetThreadStackGuarantee(&stack_guarantee);
SetUnhandledExceptionFilter(windowsExceptionFilter);
signal(SIGABRT, windowsAbortHandler);
#else
stack_t ss;
ss.ss_sp = g_altstack;
ss.ss_size = sizeof(g_altstack);
ss.ss_flags = 0;
sigaltstack(&ss, nullptr);
#ifdef QET_CRASH_BACKTRACE
// Warm the unwinder. backtrace()'s *first* call resolves dynamic
// linker state and may allocate; every call after that does not. Doing
// it here, in normal context, is what lets the handler call it without
// breaking invariant 2. The result is deliberately discarded.
void *warmup[4];
(void) ::backtrace(warmup, 4);
#endif
struct sigaction sa {};
sa.sa_handler = signalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK;
for (int sig : kHandledSignals) {
sigaction(sig, &sa, nullptr);
}
#endif
}