#!/usr/bin/python # Collect information about a crash and create a report in the directory # specified by apport.fileutils.report_dir. # See https://wiki.ubuntu.com/Apport for details. # # Copyright (c) 2006 - 2009 Canonical Ltd. # Author: Martin Pitt # # 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. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. import sys, os, os.path, subprocess, time, traceback, tempfile, glob import signal, inspect, grp, fcntl import apport, apport.fileutils ################################################################# # # functions # ################################################################# def check_lock(): '''Abort if another instance of apport is already running. This avoids bringing down the system to its knees if there is a series of crashes.''' # create a lock file lockfile = os.path.join(apport.fileutils.report_dir, '.lock') try: fd = os.open(lockfile, os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW) except OSError as e: error_log('cannot create lock file (uid %i): %s' % (os.getuid(), str(e))) sys.exit(1) try: fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: error_log('another apport instance is already running, aborting') sys.exit(1) def drop_privileges(pid, partial=False): '''Change user and group to match the given target process.''' stat = None try: stat = os.stat('/proc/' + pid) except OSError as e: raise ValueError('Invalid process ID: ' + str(e)) if partial: effective_gid = os.getegid() effective_uid = os.geteuid() else: effective_gid = stat.st_gid effective_uid = stat.st_uid os.setregid(stat.st_gid, effective_gid) os.setreuid(stat.st_uid, effective_uid) assert os.getegid() == effective_gid assert os.getgid() == stat.st_gid assert os.geteuid() == effective_uid assert os.getuid() == stat.st_uid def init_error_log(): '''Open a suitable error log if sys.stderr is not a tty.''' if not os.isatty(sys.stderr.fileno()): log = os.environ.get('APPORT_LOG_FILE', '/var/log/apport.log') try: f = os.open(log, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0o600) try: admgid = grp.getgrnam('adm')[2] os.chown(log, -1, admgid) os.chmod(log, 0o640) except KeyError: pass # if group adm doesn't exist, just leave it as root except OSError: # on a permission error, don't touch stderr return os.dup2(f, sys.stderr.fileno()) os.dup2(f, sys.stdout.fileno()) os.close(f) def error_log(msg): '''Output something to the error log.''' apport.error('apport (pid %s) %s: %s', os.getpid(), time.asctime(), msg) def _log_signal_handler(sgn, frame): '''Internal apport signal handler. Just log the signal handler and exit.''' # reset handler so that we do not get stuck in loops signal.signal(sgn, signal.SIG_IGN) try: error_log('Got signal %i, aborting; frame:' % sgn) for s in inspect.stack(): error_log(str(s)) except: pass sys.exit(1) def setup_signals(): '''Install a signal handler for all crash-like signals, so that apport is not called on itself when apport crashed.''' signal.signal(signal.SIGILL, _log_signal_handler) signal.signal(signal.SIGABRT, _log_signal_handler) signal.signal(signal.SIGFPE, _log_signal_handler) signal.signal(signal.SIGSEGV, _log_signal_handler) signal.signal(signal.SIGPIPE, _log_signal_handler) signal.signal(signal.SIGBUS, _log_signal_handler) def write_user_coredump(pid, cwd, limit): '''Write the core into the current directory if ulimit requests it.''' # three cases: # limit == 0: do not write anything # limit == None: unlimited or large enough, write out everything # limit nonzero: crashed process' core size ulimit in bytes if limit == '0': return if limit: limit = int(limit) else: assert limit is None # limit -1 means 'unlimited' if limit < 0: limit = None else: # ulimit specifies kB limit *= 1024 core_path = os.path.join(cwd, 'core') try: if open('/proc/sys/kernel/core_uses_pid').read().strip() != '0': core_path += '.' + str(pid) core_file = os.open(core_path, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0o600) except (OSError, IOError): return error_log('writing core dump to %s (limit: %s)' % (core_path, str(limit))) written = 0 # Priming read block = sys.stdin.read(1048576) while True: size = len(block) if size == 0: break written += size if limit and written > limit: error_log('aborting core dump writing, size exceeds current limit %i' % limit) os.close(core_file) os.unlink(core_path) return if os.write(core_file, block) != size: error_log('aborting core dump writing, could not write') os.close(core_file) os.unlink(core_path) return block = sys.stdin.read(1048576) os.close(core_file) return core_path def usable_ram(): '''Return how many bytes of RAM is currently available that can be allocated without causing major thrashing.''' # abuse our excellent RFC822 parser to parse /proc/meminfo r = apport.Report() r.load(open('/proc/meminfo')) memfree = long(r['MemFree'].split()[0]) cached = long(r['Cached'].split()[0]) writeback = long(r['Writeback'].split()[0]) return (memfree+cached-writeback) * 1024 ################################################################# # # main # ################################################################# if len(sys.argv) != 4: try: print('Usage: %s ' % sys.argv[0]) print('The core dump is read from stdin.') except IOError: # sys.stderr might not actually exist, expecially not when being called # from the kernel pass sys.exit(1) init_error_log() check_lock() try: setup_signals() (pid, signum, core_ulimit) = sys.argv[1:] # drop our process priority level to not disturb userspace so much try: os.nice(10) except OSError: pass # *shrug*, we tried # Partially drop privs to gain proper os.access() checks drop_privileges(pid, True) # try to find the core dump file; if path is relative, prepend cwd of # crashed process cwd = os.readlink('/proc/' + pid + '/cwd') # ignore SIGQUIT (it's usually deliberately generated by users) if signum == str(signal.SIGQUIT): sys.exit(0) error_log('called for pid %s, signal %s' % (pid, signum)) try: pidstat = os.stat('/proc/' + pid) except OSError: error_log('Invalid PID') sys.exit(1) info = apport.Report('Crash') info['Signal'] = signum info['CoreDump'] = (sys.stdin, True, usable_ram()*3/4, True) # We already need this here to figure out the ExecutableName (for scripts, # etc). info.add_proc_info(pid) if not info.has_key('ExecutablePath'): error_log('could not determine ExecutablePath, aborting') sys.exit(1) if info.has_key('InterpreterPath'): error_log('script: %s, interpreted by %s (command line "%s")' % (info['ExecutablePath'], info['InterpreterPath'], info['ProcCmdline'])) else: error_log('executable: %s (command line "%s")' % (info['ExecutablePath'], info['ProcCmdline'])) # ignore non-package binaries (unless configured otherwise) if not apport.fileutils.likely_packaged(info['ExecutablePath']): if not apport.fileutils.get_config('main', 'unpackaged', False, bool=True): error_log('executable does not belong to a package, ignoring') # check if the user wants a core dump drop_privileges(pid) write_user_coredump(pid, cwd, core_ulimit) sys.exit(1) # ignore SIGXCPU and SIGXFSZ since this indicates some external # influence changing soft RLIMIT values when running programs. if signum in [str(signal.SIGXCPU),str(signal.SIGXFSZ)]: error_log('Ignoring signal %s (caused by exceeding soft RLIMIT)' % signum) drop_privileges(pid) write_user_coredump(pid, cwd, core_ulimit) sys.exit(0) # ignore blacklisted binaries if info.check_ignored(): error_log('executable version is blacklisted, ignoring') sys.exit(1) crash_counter = 0 # Create crash report file descriptor for writing the report into # report_dir try: report = '%s/%s.%i.crash' % (apport.fileutils.report_dir, info['ExecutablePath'].replace('/', '_'), pidstat.st_uid) if os.path.exists(report): if apport.fileutils.seen_report(report): # do not flood the logs and the user with repeated crashes crash_counter = apport.fileutils.get_recent_crashes(open(report)) crash_counter += 1 if crash_counter > 1: drop_privileges(pid) write_user_coredump(pid, cwd, core_ulimit) error_log('this executable already crashed %i times, ignoring' % crash_counter) sys.exit(1) # remove the old file, so that we can create the new one with # os.O_CREAT|os.O_EXCL os.unlink(report) else: error_log('apport: report %s already exists and unseen, doing nothing to avoid disk usage DoS' % report) drop_privileges(pid) write_user_coredump(pid, cwd, core_ulimit) sys.exit(1) reportfile = os.fdopen(os.open(report, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0), 'w') assert reportfile.fileno() > sys.stderr.fileno() os.chown(report, pidstat.st_uid, pidstat.st_gid) except (OSError, IOError) as e: error_log('Could not create report file: %s' % str(e)) sys.exit(1) # Totally drop privs before writing out the reportfile. drop_privileges(pid) # check if the user wants a core dump coredump = write_user_coredump(pid, cwd, core_ulimit) # Now that we've written it, it's no longer on stdin, so we need # to change the CoreDump info if we want it in the crash report. if coredump: info['CoreDump'] = (open(coredump), True, usable_ram()*3/4) info.add_user_info() info.add_os_info() if crash_counter > 0: info['CrashCounter'] = '%i' % crash_counter try: info.write(reportfile) except IOError: if reportfile != sys.stderr: os.unlink(report) raise if report: os.chmod(report, 0o600) if reportfile != sys.stderr: error_log('wrote report %s' % report) except (SystemExit, KeyboardInterrupt): raise except Exception as e: error_log('Unhandled exception:') traceback.print_exc() error_log('pid: %i, uid: %i, gid: %i, euid: %i, egid: %i' % ( os.getpid(), os.getuid(), os.getgid(), os.geteuid(), os.getegid())) error_log('environment: %s' % str(os.environ))