pissircd/src/crashreport.c

898 lines
21 KiB
C

/* UnrealIRCd crash reporter code.
* (C) Copyright 2015-2019 Bram Matthys ("Syzop") and the UnrealIRCd Team.
* License: GPLv2 or later
*/
#include "unrealircd.h"
#ifdef _WIN32
extern void StartUnrealAgain(void);
#endif
#include "version.h"
extern char *getosname(void);
char *find_best_coredump(void)
{
static char best_fname[512];
time_t best_time = 0, t;
struct dirent *dir;
#ifndef _WIN32
DIR *fd = opendir(TMPDIR);
if (!fd)
return NULL;
*best_fname = '\0';
while ((dir = readdir(fd)))
{
char *fname = dir->d_name;
if (strstr(fname, "core") && !strstr(fname, ".so") &&
!strstr(fname, ".conf") && !strstr(fname, ".txt") &&
!strstr(fname, ".done"))
{
char buf[512];
snprintf(buf, sizeof(buf), "%s/%s", TMPDIR, fname);
t = get_file_time(buf);
if (t && (t > best_time))
{
best_time = t;
strlcpy(best_fname, buf, sizeof(best_fname));
}
}
}
closedir(fd);
#else
/* Windows */
WIN32_FIND_DATA hData;
HANDLE hFile;
hFile = FindFirstFile("unrealircd.*.core", &hData);
if (hFile == INVALID_HANDLE_VALUE)
return NULL;
do
{
char *fname = hData.cFileName;
if (!strstr(fname, ".done"))
{
char buf[512];
strlcpy(buf, fname, sizeof(buf));
t = get_file_time(buf);
if (t && (t > best_time))
{
best_time = t;
strlcpy(best_fname, buf, sizeof(best_fname));
}
}
} while (FindNextFile(hFile, &hData));
FindClose(hFile);
#endif
if (*best_fname)
return best_fname;
return NULL; /* none found */
}
/** Find the latest AddressSanitizer log file */
char *find_best_asan_log(void)
{
#ifndef _WIN32
static char best_fname[512];
time_t best_time = 0, t;
struct dirent *dir;
DIR *fd = opendir(TMPDIR);
if (!fd)
return NULL;
*best_fname = '\0';
while ((dir = readdir(fd)))
{
char *fname = dir->d_name;
if (strstr(fname, "unrealircd_asan.") && !strstr(fname, ".so") &&
!strstr(fname, ".conf") && !strstr(fname, ".txt") &&
!strstr(fname, ".done"))
{
char buf[512];
snprintf(buf, sizeof(buf), "%s/%s", TMPDIR, fname);
t = get_file_time(buf);
if (t && (t > best_time))
{
best_time = t;
strlcpy(best_fname, buf, sizeof(best_fname));
}
}
}
closedir(fd);
return *best_fname ? best_fname : NULL;
#else
return NULL;
#endif
}
#define EL_AR_MAX MAXPARA
char **explode(char *str, char *delimiter)
{
static char *ret[EL_AR_MAX+1];
static char buf[1024];
char *p, *name;
int cnt = 0;
memset(&ret, 0, sizeof(ret)); /* make sure all elements are NULL */
strlcpy(buf, str, sizeof(buf));
for (name = strtoken(&p, buf, delimiter); name; name = strtoken(&p, NULL, delimiter))
{
ret[cnt++] = name;
if (cnt == EL_AR_MAX)
break;
}
ret[cnt] = NULL;
return ret;
}
void crash_report_fix_libs(char *coredump, int *thirdpartymods)
{
#ifndef _WIN32
FILE *fd;
char cmd[512], buf[1024];
/* This is needed for this function to work, but we keep it since it's
* useful in general to have the bug report in English as well.
*/
setenv("LANG", "C", 1);
setenv("LC_ALL", "C", 1);
snprintf(cmd, sizeof(cmd), "echo info sharedlibrary|gdb %s/unrealircd %s 2>&1",
BINDIR, coredump);
fd = popen(cmd, "r");
if (!fd)
return;
while((fgets(buf, sizeof(buf), fd)))
{
char *file, *path;
char target[512];
char **arr;
stripcrlf(buf);
if (strstr(buf, ".third."))
*thirdpartymods = 1;
/* Output we are interested is something like this:
* <many spaces> No /home/blabla/unrealircd/tmp/5114DF16.m_kick.so
*/
if (!strstr(buf, " No "))
continue;
path = strchr(buf, '/');
if (!path)
continue;
if (!strstr(path, TMPDIR))
continue; /* we only care about our TMPDIR stuff */
file = strrchr(path, '/');
if (!file)
continue;
file++;
/* files have the following two formats:
* 5BE7DF9.m_svsnline.so for modules/m_svsnline.so
* 300AA138.chanmodes.nokick.so for modules/chanmodes/nokick.so
*/
arr = explode(file, ".");
if (!arr[3])
snprintf(target, sizeof(target), "%s/%s.%s", MODULESDIR, arr[1], arr[2]);
else
snprintf(target, sizeof(target), "%s/%s/%s.%s", MODULESDIR, arr[1], arr[2], arr[3]);
if (!file_exists(target))
{
printf("WARNING: could not resolve %s: %s does not exist\n", path, target);
} else {
if (symlink(target, path) < 0)
printf("WARNING: could not create symlink %s -> %s\n", path, target);
}
}
pclose(fd);
#endif
}
int crash_report_backtrace(FILE *reportfd, char *coredump)
{
FILE *fd;
char cmd[512], buf[1024];
int n;
#ifndef _WIN32
snprintf(buf, sizeof(buf), "%s/gdb.commands", TMPDIR);
fd = fopen(buf, "w");
if (!fd)
{
printf("ERROR: Could not write to %s.\n", buf);
return 0;
}
fprintf(fd, "frame\n"
"echo \\n\n"
"list\n"
"echo \\n\n"
"x/s our_mod_version\n"
"echo \\n\n"
"x/s backupbuf\n"
"echo \\n\n"
"bt\n"
"echo \\n\n"
"bt full\n"
"echo \\n\n"
"quit\n");
fclose(fd);
snprintf(cmd, sizeof(cmd), "gdb -batch -x %s %s/unrealircd %s 2>&1",
buf, BINDIR, coredump);
fd = popen(cmd, "r");
if (!fd)
return 0;
fprintf(reportfd, "START OF BACKTRACE\n");
while((fgets(buf, sizeof(buf), fd)))
{
stripcrlf(buf);
fprintf(reportfd, " %s\n", buf);
}
n = pclose(fd);
fprintf(reportfd, "END OF BACKTRACE\n");
if (WEXITSTATUS(n) == 127)
return 0;
return 1;
#else
fd = fopen(coredump, "r");
if (!fd)
return 0;
fprintf(reportfd, "START OF CRASH DUMP\n");
while((fgets(buf, sizeof(buf), fd)))
{
stripcrlf(buf);
fprintf(reportfd, " %s\n", buf);
}
fclose(fd);
fprintf(reportfd, "END OF CRASH DUMP\n");
return 1;
#endif
}
int crash_report_asan_log(FILE *reportfd, char *coredump)
{
#ifndef _WIN32
time_t coretime, asantime;
FILE *fd;
char buf[1024];
char *asan_log = find_best_asan_log();
int n;
if (!asan_log)
return 0;
coretime = get_file_time(coredump);
asantime = get_file_time(asan_log);
fprintf(reportfd, "ASan log file found '%s' which is %ld seconds older than core file\n",
asan_log,
(long)((long)(coretime) - (long)asantime));
fd = fopen(asan_log, "r");
if (!fd)
{
fprintf(reportfd, "Could not open ASan log (%s)\n", strerror(errno));
return 0;
}
fprintf(reportfd, "START OF ASAN LOG\n");
while((fgets(buf, sizeof(buf), fd)))
{
stripcrlf(buf);
fprintf(reportfd, " %s\n", buf);
}
n = fclose(fd);
fprintf(reportfd, "END OF ASAN LOG\n");
if (WEXITSTATUS(n) == 127)
return 0;
return 1;
#else
return 0;
#endif
}
void crash_report_header(FILE *reportfd, char *coredump)
{
time_t t;
fprintf(reportfd, "== UNREALIRCD CRASH REPORT ==\n"
"\n"
"SYSTEM INFORMATION:\n");
fprintf(reportfd, "UnrealIRCd version: %s\n", VERSIONONLY);
#if defined(__VERSION__)
fprintf(reportfd, " Compiler: %s\n", __VERSION__);
#endif
fprintf(reportfd, " Operating System: %s\n", MYOSNAME);
fprintf(reportfd, "Using core file: %s\n", coredump);
t = get_file_time(coredump);
if (t != 0)
{
fprintf(reportfd, "Crash date/time: %s\n", myctime(t) ? myctime(t) : "???");
fprintf(reportfd, " Crash secs ago: %ld\n",
(long)(time(NULL) - t));
} else {
fprintf(reportfd, "Crash date/time: UNKNOWN\n");
fprintf(reportfd, " Crash secs ago: UNKNOWN\n");
}
fprintf(reportfd, "\n");
}
/** Checks if the binary is newer than the coredump.
* If that's the case (1) then the core dump is likely not very usable.
*/
int corefile_vs_binary_mismatch(char *coredump)
{
#ifndef _WIN32
time_t core, binary;
char fname[512];
snprintf(fname, sizeof(fname), "%s/unrealircd", BINDIR);
core = get_file_time(coredump);
binary = get_file_time(fname);
if (!core || !binary)
return 0; /* don't know then */
if (binary > core)
return 1; /* yup, mismatch ;/ */
return 0; /* GOOD! */
#else
return 0; /* guess we don't check this on Windows? Or will we check UnrealIRCd.exe... hmm.. yeah maybe good idea */
#endif
}
int attach_file(FILE *fdi, FILE *fdo)
{
char binbuf[60];
char printbuf[100];
size_t n, total = 0;
fprintf(fdo, "\n*** ATTACHMENT ****\n");
while((n = fread(binbuf, 1, sizeof(binbuf), fdi)) > 0)
{
b64_encode(binbuf, n, printbuf, sizeof(printbuf));
fprintf(fdo, "%s\n", printbuf);
total += strlen(printbuf);
if (total > 15000000)
return 0; /* Safety limit */
}
fprintf(fdo, "*** END OF ATTACHMENT ***\n");
return 1;
}
/** Figure out the libc library name (.so file), copy it to tmp/
* to include it in the bug report. This can improve the backtrace
* a lot (read: make it actually readable / useful) in case we
* crash in a libc function.
*/
char *copy_libc_so(void)
{
#ifdef _WIN32
return "";
#else
FILE *fd;
char buf[1024];
static char ret[512];
char *basename = NULL, *libcname = NULL, *p, *start;
snprintf(buf, sizeof(buf), "ldd %s/unrealircd 2>/dev/null", BINDIR);
fd = popen(buf, "r");
if (!fd)
return "";
while ((fgets(buf, sizeof(buf), fd)))
{
stripcrlf(buf);
p = strstr(buf, "libc.so");
if (!p)
continue;
basename = p;
p = strchr(p, ' ');
if (!p)
continue;
*p++ = '\0';
p = strstr(p, "=> ");
if (!p)
continue;
start = p += 3; /* skip "=> " */
p = strchr(start, ' ');
if (!p)
continue;
*p = '\0';
libcname = start;
break;
}
pclose(fd);
if (!basename || !libcname)
return ""; /* not found, weird */
snprintf(ret, sizeof(ret), "%s/%s", TMPDIR, basename);
if (!unreal_copyfile(libcname, ret))
return ""; /* copying failed */
return ret;
#endif
}
int attach_coredump(FILE *fdo, char *coredump)
{
FILE *fdi;
char fname[512];
char *libcname = copy_libc_so();
#ifndef _WIN32
/* On *NIX we create a .tar.bz2 / .tar.gz (may take a couple of seconds) */
printf("Please wait...\n");
snprintf(fname, sizeof(fname), "tar c %s/unrealircd %s %s %s 2>/dev/null|(bzip2 || gzip) 2>/dev/null",
BINDIR, coredump, MODULESDIR, libcname);
fdi = popen(fname, "r");
#else
/* On Windows we attach de .mdmp, the small minidump file */
strlcpy(fname, coredump, sizeof(fname));
if (strlen(fname) > 5)
fname[strlen(fname)-5] = '\0'; /* cut off the '.core' part */
strlcat(fname, ".mdmp", sizeof(fname)); /* and add '.mdmp' */
fprintf(fdo, "Windows MINIDUMP: %s\n", fname);
fdi = fopen(fname, "rb");
#endif
if (!fdi)
return 0;
attach_file(fdi, fdo);
#ifndef _WIN32
pclose(fdi);
#else
fclose(fdi);
#endif
return 1;
}
char *generate_crash_report(char *coredump, int *thirdpartymods)
{
static char reportfname[512];
FILE *reportfd;
*thirdpartymods = 0;
if (coredump == NULL)
coredump = find_best_coredump();
if (coredump == NULL)
return NULL; /* nothing available */
if (corefile_vs_binary_mismatch(coredump))
return NULL;
snprintf(reportfname, sizeof(reportfname), "%s/crash.report.%s.%ld.txt",
TMPDIR, unreal_getfilename(coredump), (long)time(NULL));
reportfd = fopen(reportfname, "w");
if (!reportfd)
{
printf("ERROR: could not open '%s' for writing\n", reportfname);
return NULL;
}
crash_report_header(reportfd, coredump);
crash_report_fix_libs(coredump, thirdpartymods);
crash_report_backtrace(reportfd, coredump);
crash_report_asan_log(reportfd, coredump);
attach_coredump(reportfd, coredump);
fclose(reportfd);
return reportfname;
}
#define REPORT_NEVER -1
#define REPORT_ASK 0
#define REPORT_AUTO 1
#define CRASH_REPORT_HOST "crash.unrealircd.org"
SSL_CTX *crashreport_init_tls(void)
{
SSL_CTX *ctx_client;
char buf[512];
SSL_load_error_strings();
SSLeay_add_ssl_algorithms();
ctx_client = SSL_CTX_new(SSLv23_client_method());
if (!ctx_client)
return NULL;
#ifdef HAS_SSL_CTX_SET_MIN_PROTO_VERSION
SSL_CTX_set_min_proto_version(ctx_client, TLS1_2_VERSION);
#endif
SSL_CTX_set_options(ctx_client, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1);
/* Verify peer certificate */
snprintf(buf, sizeof(buf), "%s/tls/curl-ca-bundle.crt", CONFDIR);
SSL_CTX_load_verify_locations(ctx_client, buf, NULL);
SSL_CTX_set_verify(ctx_client, SSL_VERIFY_PEER, NULL);
/* Limit ciphers as well */
SSL_CTX_set_cipher_list(ctx_client, UNREALIRCD_DEFAULT_CIPHERS);
return ctx_client;
}
int crashreport_send(char *fname)
{
char buf[1024];
char header[512], footer[512];
char delimiter[41];
int filesize;
int n;
FILE *fd;
SSL_CTX *ctx_client;
SSL *ssl = NULL;
BIO *socket = NULL;
int xfr = 0;
char *errstr = NULL;
filesize = get_file_size(fname);
if (filesize < 0)
return 0;
for (n = 0; n < sizeof(delimiter); n++)
delimiter[n] = getrandom8()%26 + 'a';
delimiter[sizeof(delimiter)-1] = '\0';
snprintf(header, sizeof(header), "--%s\r\n"
"Content-Disposition: form-data; name=\"upload\"; filename=\"crash.txt\"\r\n"
"Content-Type: text/plain\r\n"
"\r\n",
delimiter);
snprintf(footer, sizeof(footer), "\r\n--%s--\r\n", delimiter);
ctx_client = crashreport_init_tls();
if (!ctx_client)
{
printf("ERROR: TLS initalization failure (I)\n");
return 0;
}
socket = BIO_new_ssl_connect(ctx_client);
if (!socket)
{
printf("ERROR: TLS initalization failure (II)\n");
return 0;
}
BIO_get_ssl(socket, &ssl);
if (!ssl)
{
printf("ERROR: Could not get TLS connection from BIO\n");
return 0;
}
SSL_set_tlsext_host_name(ssl, CRASH_REPORT_HOST); /* SNI needs to be set explicitly */
BIO_set_conn_hostname(socket, CRASH_REPORT_HOST ":443");
if (BIO_do_connect(socket) != 1)
{
printf("ERROR: Could not connect to %s\n", CRASH_REPORT_HOST);
return 0;
}
if (BIO_do_handshake(socket) != 1)
{
printf("ERROR: Could not connect to %s (TLS handshake failed)\n", CRASH_REPORT_HOST);
return 0;
}
if (!verify_certificate(ssl, CRASH_REPORT_HOST, &errstr))
{
printf("Certificate problem with crash.unrealircd.org: %s\n", errstr);
printf("Fatal error. See above.\n");
return 0;
}
snprintf(buf, sizeof(buf), "POST /crash.php HTTP/1.1\r\n"
"User-Agent: UnrealIRCd %s\r\n"
"Host: %s\r\n"
"Accept: */*\r\n"
"Content-Length: %d\r\n"
"Expect: 100-continue\r\n"
"Content-Type: multipart/form-data; boundary=%s\r\n"
"\r\n",
VERSIONONLY,
CRASH_REPORT_HOST,
(int)(filesize+strlen(header)+strlen(footer)),
delimiter);
BIO_puts(socket, buf);
memset(buf, 0, sizeof(buf));
n = BIO_read(socket, buf, 255);
if ((n < 0) || strncmp(buf, "HTTP/1.1 100", 12))
{
printf("Error transmitting bug report (stage II, n=%d)\n", n);
if (!strncmp(buf, "HTTP/1.1 403", 12))
{
printf("Your crash report was rejected automatically.\n"
"This normally means your UnrealIRCd version is too old and unsupported.\n"
"Chances are that your crash issue is already fixed in a later release.\n"
"Check https://www.unrealircd.org/ for latest releases!\n");
}
return 0;
}
fd = fopen(fname, "rb");
if (!fd)
return 0;
BIO_puts(socket, header);
#ifndef _WIN32
printf("Sending...");
#endif
while ((fgets(buf, sizeof(buf), fd)))
{
BIO_puts(socket, buf);
#ifndef _WIN32
if ((++xfr % 1000) == 0)
{
printf(".");
fflush(stdout);
}
#endif
}
fclose(fd);
BIO_puts(socket, footer);
do { } while(BIO_should_retry(socket)); /* make sure we are really finished (you never know with TLS) */
#ifndef _WIN32
printf("\n");
#endif
BIO_free_all(socket);
SSL_CTX_free(ctx_client);
return 1;
}
void mark_coredump_as_read(char *coredump)
{
char buf[512];
snprintf(buf, sizeof(buf), "%s.%ld.done", coredump, (long)time(NULL));
(void)rename(coredump, buf);
}
static int report_pref = REPORT_ASK;
void report_crash_not_sent(char *fname)
{
printf("Crash report will not be sent to UnrealIRCd Team.\n"
"\n"
"Feel free to read the report at %s and delete it.\n"
"Or, if you change your mind, you can submit it anyway at https://bugs.unrealircd.org/\n"
" (if you do, please set the option 'View Status' at the end of the bug report page to 'private'!!)\n", fname);
}
/** This checks if there are indications that 3rd party modules are
* loaded. This is used to provide a small warning to the user that
* the crash may be likely due to that.
*/
int check_third_party_mods_present(void)
{
#ifndef _WIN32
struct dirent *dir;
DIR *fd = opendir(TMPDIR);
if (!fd)
return 0;
/* We search for files like tmp/FC5C3116.third.somename.so */
while ((dir = readdir(fd)))
{
char *fname = dir->d_name;
if (strstr(fname, ".third.") && strstr(fname, ".so"))
{
closedir(fd);
return 1;
}
}
closedir(fd);
#endif
return 0;
}
void report_crash(void)
{
char *coredump, *fname;
int thirdpartymods = 0;
int crashed_secs_ago;
if (!running_interactively() && (report_pref != REPORT_AUTO))
exit(0); /* don't bother if we run through cron or something similar */
coredump = find_best_coredump();
if (!coredump)
return; /* no crashes */
crashed_secs_ago = time(NULL) - get_file_time(coredump);
if (crashed_secs_ago > 86400*7)
return; /* stop bothering about it after a while */
fname = generate_crash_report(coredump, &thirdpartymods);
if (!fname)
return;
if (thirdpartymods == 0)
thirdpartymods = check_third_party_mods_present();
#ifndef _WIN32
printf("The IRCd has been started now (and is running), but it did crash %d seconds ago.\n", crashed_secs_ago);
printf("Crash report generated in: %s\n\n", fname);
if (thirdpartymods)
{
printf("** IMPORTANT **\n"
"Your UnrealIRCd crashed and you have 3rd party modules loaded (modules created\n"
"by someone other than the UnrealIRCd team). If you installed new 3rd party\n"
"module(s) in the past few weeks we suggest to unload these modules and see if\n"
"the crash issue dissapears. If so, that module is probably to blame.\n"
"If you keep crashing without any 3rd party modules loaded then please do report\n"
"it to the UnrealIRCd team.\n"
"The reason we ask you to do this is because MORE THAN 95%% OF ALL CRASH ISSUES\n"
"ARE CAUSED BY 3RD PARTY MODULES and not by an UnrealIRCd bug.\n"
"\n");
}
if (report_pref == REPORT_NEVER)
{
report_crash_not_sent(fname);
return;
} else
if (report_pref == REPORT_ASK)
{
char answerbuf[64], *answer;
printf("Shall I send a crash report to the UnrealIRCd developers?\n");
if (!thirdpartymods)
printf("Crash reports help us greatly with fixing bugs that affect you and others\n");
else
printf("NOTE: If the crash is caused by a 3rd party module then UnrealIRCd devs can't fix that.\n");
printf("\n");
do
{
printf("Answer (Y/N): ");
*answerbuf = '\0';
answer = fgets(answerbuf, sizeof(answerbuf), stdin);
if (answer && (toupper(*answer) == 'N'))
{
report_crash_not_sent(fname);
return;
}
if (answer && (toupper(*answer) == 'Y'))
{
break;
}
printf("Invalid response. Please enter either Y or N\n\n");
} while(1);
} else if (report_pref != REPORT_AUTO)
{
printf("Huh. report_pref setting is weird. Aborting.\n");
return;
}
if (running_interactively())
{
char buf[8192], *line;
printf("\nDo you want to give your e-mail address so we could ask for additional information or "
"give you feedback about the crash? This is completely optional, just press ENTER to skip.\n\n"
"E-mail address (optional): ");
line = fgets(buf, sizeof(buf), stdin);
if (line && *line && (*line != '\n'))
{
FILE *fd = fopen(fname, "a");
if (fd)
{
fprintf(fd, "\nUSER E-MAIL ADDRESS: %s\n", line);
fclose(fd);
}
}
printf("\nDo you have anything else to tell about the crash, maybe the circumstances? You can type 1 single line\n"
"Again, this is completely optional. Just press ENTER to skip.\n\n"
"Additional information (optional): ");
line = fgets(buf, sizeof(buf), stdin);
if (line && *line && (*line != '\n'))
{
FILE *fd = fopen(fname, "a");
if (fd)
{
fprintf(fd, "\nCOMMENT BY USER: %s\n", line);
fclose(fd);
}
}
}
if (crashreport_send(fname))
{
printf("\nThe crash report has been sent to the UnrealIRCd developers. "
"Thanks a lot for helping to make UnrealIRCd a better product!\n\n");
}
#else
/* Windows */
if (MessageBox(NULL, "UnrealIRCd crashed. May I send a report about this to the UnrealIRCd developers? This helps us a lot.",
"UnrealIRCd crash",
MB_YESNO|MB_ICONQUESTION) == IDYES)
{
/* Yay */
if (crashreport_send(fname))
{
MessageBox(NULL, "The crash report has been sent to the UnrealIRCd developers. "
"If you have any additional information (like details surrounding "
"the crash) then please e-mail syzop@unrealircd.org, such "
"information is most welcome. Thanks!",
"UnrealIRCd crash report sent", MB_ICONINFORMATION|MB_OK);
}
}
#endif
mark_coredump_as_read(coredump);
#ifdef _WIN32
if (MessageBox(NULL, "Start UnrealIRCd again?",
"UnrealIRCd crash",
MB_YESNO|MB_ICONQUESTION) == IDYES)
{
StartUnrealAgain();
}
#endif
}