Current Path : /usr/share/apport/ |
Current File : //usr/share/apport/apport |
#!/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 - 2011 Canonical Ltd. # Author: Martin Pitt <martin.pitt@ubuntu.com> # # 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, pwd 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) (pidstat, real_uid, real_gid, cwd) = (None, None, None, None) def get_pid_info(pid): '''Read /proc information about pid''' global pidstat, real_uid, real_gid, cwd # unhandled exceptions on missing or invalidly formatted files are okay # here -- we want to know in the log file pidstat = os.stat('/proc/%s/stat' % pid) # determine real UID of the target process; do *not* use the owner of # /proc/pid/stat, as that will be root for setuid or unreadable programs! # (this matters when suid_dumpable is enabled) with open('/proc/%s/status' % pid) as f: for line in f: if line.startswith('Uid:'): real_uid = int(line.split()[1]) elif line.startswith('Gid:'): real_gid = int(line.split()[1]) break assert real_uid is not None, 'failed to parse Uid' assert real_gid is not None, 'failed to parse Gid' cwd = os.readlink('/proc/' + pid + '/cwd') def drop_privileges(pid, real_only=False): '''Change user and group to match the given target process Normally that irrevocably drops privileges to the real user/group of the target process. With real_only=True only the real IDs are changed, but the effective IDs remain. ''' if real_only: os.setregid(real_gid, -1) os.setreuid(real_uid, -1) else: os.setgid(real_gid) os.setuid(real_uid) assert os.getegid() == real_gid assert os.geteuid() == real_uid assert os.getgid() == real_gid assert os.getuid() == real_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 # don't write a core dump for suid/sgid/unreadable or otherwise # protected executables, in accordance with core(5) # (suid_dumpable==2 and core_pattern restrictions); when this happens, # /proc/pid/stat is owned by root (or the user suid'ed to), but we already # changed to the crashed process' real uid assert pidstat, 'pidstat not initialized' if pidstat.st_uid != os.getuid() or pidstat.st_gid != os.getgid(): error_log('disabling core dump for suid/sgid/unreadable executable') return 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 def is_closing_session(pid, uid): '''Check if pid is in a closing user session. During that, crashes are common as the session D-BUS and X.org are going away, etc. These crash reports are mostly noise, so should be ignored. ''' with open('/proc/%s/environ' % pid) as e: env = e.read().split('\0') for e in env: if e.startswith('DBUS_SESSION_BUS_ADDRESS='): dbus_addr = e.split('=', 1)[1] break else: error_log('is_closing_session(): no DBUS_SESSION_BUS_ADDRESS in environment') return False orig_uid = os.geteuid() os.setresuid(-1, os.getuid(), -1) try: gdbus = subprocess.Popen(['/usr/bin/gdbus', 'call', '-e', '-d', 'org.gnome.SessionManager', '-o', '/org/gnome/SessionManager', '-m', 'org.gnome.SessionManager.IsSessionRunning'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'DBUS_SESSION_BUS_ADDRESS': dbus_addr}) (out, err) = gdbus.communicate() if err: error_log('gdbus call error: ' + err.decode('UTF-8')) except OSError as e: error_log('gdbus call failed, cannot determine running session: ' + str(e)) return False finally: os.setresuid(-1, orig_uid, -1) error_log('debug: session gdbus call: ' + out.decode('UTF-8')) if out.startswith(b'(false,'): return True return False ################################################################# # # main # ################################################################# if len(sys.argv) != 4: try: print('Usage: %s <pid> <signal number> <core file ulimit>' % 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 get_pid_info(pid) # Partially drop privs to gain proper os.access() checks drop_privileges(pid, True) # 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)) # check if the executable was modified after the process started (e. g. # package got upgraded in between) exe_mtime = os.stat('/proc/%s/exe' % pid).st_mtime process_start = os.lstat('/proc/%s/cmdline' % pid).st_mtime if not os.path.exists(os.readlink('/proc/%s/exe' % pid)) or exe_mtime > process_start: error_log('executable was modified after program start, ignoring') 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 'ExecutablePath' not in info: error_log('could not determine ExecutablePath, aborting') sys.exit(1) if 'InterpreterPath' in info: 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) if is_closing_session(pid, pidstat.st_uid): error_log('happens for shutting down session, 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) # we prefer having a file mode of 0 while writing; this doesn't work # for suid binaries as we completely drop privs and thus can't chmod # them later on if pidstat.st_uid != os.getuid(): mode = 0o640 else: mode = 0 reportfile = os.fdopen(os.open(report, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode), 'wb') assert reportfile.fileno() > sys.stderr.fileno() # Make sure the crash reporting daemon can read this report try: gid = pwd.getpwnam('whoopsie').pw_gid os.chown(report, pidstat.st_uid, gid) except (OSError, KeyError): 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 and mode == 0: # for non-suid programs, make the report writable now, when it's # completely written os.chmod(report, 0o640) 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))