/* Copyright (C) 2009 Zdenek Prikryl (zprikryl@redhat.com) Copyright (C) 2009 Red Hat, 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 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; /** * * @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 env_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_SETSID | EXECFLG_QUIET; if (uid != (uid_t)-1L) flags |= EXECFLG_SETGUID; if (redirect_stderr) flags |= EXECFLG_ERR2OUT; VERB1 flags &= ~EXECFLG_QUIET; int pipeout[2]; pid_t child = fork_execv_on_steroids(flags, args, pipeout, (char**)env_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_ext(dd, FILENAME_UID, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); uid_t uid = -1L; if (uid_str) { uid = xatoi_positive(uid_str); free(uid_str); if (uid == geteuid()) { uid = -1L; /* no need to setuid/gid if we are already under right uid */ } } 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) { abrt_init(argv); char *i_opt = NULL; /* Can't keep these strings/structs static: _() doesn't support that */ const char *program_usage_string = _( "\b [options] -d DIR\n" "\n" "Analyzes coredump in dump directory DIR, generates and saves backtrace" ); 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 NUM seconds")), OPT_END() }; /*unsigned opts =*/ parse_opts(argc, argv, program_options, program_usage_string); export_abrt_envvars(0); 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; /* Create gdb backtrace */ /* NB: get_backtrace() closes dd */ char *backtrace = get_backtrace(dd); if (!backtrace) { backtrace = xstrdup(""); log("get_backtrace() returns NULL, broken core/gdb?"); } /* Store gdb backtrace */ dd = dd_opendir(dump_dir_name, /*flags:*/ 0); if (!dd) return 1; dd_save_text(dd, FILENAME_BACKTRACE, backtrace); dd_close(dd); /* 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)); free(backtrace); return 0; }