cnq3/code/linux/linux_signals.cpp

602 lines
14 KiB
C++

/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.
This file is part of Quake III Arena source code.
Quake III Arena source code 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.
Quake III Arena source code 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 Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
#include <signal.h>
#include <execinfo.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dlfcn.h>
#include <cxxabi.h>
#include <sys/stat.h>
#include <sys/types.h>
#define UNW_LOCAL_ONLY
#include "../libunwind/libunwind.h"
#include "../qcommon/q_shared.h"
#include "../qcommon/qcommon.h"
#include "../qcommon/crash.h"
#include "linux_local.h"
/*
A sub-process created by fork from a signal handler will most certainly
run in the context of the signal handler and should therefore also be
limited to calling async-signal-safe functions only.
So the idea of launching a new process from a handler for doing the heavy
lifting (resolving symbols etc) is completely dead.
*/
// the app crashed
// columns: Symbol, Desc
#define CRASH_SIGNAL_LIST(X) \
X(SIGILL, "illegal instruction") \
X(SIGIOT, "IOT trap (a synonym for SIGABRT)") \
X(SIGBUS, "bus error (bad memory access)") \
X(SIGFPE, "fatal arithmetic error") \
X(SIGSEGV, "invalid memory reference")
// the app should terminate normally
// columns: Symbol, Desc
#define TERM_SIGNAL_LIST(X) \
X(SIGHUP, "hangup detected on controlling terminal or death of controlling process") \
X(SIGQUIT, "quit from keyboard") \
X(SIGTRAP, "trace/breakpoint trap") \
X(SIGTERM, "termination signal") \
X(SIGINT, "interrupt")
#define SIGNAL_ITEM(Symbol, Desc) 1 +
static const int sig_crashSignalCount = CRASH_SIGNAL_LIST(SIGNAL_ITEM) 0;
static const int sig_termSignalCount = TERM_SIGNAL_LIST(SIGNAL_ITEM) 0;
#undef SIGNAL_ITEM
#define SIGNAL_ITEM(Symbol, Desc) Symbol,
static const int sig_crashSignals[sig_crashSignalCount + 1] =
{
CRASH_SIGNAL_LIST(SIGNAL_ITEM)
0
};
static const int sig_termSignals[sig_termSignalCount + 1] =
{
TERM_SIGNAL_LIST(SIGNAL_ITEM)
0
};
#undef SIGNAL_ITEM
static char sig_backTraceFilePath[MAX_OSPATH];
static char sig_jsonFilePath[MAX_OSPATH];
static void Sig_Unwind_OpenLibrary();
static void Sig_Unwind_GetContext();
static void Sig_Unwind_Print(FILE* file);
static void Sig_Unwind_PrintASS(int fd); // async-signal-safe
// both of these can call Com_Error, which is not safe
#define Q_strncpyz Sig_strncpyz
#define Q_strcat Sig_strcat
// make sure we don't use these
#define strcpy do_not_use_strcpy
#define strcat do_not_use_strcat
// async-signal-safe
static void Sig_strncpyz(char* dest, const char* src, int size)
{
if (!dest || !src || size < 1)
return;
strncpy(dest, src, size - 1);
dest[size - 1] = '\0';
}
// async-signal-safe
static void Sig_strcat(char* dest, int size, const char* src)
{
if (!dest || !src || size < 1)
return;
const int destLength = strlen(dest);
if (destLength >= size)
return;
Sig_strncpyz(dest + destLength, src, size - destLength);
}
static const char* Sig_GetDescription(int sig)
{
#define SIGNAL_ITEM(Symbol, Desc) case Symbol: return Desc;
switch (sig)
{
CRASH_SIGNAL_LIST(SIGNAL_ITEM)
TERM_SIGNAL_LIST(SIGNAL_ITEM)
default: return "unhandled signal";
}
#undef SIGNAL_ITEM
}
static const char* Sig_GetName(int sig)
{
#define SIGNAL_ITEM(Symbol, Desc) case Symbol: return #Symbol;
switch (sig)
{
CRASH_SIGNAL_LIST(SIGNAL_ITEM)
TERM_SIGNAL_LIST(SIGNAL_ITEM)
default: return "unhandled signal";
}
#undef SIGNAL_ITEM
}
static void Sig_UpdateFilePaths()
{
// gmtime and va (sprintf) are not async-signal-safe
const time_t epochTime = time(NULL);
struct tm* const utcTime = gmtime(&epochTime);
const int y = 1900 + utcTime->tm_year;
const int m = utcTime->tm_mon + 1;
const int d = utcTime->tm_mday;
const int h = utcTime->tm_hour;
const int mi = utcTime->tm_min;
const int s = utcTime->tm_sec;
const char* const bn = va(
"%s-crash-%d.%02d.%02d-%02d.%02d.%02d",
q_argv[0], y, m, d, h, mi, s);
char baseName[MAX_OSPATH];
Q_strncpyz(baseName, bn, sizeof(baseName));
Q_strncpyz(sig_backTraceFilePath, va("%s.bt", baseName), sizeof(sig_backTraceFilePath));
Q_strncpyz(sig_jsonFilePath, va("%s.json", baseName), sizeof(sig_jsonFilePath));
}
static const char* Sig_BackTraceFilePath()
{
if (sig_backTraceFilePath[0] == '\0')
return "cnq3-crash.bt";
return sig_backTraceFilePath;
}
static const char* Sig_JSONFilePath()
{
if (sig_jsonFilePath[0] == '\0')
return "cnq3-crash.json";
return sig_jsonFilePath;
}
static void Sig_WriteJSON(int sig)
{
FILE* const file = fopen(Sig_JSONFilePath(), "w");
if (file == NULL)
return;
JSONW_BeginFile(file);
JSONW_IntegerValue("signal", sig);
JSONW_StringValue("signal_name", Sig_GetName(sig));
JSONW_StringValue("signal_description", Sig_GetDescription(sig));
Crash_PrintToFile(q_argv[0]);
JSONW_EndFile();
fclose(file);
}
static void Sig_Backtrace_Print(FILE* file)
{
void* addresses[64];
const int addresscount = backtrace(addresses, sizeof(addresses));
if (addresscount > 0)
{
fflush(file);
backtrace_symbols_fd(addresses, addresscount, fileno(file));
}
else
{
fprintf(file, "The call to backtrace failed\r\n");
}
}
static void libbt_ErrorCallback(void* data, const char* msg, int errnum)
{
fprintf((FILE*)data, "libbacktrace error: %s (%d)\r\n", msg, errnum);
}
static void Sig_Print(int fd, const char* msg)
{
write(fd, msg, strlen(msg));
}
static void Sig_PrintLine(int fd, const char* msg)
{
write(fd, msg, strlen(msg));
write(fd, "\r\n", 2);
}
static void Sig_PrintSignalCaught(int sig)
{
// fprintf is not async-signal-safe, but write is
Sig_Print(STDERR_FILENO, "\nSignal caught: ");
Sig_Print(STDERR_FILENO, Sig_GetName(sig));
Sig_Print(STDERR_FILENO, " (");
Sig_Print(STDERR_FILENO, Sig_GetDescription(sig));
Sig_Print(STDERR_FILENO, ")\r\n");
}
static void Sig_HandleExit()
{
Lin_ConsoleInputShutdown();
}
static volatile sig_atomic_t sig_termRequested = 0;
static void Sig_HandleTermSignal(int sig)
{
Sig_PrintSignalCaught(sig);
sig_termRequested = 1;
}
static void Sig_PrintAttempt(const char* what)
{
Sig_Print(STDERR_FILENO, "Attempting to ");
Sig_Print(STDERR_FILENO, what);
Sig_Print(STDERR_FILENO, "...");
}
static void Sig_PrintDone()
{
Sig_Print(STDERR_FILENO, " done\r\n");
}
static void Sig_HandleCrash(int sig)
{
//
// Phase 1: async-signal-safe code
//
Sig_Unwind_GetContext();
const int fd = open(Sig_BackTraceFilePath(), O_CREAT | O_TRUNC | O_WRONLY);
if (fd != -1)
{
Sig_PrintAttempt("write safe libunwind stack trace");
Sig_PrintLine(fd, "safe libunwind stack trace:");
Sig_Unwind_PrintASS(fd);
Sig_PrintDone();
Sig_PrintAttempt("write safe mod stack traces");
Sig_PrintLine(fd, "\r\n\r\nmod stack traces:");
Crash_PrintVMStackTracesASS(fd);
Sig_PrintDone();
// user R+W - group R - others R
chmod(Sig_BackTraceFilePath(), S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
}
//
// Phase 2: buckle up
//
Sig_PrintAttempt("restore tty settings");
Lin_ConsoleInputShutdown();
Sig_PrintDone();
Sig_PrintAttempt("write JSON report");
Sig_WriteJSON(sig);
Sig_PrintDone();
if (fd != -1)
{
FILE* const file = fdopen(fd, "a");
Sig_PrintAttempt("write backtrace stack trace");
fprintf(file, "\r\n\r\nbacktrace_symbols_fd stack trace:\r\n");
Sig_Backtrace_Print(file);
fflush(file);
Sig_PrintDone();
Sig_PrintAttempt("write detailed libunwind stack trace");
fprintf(file, "\r\n\r\ndetailed libunwind stack trace:\r\n");
Sig_Unwind_Print(file);
fflush(file);
Sig_PrintDone();
}
}
static void Sig_HandleCrashSignal(int sig)
{
static volatile sig_atomic_t counter = 0;
Sig_PrintSignalCaught(sig);
++counter;
if (counter == 1)
Sig_HandleCrash(sig);
else if (counter >= 2)
Sig_PrintLine(STDERR_FILENO, "Double fault! Shutting down immediately.");
_exit(1);
}
static void Sig_RegisterSignals(const int* signals, int count, void (*handler)(int), int flags)
{
sigset_t mask;
sigemptyset(&mask);
struct sigaction action;
memset(&action, 0, sizeof(action));
action.sa_handler = handler;
action.sa_mask = mask;
action.sa_flags = flags;
for (int i = 0; i < count; ++i)
sigaction(signals[i], &action, NULL);
}
void SIG_InitChild()
{
// This is unfortunately needed because some code might
// call exit and bypass all the clean-up work without
// there ever being a real crash.
// This happens for instance with the "fatal IO error"
// of the X server.
atexit(Sig_HandleExit);
// The crash handler gets SA_NODEFER so that we can handle faults it generates.
Sig_RegisterSignals(sig_crashSignals, sig_crashSignalCount, Sig_HandleCrashSignal, SA_NODEFER);
Sig_RegisterSignals(sig_termSignals, sig_termSignalCount, Sig_HandleTermSignal, 0);
// Must do this now because it's not safe in a signal handler.
Sig_UpdateFilePaths();
Sig_Unwind_OpenLibrary();
}
void SIG_InitParent()
{
// Signals are sent to both the parent and the child, which can be an issue.
// Our parent process has to ignore all signals handled by the child and
// let the child do whatever needs to be done.
for (int i = 0; i < sig_crashSignalCount; ++i)
signal(sig_crashSignals[i], SIG_IGN);
for (int i = 0; i < sig_termSignalCount; ++i)
signal(sig_termSignals[i], SIG_IGN);
}
void SIG_Frame()
{
// Must do this now because it's not safe in a signal handler.
Sig_UpdateFilePaths();
if (sig_termRequested != 0)
Com_Quit(0);
}
// We require libunwind8 specifically for now.
// Even if there was a general symlink without the number,
// it would still not be safe to load it because
// the library provides no way to query its version.
#define LIBUNWIND_PATH "libunwind-" XSTRING(UNW_TARGET) ".so.8"
struct libUnwind_t
{
unw_context_t context;
int (*getcontext)(unw_context_t *);
int (*init_local)(unw_cursor_t *, unw_context_t *);
int (*step)(unw_cursor_t *);
int (*get_reg)(unw_cursor_t *, unw_regnum_t, unw_word_t *);
int (*get_proc_name)(unw_cursor_t *, char *, size_t, unw_word_t *);
void* handle;
qbool valid;
};
static libUnwind_t unw;
static void Sig_Unwind_GetContext()
{
if (!unw.valid)
return;
if (unw.getcontext(&unw.context) != 0)
{
Sig_PrintLine(STDERR_FILENO, "The call to libunwind's getcontext failed");
unw.valid = qfalse;
}
}
static int Sig_Unwind_GetFunction(void** func, const char* name)
{
*func = dlsym(unw.handle, name);
return *func != NULL;
}
static void Sig_Unwind_OpenLibrary()
{
// dlopen, dlsym, fprintf are not async-signal-safe
unw.valid = qfalse;
unw.handle = dlopen(LIBUNWIND_PATH, RTLD_NOW);
if (unw.handle == NULL)
{
const char* errorMsg = dlerror();
if (errorMsg != NULL)
fprintf(stderr, "\nFailed to load %s: %s\n", LIBUNWIND_PATH, errorMsg);
else
fprintf(stderr, "\nFailed to find/load %s\n", LIBUNWIND_PATH);
return;
}
#define GET2(Var, Name) \
if (!Sig_Unwind_GetFunction((void**)&unw.Var, Name)) \
{ \
fprintf(stderr, "\nFailed to find libunwind function %s\n", Name); \
return; \
}
#define GET(Name) GET2(Name, XSTRING(UNW_OBJ(Name)))
GET2(getcontext, "_Ux86_64_getcontext");
GET(init_local);
GET(step);
GET(get_reg);
GET(get_proc_name);
#undef GET
#undef GET2
unw.valid = qtrue;
}
static int Sig_Unwind_GetFileAndLine(unw_word_t addr, char* file, size_t flen, int* line)
{
static char buf[1024];
sprintf(buf, "addr2line -C -e %s -f -i %lx", q_argv[0], (unsigned long)addr);
FILE* f = popen(buf, "r");
if (f == NULL)
return 0;
fgets(buf, sizeof(buf), f); // function name
fgets(buf, sizeof(buf), f); // file and line
pclose(f);
if (buf[0] == '?')
return 0;
// file name before ':'
char* p = buf;
while (*p != ':')
++p;
*p = '\0';
// skip the folder names
char* name = strrchr(buf, '/');
name = name ? (name + 1) : buf;
// line number
++p;
Q_strncpyz(file, name, flen);
sscanf(p, "%d", line);
return 1;
}
void Sig_Unwind_Print(FILE* fp)
{
static char name[1024];
static char file[1024];
if (!unw.valid)
return;
unw_cursor_t cursor;
if (unw.init_local(&cursor, &unw.context) != 0)
{
fprintf(fp, "The call to libunwind's init_local failed\r\n");
return;
}
while (unw.step(&cursor) > 0)
{
const char* func = name;
unw_word_t ip, sp, offp;
if (unw.get_proc_name(&cursor, name, sizeof(name), &offp))
{
Q_strncpyz(name, "???", sizeof(name));
}
else
{
int status;
char* niceName = abi::__cxa_demangle(name, NULL, NULL, &status);
if (status == 0)
{
Q_strncpyz(name, niceName, sizeof(name));
free(niceName);
}
}
int line = 0;
unw.get_reg(&cursor, UNW_REG_IP, &ip);
if (Sig_Unwind_GetFileAndLine((long)ip, file, sizeof(file), &line))
fprintf(fp, "%s at %s:%d\n", name, file, line);
else
fprintf(fp, "%s\n", name);
}
}
// async-signal-safe
void Sig_Unwind_PrintASS(int fd)
{
static char name[1024];
if (!unw.valid)
return;
unw_cursor_t cursor;
if (unw.init_local(&cursor, &unw.context) != 0)
return;
while (unw.step(&cursor) > 0)
{
unw_word_t offp;
if (unw.get_proc_name(&cursor, name, sizeof(name), &offp))
Q_strncpyz(name, "???", sizeof(name));
Sig_PrintLine(fd, name);
}
}