/*
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
#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
LONG WINAPI windowsExceptionFilter(EXCEPTION_POINTERS *)
{
bool expected = false;
if (!g_already_dumped.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
return EXCEPTION_CONTINUE_SEARCH;
}
int fd = -1;
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) {
if (g_header_len > 0) {
_write(fd, g_header, g_header_len);
}
if (g_ring) {
g_ring->dumpToFd(fd);
}
_close(fd);
}
// 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;
}
#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::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
SetUnhandledExceptionFilter(windowsExceptionFilter);
#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
}