/* Copyright (C) 2009 Zdenek Prikryl (zprikryl@redhat.com) Copyright (C) 2009 RedHat inc. This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "abrtlib.h" #include "../btparser/backtrace.h" #include "../btparser/frame.h" #include "../btparser/location.h" #include "parse_options.h" #define PROGNAME "abrt-action-generate-backtrace" #define DEBUGINFO_CACHE_DIR LOCALSTATEDIR"/cache/abrt-di" static const char *dump_dir_name = "."; static const char *debuginfo_dirs = DEBUGINFO_CACHE_DIR; /* 60 seconds was too limiting on slow machines */ static int exec_timeout_sec = 240; static void create_hash(char hash_str[SHA1_RESULT_LEN*2 + 1], const char *pInput) { unsigned len; unsigned char hash2[SHA1_RESULT_LEN]; sha1_ctx_t sha1ctx; sha1_begin(&sha1ctx); sha1_hash(pInput, strlen(pInput), &sha1ctx); sha1_end(hash2, &sha1ctx); len = SHA1_RESULT_LEN; char *d = hash_str; unsigned char *s = hash2; while (len) { *d++ = "0123456789abcdef"[*s >> 4]; *d++ = "0123456789abcdef"[*s & 0xf]; s++; len--; } *d = '\0'; //log("hash2:%s str:'%s'", hash_str, pInput); } /** * * @param[out] status See `man 2 wait` for status information. * @return Malloc'ed string */ static char* exec_vp(char **args, uid_t uid, int redirect_stderr, int *status) { /* Nuke everything which may make setlocale() switch to non-POSIX locale: * we need to avoid having gdb output in some obscure language. */ static const char *const unsetenv_vec[] = { "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", /* Workaround for * http://sourceware.org/bugzilla/show_bug.cgi?id=9622 * (gdb emitting ESC sequences even with -batch) */ "TERM", NULL }; int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_SETGUID | EXECFLG_SETSID | EXECFLG_QUIET; if (redirect_stderr) flags |= EXECFLG_ERR2OUT; VERB1 flags &= ~EXECFLG_QUIET; int pipeout[2]; pid_t child = fork_execv_on_steroids(flags, args, pipeout, (char**)unsetenv_vec, /*dir:*/ NULL, uid); /* We use this function to run gdb and unstrip. Bugs in gdb or corrupted * coredumps were observed to cause gdb to enter infinite loop. * Therefore we have a (largish) timeout, after which we kill the child. */ int t = time(NULL); /* int is enough, no need to use time_t */ int endtime = t + exec_timeout_sec; struct strbuf *buf_out = strbuf_new(); while (1) { int timeout = endtime - t; if (timeout < 0) { kill(child, SIGKILL); strbuf_append_strf(buf_out, "\nTimeout exceeded: %u seconds, killing %s\n", exec_timeout_sec, args[0]); break; } /* We don't check poll result - checking read result is enough */ struct pollfd pfd; pfd.fd = pipeout[0]; pfd.events = POLLIN; poll(&pfd, 1, timeout * 1000); char buff[1024]; int r = read(pipeout[0], buff, sizeof(buff) - 1); if (r <= 0) break; buff[r] = '\0'; strbuf_append_str(buf_out, buff); t = time(NULL); } close(pipeout[0]); /* Prevent having zombie child process, and maybe collect status * (note that status == NULL is ok too) */ waitpid(child, status, 0); return strbuf_free_nobuf(buf_out); } static char *get_backtrace(struct dump_dir *dd) { char *uid_str = dd_load_text(dd, FILENAME_UID); uid_t uid = xatoi_positive(uid_str); free(uid_str); char *executable = dd_load_text(dd, FILENAME_EXECUTABLE); dd_close(dd); char *args[21]; args[0] = (char*)"gdb"; args[1] = (char*)"-batch"; // when/if gdb supports "set debug-file-directory DIR1:DIR2": // (https://bugzilla.redhat.com/show_bug.cgi?id=528668): args[2] = (char*)"-ex"; struct strbuf *set_debug_file_directory = strbuf_new(); strbuf_append_str(set_debug_file_directory, "set debug-file-directory /usr/lib/debug"); const char *p = debuginfo_dirs; while (1) { while (*p == ':') p++; if (*p == '\0') break; const char *colon_or_nul = strchrnul(p, ':'); strbuf_append_strf(set_debug_file_directory, ":%.*s/usr/lib/debug", (int)(colon_or_nul - p), p); p = colon_or_nul; } args[3] = strbuf_free_nobuf(set_debug_file_directory); /* "file BINARY_FILE" is needed, without it gdb cannot properly * unwind the stack. Currently the unwind information is located * in .eh_frame which is stored only in binary, not in coredump * or debuginfo. * * Fedora GDB does not strictly need it, it will find the binary * by its build-id. But for binaries either without build-id * (= built on non-Fedora GCC) or which do not have * their debuginfo rpm installed gdb would not find BINARY_FILE * so it is still makes sense to supply "file BINARY_FILE". * * Unfortunately, "file BINARY_FILE" doesn't work well if BINARY_FILE * was deleted (as often happens during system updates): * gdb uses specified BINARY_FILE * even if it is completely unrelated to the coredump. * See https://bugzilla.redhat.com/show_bug.cgi?id=525721 * * TODO: check mtimes on COREFILE and BINARY_FILE and not supply * BINARY_FILE if it is newer (to at least avoid gdb complaining). */ args[4] = (char*)"-ex"; args[5] = xasprintf("file %s", executable); free(executable); args[6] = (char*)"-ex"; args[7] = xasprintf("core-file %s/"FILENAME_COREDUMP, dump_dir_name); args[8] = (char*)"-ex"; /*args[9] = ... see below */ args[10] = (char*)"-ex"; args[11] = (char*)"info sharedlib"; /* glibc's abort() stores its message in __abort_msg variable */ args[12] = (char*)"-ex"; args[13] = (char*)"print (char*)__abort_msg"; args[14] = (char*)"-ex"; args[15] = (char*)"print (char*)__glib_assert_msg"; args[16] = (char*)"-ex"; args[17] = (char*)"info registers"; args[18] = (char*)"-ex"; args[19] = (char*)"disassemble"; args[20] = NULL; /* Get the backtrace, but try to cap its size */ /* Limit bt depth. With no limit, gdb sometimes OOMs the machine */ unsigned bt_depth = 2048; const char *thread_apply_all = "thread apply all"; const char *full = " full"; char *bt = NULL; while (1) { args[9] = xasprintf("%s backtrace %u%s", thread_apply_all, bt_depth, full); bt = exec_vp(args, uid, /*redirect_stderr:*/ 1, NULL); free(args[9]); if ((bt && strnlen(bt, 256*1024) < 256*1024) || bt_depth <= 32) { break; } free(bt); bt_depth /= 2; if (bt_depth <= 64 && thread_apply_all[0] != '\0') { /* This program likely has gazillion threads, dont try to bt them all */ bt_depth = 256; thread_apply_all = ""; } if (bt_depth <= 64 && full[0] != '\0') { /* Looks like there are gigantic local structures or arrays, disable "full" bt */ bt_depth = 256; full = ""; } } free(args[3]); free(args[5]); free(args[7]); return bt; } int main(int argc, char **argv) { char *env_verbose = getenv("ABRT_VERBOSE"); if (env_verbose) g_verbose = atoi(env_verbose); char *i_opt = NULL; /* Can't keep these strings/structs static: _() doesn't support that */ const char *program_usage_string = _( PROGNAME" [options] -d DIR\n" "\n" "Generates and saves backtrace for coredump in dump directory DIR" ); enum { OPT_v = 1 << 0, OPT_d = 1 << 1, OPT_i = 1 << 2, OPT_t = 1 << 3, }; /* Keep enum above and order of options below in sync! */ struct options program_options[] = { OPT__VERBOSE(&g_verbose), OPT_STRING( 'd', NULL, &dump_dir_name , "DIR" , _("Dump directory")), OPT_STRING( 'i', NULL, &i_opt , "DIR1[:DIR2]...", _("Additional debuginfo directories")), OPT_INTEGER('t', NULL, &exec_timeout_sec, _("Kill gdb if it runs for more than N seconds")), OPT_END() }; /*unsigned opts =*/ parse_opts(argc, argv, program_options, program_usage_string); putenv(xasprintf("ABRT_VERBOSE=%u", g_verbose)); char *pfx = getenv("ABRT_PROG_PREFIX"); if (pfx && string_to_bool(pfx)) msg_prefix = PROGNAME; if (i_opt) { debuginfo_dirs = xasprintf("%s:%s", DEBUGINFO_CACHE_DIR, i_opt); } struct dump_dir *dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (!dd) return 1; char *package = dd_load_text(dd, FILENAME_PACKAGE); char *executable = dd_load_text(dd, FILENAME_EXECUTABLE); /* Create gdb backtrace */ /* NB: get_backtrace() closes dd */ char *backtrace_str = get_backtrace(dd); if (!backtrace_str) { backtrace_str = xstrdup(""); log("get_backtrace() returns NULL, broken core/gdb?"); } /* Compute backtrace hash */ struct btp_location location; btp_location_init(&location); char *backtrace_str_ptr = backtrace_str; struct btp_backtrace *backtrace = btp_backtrace_parse(&backtrace_str_ptr, &location); /* Store gdb backtrace */ dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (!dd) return 1; dd_save_text(dd, FILENAME_BACKTRACE, backtrace_str); /* Don't be completely silent. gdb run takes a few seconds, * it is useful to let user know it (maybe) worked. */ log(_("Backtrace is generated and saved, %u bytes"), (int)strlen(backtrace_str)); free(backtrace_str); /* Store backtrace hash */ if (!backtrace) { /* * The parser failed. Compute the UUID from the executable * and package only. This is not supposed to happen often. */ VERB1 log(_("Backtrace parsing failed for %s"), dump_dir_name); VERB1 log("%d:%d: %s", location.line, location.column, location.message); struct strbuf *emptybt = strbuf_new(); strbuf_prepend_str(emptybt, executable); strbuf_prepend_str(emptybt, package); char hash_str[SHA1_RESULT_LEN*2 + 1]; create_hash(hash_str, emptybt->buf); dd_save_text(dd, FILENAME_DUPHASH, hash_str); /* * Other parts of ABRT assume that if no rating is available, * it is ok to allow reporting of the bug. To be sure no bad * backtrace is reported, rate the backtrace with the lowest * rating. */ dd_save_text(dd, FILENAME_RATING, "0"); strbuf_free(emptybt); free(package); free(executable); dd_close(dd); /* Report success even if the parser failed, as the backtrace * has been created and rated. The failure is caused by a flaw * in the parser, not in the backtrace. */ return 0; } /* Compute duplication hash. */ char *str_hash_core = btp_backtrace_get_duplication_hash(backtrace); struct strbuf *str_hash = strbuf_new(); strbuf_append_str(str_hash, package); strbuf_append_str(str_hash, executable); strbuf_append_str(str_hash, str_hash_core); char hash_str[SHA1_RESULT_LEN*2 + 1]; create_hash(hash_str, str_hash->buf); dd_save_text(dd, FILENAME_DUPHASH, hash_str); strbuf_free(str_hash); free(str_hash_core); /* Compute the backtrace rating. */ float quality = btp_backtrace_quality_complex(backtrace); const char *rating; if (quality < 0.6f) rating = "0"; else if (quality < 0.7f) rating = "1"; else if (quality < 0.8f) rating = "2"; else if (quality < 0.9f) rating = "3"; else rating = "4"; dd_save_text(dd, FILENAME_RATING, rating); /* Get the function name from the crash frame. */ struct btp_frame *crash_frame = btp_backtrace_get_crash_frame(backtrace); if (crash_frame) { if (crash_frame->function_name && 0 != strcmp(crash_frame->function_name, "??")) { dd_save_text(dd, FILENAME_CRASH_FUNCTION, crash_frame->function_name); } btp_frame_free(crash_frame); } btp_backtrace_free(backtrace); dd_close(dd); free(executable); free(package); return 0; }