diff -Nru apport-2.20.11/apport/fileutils.py apport-2.20.11/apport/fileutils.py --- apport-2.20.11/apport/fileutils.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/apport/fileutils.py 2022-09-15 12:43:39.000000000 +0000 @@ -45,6 +45,33 @@ return False +def get_dbus_socket(dbus_addr): + '''Extract the socket from a DBus address.''' + + if not dbus_addr: + return None + + # Only support unix domain sockets, and only the default Ubuntu path + if not dbus_addr.startswith("unix:path=/run/user/"): + return None + + # Prevent path traversal + if "../" in dbus_addr: + return None + + # Don't support escaped values, multiple addresses, or multiple keys + # and values + for search in ["%", ",", ";"]: + if search in dbus_addr: + return None + + parts = dbus_addr.split("=") + if len(parts) != 2: + return None + + return parts[1] + + def find_package_desktopfile(package): '''Return a package's .desktop file. @@ -360,7 +387,7 @@ fd = None f = None if not get_config.config: - get_config.config = ConfigParser() + get_config.config = ConfigParser(interpolation=None) try: fd = os.open(path, os.O_NOFOLLOW | os.O_RDONLY) @@ -422,6 +449,22 @@ return (real_uid, real_gid) +def search_map(mapfd, uid): + '''Search for an ID in a map fd''' + for line in mapfd: + fields = line.split() + if len(fields) != 3: + continue + + host_start = int(fields[1]) + host_end = host_start + int(fields[2]) + + if uid >= host_start and uid <= host_end: + return True + + return False + + def get_boot_id(): '''Gets the kernel boot id''' @@ -430,7 +473,18 @@ return boot_id -def get_core_path(pid=None, exe=None, uid=None, timestamp=None): +def get_process_path(proc_pid_fd=None): + '''Gets the process path from a proc directory file descriptor''' + + if proc_pid_fd is None: + return 'unknown' + try: + return os.readlink('exe', dir_fd=proc_pid_fd) + except OSError: + return 'unknown' + + +def get_core_path(pid=None, exe=None, uid=None, timestamp=None, proc_pid_fd=None): '''Get the path to a core file''' if pid is None: @@ -443,9 +497,8 @@ timestamp = get_starttime(stat_contents) if exe is None: - exe = 'unknown' - else: - exe = exe.replace('/', '_').replace('.', '_') + exe = get_process_path(proc_pid_fd) + exe = exe.replace('/', '_').replace('.', '_') if uid is None: uid = os.getuid() @@ -464,6 +517,7 @@ '''Searches the core file directory for files that belong to a specified uid. Returns a list of lists containing the filename and the file modification time.''' + uid = str(uid) core_files = [] uid_files = [] @@ -472,7 +526,7 @@ for f in core_files: try: - if f.split('.')[2] == str(uid): + if f.split('.')[2] == uid: time = os.path.getmtime(os.path.join(core_dir, f)) uid_files.append([f, time]) except (IndexError, FileNotFoundError): @@ -487,7 +541,7 @@ uid_files = find_core_files_by_uid(uid) sorted_files = sorted(uid_files, key=itemgetter(1)) - # Substract a extra one to make room for the new core file + # Subtract a extra one to make room for the new core file if len(uid_files) > max_corefiles_per_uid - 1: for x in range(len(uid_files) - max_corefiles_per_uid + 1): os.remove(os.path.join(core_dir, sorted_files[0][0])) diff -Nru apport-2.20.11/apport/hookutils.py apport-2.20.11/apport/hookutils.py --- apport-2.20.11/apport/hookutils.py 2021-08-26 14:29:45.000000000 +0000 +++ apport-2.20.11/apport/hookutils.py 2022-09-15 12:43:39.000000000 +0000 @@ -723,6 +723,29 @@ attach_gsettings_schema(report, schema) +def attach_journal_errors(report, time_window=10) -> None: + '''Attach journal warnings and errors. + + If the report contains a date, get the journal logs around that + date (plus/minus the time_window in seconds). Otherwise attach the + latest 1000 journal logs since the last boot. + ''' + + if not os.path.exists('/run/systemd/system'): + return + + crash_timestamp = report.get_timestamp() + if crash_timestamp: + before_crash = crash_timestamp - time_window + after_crash = crash_timestamp + time_window + args = [f'--since=@{before_crash}', f'--until=@{after_crash}'] + else: + args = ['-b', '--lines=1000'] + report['JournalErrors'] = command_output( + ['journalctl', '--priority=warning'] + args + ) + + def attach_network(report): '''Attach generic network-related information to report.''' diff -Nru apport-2.20.11/apport/report.py apport-2.20.11/apport/report.py --- apport-2.20.11/apport/report.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/apport/report.py 2022-09-15 12:43:39.000000000 +0000 @@ -9,8 +9,8 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import subprocess, tempfile, os.path, re, pwd, grp, os, time, io -import fnmatch, glob, traceback, errno, sys, atexit, locale, imp, stat +import subprocess, tempfile, os.path, re, pwd, grp, os, io +import fnmatch, glob, traceback, errno, sys, atexit, imp, stat import xml.dom, xml.dom.minidom from xml.parsers.expat import ExpatError @@ -1794,21 +1794,3 @@ return None return (my_session, session_start_time) - - def get_timestamp(self): - '''Get timestamp (seconds since epoch) from Date field - - Return None if it is not present. - ''' - # report time is from asctime(), not in locale representation - orig_ctime = locale.getlocale(locale.LC_TIME) - try: - try: - locale.setlocale(locale.LC_TIME, 'C') - return time.mktime(time.strptime(self['Date'])) - except KeyError: - return None - finally: - locale.setlocale(locale.LC_TIME, orig_ctime) - except locale.Error: - return None diff -Nru apport-2.20.11/data/apport apport-2.20.11/data/apport --- apport-2.20.11/data/apport 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/data/apport 2022-09-15 12:43:39.000000000 +0000 @@ -179,7 +179,9 @@ signal.signal(signal.SIGBUS, _log_signal_handler) -def write_user_coredump(pid, timestamp, limit, from_report=None): +def write_user_coredump( + pid, timestamp, limit, coredump_fd=None, from_report=None +): '''Write the core into a directory if ulimit requests it.''' # three cases: @@ -201,9 +203,10 @@ return (core_name, core_path) = apport.fileutils.get_core_path(pid, - options.exe_path, + options.executable_path, crash_uid, - timestamp) + timestamp, + proc_pid_fd) try: # Limit number of core files to prevent DoS @@ -229,8 +232,7 @@ error_log('writing core dump %s of size %i' % (core_name, core_size)) os.write(core_file, r['CoreDump']) else: - # read from stdin - block = os.read(0, 1048576) + block = os.read(coredump_fd, 1048576) while True: size = len(block) @@ -247,7 +249,7 @@ os.close(core_file) os.unlink(core_path, dir_fd=cwd) return - block = os.read(0, 1048576) + block = os.read(coredump_fd, 1048576) # Make sure the user can read it os.fchown(core_file, crash_uid, -1) @@ -270,12 +272,53 @@ return (memfree + cached - writeback) * 1024 +def _run_with_output_limit_and_timeout(args, output_limit, timeout, close_fds=True, env=None): + '''Run command like subprocess.run() but with output limit and timeout. + + Return (stdout, stderr).''' + + stdout = b"" + stderr = b"" + + process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + close_fds=close_fds, env=env) + try: + # Don't block so we don't deadlock + os.set_blocking(process.stdout.fileno(), False) + os.set_blocking(process.stderr.fileno(), False) + + for _ in range(timeout): + alive = process.poll() is None + + while len(stdout) < output_limit and len(stderr) < output_limit: + tempout = process.stdout.read(100) + if tempout: + stdout += tempout + temperr = process.stderr.read(100) + if temperr: + stderr += temperr + if not tempout and not temperr: + break + + if not alive or len(stdout) >= output_limit or len(stderr) >= output_limit: + break + time.sleep(1) + finally: + process.kill() + + return stdout, stderr + + def is_closing_session(): '''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. ''' + # Sanity check, don't do anything for root processes + if crash_uid == 0 or crash_gid == 0: + return False + with open('environ', 'rb', opener=proc_pid_opener) as e: env = e.read().split(b'\0') for e in env: @@ -286,6 +329,15 @@ error_log('is_closing_session(): no DBUS_SESSION_BUS_ADDRESS in environment') return False + dbus_socket = apport.fileutils.get_dbus_socket(dbus_addr) + if not dbus_socket: + error_log('is_closing_session(): Could not determine DBUS socket.') + return False + + if not os.path.exists(dbus_socket): + error_log("is_closing_session(): DBUS socket doesn't exist.") + return False + # We need to drop both the real and effective uid/gid before calling # gdbus because DBUS_SESSION_BUS_ADDRESS is untrusted and may allow # reading arbitrary files as a noncefile. We can't just drop effective @@ -298,11 +350,11 @@ try: os.setresgid(crash_gid, crash_gid, real_gid) os.setresuid(crash_uid, crash_uid, real_uid) - 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() + out, err = _run_with_output_limit_and_timeout(['/usr/bin/gdbus', 'call', '-e', '-d', + 'org.gnome.SessionManager', '-o', '/org/gnome/SessionManager', '-m', + 'org.gnome.SessionManager.IsSessionRunning', '-t', '5'], + 1000, 5, env={'DBUS_SESSION_BUS_ADDRESS': dbus_addr}) + if err: error_log('gdbus call error: ' + err.decode('UTF-8')) except OSError as e: @@ -385,38 +437,46 @@ parser.add_argument("-s", "--signal-number", help="signal number (%%s)") parser.add_argument("-c", "--core-ulimit", help="core ulimit (%%c)") parser.add_argument("-d", "--dump-mode", help="dump mode (%%d)") - parser.add_argument("-P", "--global-pid", nargs='?', help="pid in root namespace (%%P)") - parser.add_argument("-E", "--executable-path", nargs='?', help="path of executable (%%E)") + parser.add_argument("-P", "--global-pid", help="pid in root namespace (%%P)") + parser.add_argument("-u", "--uid", type=int, help="real UID (%%u)") + parser.add_argument("-g", "--gid", type=int, help="real GID (%%g)") + parser.add_argument("executable_path", nargs='*', help="path of executable (%%E)") options, rest = parser.parse_known_args() - if options.pid is not None: - for arg in rest: - error_log("Unknown argument: %s", arg) - - elif len(rest) in (4, 5, 6, 7): + # Legacy command line needs to remain for the scenario where a more + # recent apport is running inside a container with an older apport + # running on the host. + if options.pid is None and len(sys.argv) == 5: # Translate legacy command line - options.pid = rest[0] - options.signal_number = rest[1] - options.core_ulimit = rest[2] - options.dump_mode = rest[3] - try: - options.global_pid = rest[4] - except IndexError: - options.global_pid = None - try: - options.exe_path = rest[5].replace('!', '/') - except IndexError: - options.exe_path = None - try: - if rest[6] == '(deleted)': - options.exe_path += ' %s' % rest[6] - except IndexError: - pass - else: + return argparse.Namespace( + pid=sys.argv[1], + signal_number=sys.argv[2], + core_ulimit=sys.argv[3], + dump_mode=sys.argv[4], + global_pid=None, + uid=None, + gid=None, + executable_path=None, + ) + + if options.pid is None: parser.print_usage() sys.exit(1) + for arg in rest: + error_log("Unknown argument: %s" % arg) + + # In kernels before 5.3.0, an executable path with spaces may be split + # into separate arguments. If options.executable_path is a list, join + # it back into a string. Also restore directory separators. + if isinstance(options.executable_path, list): + options.executable_path = " ".join(options.executable_path) + options.executable_path = options.executable_path.replace('!', '/') + # Sanity check to prevent trickery later on + if '../' in options.executable_path: + options.executable_path = None + return options @@ -426,6 +486,8 @@ # ################################################################# +init_error_log() + # systemd socket activation if 'LISTEN_FDS' in os.environ: try: @@ -471,8 +533,6 @@ options = parse_arguments() -init_error_log() - # Check if we received a valid global PID (kernel >= 3.12). If we do, # then compare it with the local PID. If they don't match, it's an # indication that the crash originated from another PID namespace. @@ -486,32 +546,34 @@ # Instead, attempt to find apport inside the container and # forward the process information there. - if not os.path.exists('/proc/%d/root/run/apport.socket' % host_pid): - error_log('host pid %s crashed in a container without apport support' % - options.global_pid) - sys.exit(0) proc_host_pid_fd = os.open('/proc/%d' % host_pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) def proc_host_pid_opener(path, flags): return os.open(path, flags, dir_fd=proc_host_pid_fd) + # Validate that the target socket is owned by the user namespace of the process + try: + sock_fd = os.open("root/run/apport.socket", os.O_RDONLY | os.O_PATH, dir_fd=proc_host_pid_fd) + socket_uid = os.fstat(sock_fd).st_uid + except FileNotFoundError: + error_log('host pid %s crashed in a container without apport support' % + options.global_pid) + sys.exit(0) + + try: + with open("uid_map", "r", opener=proc_host_pid_opener) as fd: + if not apport.fileutils.search_map(fd, socket_uid): + error_log("user is trying to trick apport into accessing a socket that doesn't belong to the container") + sys.exit(0) + except FileNotFoundError: + pass + # Validate that the crashed binary is owned by the user namespace of the process task_uid = os.stat("exe", dir_fd=proc_host_pid_fd).st_uid try: with open("uid_map", "r", opener=proc_host_pid_opener) as fd: - for line in fd: - fields = line.split() - if len(fields) != 3: - continue - - host_start = int(fields[1]) - host_end = host_start + int(fields[2]) - - if task_uid >= host_start and task_uid <= host_end: - break - - else: + if not apport.fileutils.search_map(fd, task_uid): error_log("host pid %s crashed in a container with no access to the binary" % options.global_pid) sys.exit(0) @@ -521,45 +583,28 @@ task_gid = os.stat("exe", dir_fd=proc_host_pid_fd).st_gid try: with open("gid_map", "r", opener=proc_host_pid_opener) as fd: - for line in fd: - fields = line.split() - if len(fields) != 3: - continue - - host_start = int(fields[1]) - host_end = host_start + int(fields[2]) - - if task_gid >= host_start and task_gid <= host_end: - break - - else: + if not apport.fileutils.search_map(fd, task_gid): error_log("host pid %s crashed in a container with no access to the binary" % options.global_pid) sys.exit(0) except FileNotFoundError: pass - # Chdir and chroot to the task - # WARNING: After this point, all "import" calls are security issues - __builtins__.__dict__['__import__'] = None - - host_cwd = os.open('cwd', os.O_RDONLY | os.O_PATH | os.O_DIRECTORY, dir_fd=proc_host_pid_fd) - - os.chdir(host_cwd) - # WARNING: we really should be using a file descriptor here, - # but os.chroot won't take it - os.chroot(os.readlink('root', dir_fd=proc_host_pid_fd)) - + # Now open the socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: - sock.connect('/run/apport.socket') + sock.connect('/proc/self/fd/%s' % sock_fd) except Exception: error_log('host pid %s crashed in a container with a broken apport' % options.global_pid) sys.exit(0) - # Send all arguments except for the first (exec path) and last (global pid) - args = ' '.join(sys.argv[1:5]) + # Send main arguments only + # Older apport in containers doesn't support positional arguments + args = "%s %s %s %s" % (options.pid, + options.signal_number, + options.core_ulimit, + options.dump_mode) try: sock.sendmsg([args.encode()], [ # Send a ucred containing the global pid @@ -568,7 +613,7 @@ # Send fd 0 (the coredump) (socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', [0]))]) sock.shutdown(socket.SHUT_RDWR) - except socket.timeout: + except Exception: error_log('Container apport failed to process crash within 30s') sys.exit(0) @@ -596,19 +641,34 @@ signum = options.signal_number core_ulimit = options.core_ulimit dump_mode = options.dump_mode + coredump_fd = sys.stdin.fileno() get_pid_info(pid) - # Check if the process was replaced after the crash happened. - # Ideally we'd use the time of dump value provided by the kernel, - # but this would be a backwards-incompatible change that would - # require adding support to apport outside and inside of containers. + # Sanity check to make sure the process wasn't replaced after the crash + # happened. The start time isn't fine-grained enough to be an adequate + # security check. apport_start = get_apport_starttime() process_start = get_process_starttime() if process_start > apport_start: error_log('process was replaced after Apport started, ignoring') sys.exit(0) + # Make sure the process uid/gid match the ones provided by the kernel + # if available, if not, it may have been replaced + if (options.uid is not None) and (options.gid is not None): + if (options.uid != crash_uid) or (options.gid != crash_gid): + error_log("process uid/gid doesn't match expected, ignoring") + sys.exit(0) + + # check if the executable was modified after the process started (e. g. + # package got upgraded in between). + exe_mtime = os.stat('exe', dir_fd=proc_pid_fd).st_mtime + process_mtime = os.lstat('cmdline', dir_fd=proc_pid_fd).st_mtime + if not os.path.exists(os.readlink('exe', dir_fd=proc_pid_fd)) or exe_mtime > process_mtime: + error_log('executable was modified after program start, ignoring') + sys.exit(0) + error_log('called for pid %s, signal %s, core limit %s, dump mode %s' % (pid, signum, core_ulimit, dump_mode)) try: @@ -629,15 +689,7 @@ # ignore SIGQUIT (it's usually deliberately generated by users) if signum == str(int(signal.SIGQUIT)): - write_user_coredump(pid, process_start, core_ulimit) - sys.exit(0) - - # check if the executable was modified after the process started (e. g. - # package got upgraded in between) - exe_mtime = os.stat('exe', dir_fd=proc_pid_fd).st_mtime - process_mtime = os.lstat('cmdline', dir_fd=proc_pid_fd).st_mtime - if not os.path.exists(os.readlink('exe', dir_fd=proc_pid_fd)) or exe_mtime > process_mtime: - error_log('executable was modified after program start, ignoring') + write_user_coredump(pid, process_start, core_ulimit, coredump_fd) sys.exit(0) info = apport.Report('Crash') @@ -651,9 +703,10 @@ # We already need this here to figure out the ExecutableName (for scripts, # etc). - - if options.exe_path is not None and os.path.exists(options.exe_path): - info['ExecutablePath'] = options.exe_path + if options.executable_path is not None and os.path.exists(options.executable_path): + info['ExecutablePath'] = options.executable_path + else: + info['ExecutablePath'] = os.readlink('exe', dir_fd=proc_pid_fd) # Drop privileges temporarily to make sure that we don't # include information in the crash report that the user should @@ -689,7 +742,7 @@ error_log('executable does not belong to a package, ignoring') # check if the user wants a core dump recover_privileges() - write_user_coredump(pid, process_start, core_ulimit) + write_user_coredump(pid, process_start, core_ulimit, coredump_fd) sys.exit(0) # ignore SIGXCPU and SIGXFSZ since this indicates some external @@ -697,7 +750,7 @@ if signum in [str(signal.SIGXCPU), str(signal.SIGXFSZ)]: error_log('Ignoring signal %s (caused by exceeding soft RLIMIT)' % signum) recover_privileges() - write_user_coredump(pid, process_start, core_ulimit) + write_user_coredump(pid, process_start, core_ulimit, coredump_fd) sys.exit(0) # ignore blacklisted binaries @@ -736,15 +789,19 @@ crash_counter = apport.fileutils.get_recent_crashes(f) crash_counter += 1 if crash_counter > 1: - write_user_coredump(pid, process_start, core_ulimit) + write_user_coredump( + pid, process_start, core_ulimit, coredump_fd + ) error_log('this executable already crashed %i times, ignoring' % crash_counter) sys.exit(0) # 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) - write_user_coredump(pid, process_start, core_ulimit) + error_log('apport: report %s already exists and unseen, skipping to avoid disk usage DoS' % report) + write_user_coredump( + pid, process_start, core_ulimit, coredump_fd + ) sys.exit(0) # we prefer having a file mode of 0 while writing; fd = os.open(report, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0) diff -Nru apport-2.20.11/data/general-hooks/generic.py apport-2.20.11/data/general-hooks/generic.py --- apport-2.20.11/data/general-hooks/generic.py 2019-12-04 20:25:28.000000000 +0000 +++ apport-2.20.11/data/general-hooks/generic.py 2022-09-15 12:43:39.000000000 +0000 @@ -88,9 +88,7 @@ # log errors if report['ProblemType'] == 'Crash': - if os.path.exists('/run/systemd/system'): - report['JournalErrors'] = apport.hookutils.command_output( - ['journalctl', '-b', '--priority=warning', '--lines=1000']) + apport.hookutils.attach_journal_errors(report) if __name__ == '__main__': diff -Nru apport-2.20.11/data/whoopsie-upload-all apport-2.20.11/data/whoopsie-upload-all --- apport-2.20.11/data/whoopsie-upload-all 2021-05-13 13:31:33.000000000 +0000 +++ apport-2.20.11/data/whoopsie-upload-all 2022-09-15 12:43:39.000000000 +0000 @@ -93,7 +93,11 @@ # write updated report, we use os.open and os.fdopen as # /proc/sys/fs/protected_regular is set to 1 (LP: #1848064) # make sure the file isn't a FIFO or symlink - fd = os.open(report, os.O_NOFOLLOW | os.O_WRONLY | os.O_APPEND | os.O_NONBLOCK) + try: + fd = os.open(report, os.O_NOFOLLOW | os.O_WRONLY | os.O_APPEND | os.O_NONBLOCK) + except FileNotFoundError: + # The crash report was deleted. Nothing left to do. + return None st = os.fstat(fd) if stat.S_ISREG(st.st_mode): with os.fdopen(fd, 'wb') as f: diff -Nru apport-2.20.11/debian/apport.init apport-2.20.11/debian/apport.init --- apport-2.20.11/debian/apport.init 2019-12-06 22:22:30.000000000 +0000 +++ apport-2.20.11/debian/apport.init 2022-06-29 13:52:31.000000000 +0000 @@ -50,13 +50,9 @@ rm -f /var/lib/pm-utils/resume-hang.log fi - # Old compatibility mode, switch later to second one - if true; then - echo "|$AGENT %p %s %c %d %P %E" > /proc/sys/kernel/core_pattern - else - echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -E%E" > /proc/sys/kernel/core_pattern - fi + echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" > /proc/sys/kernel/core_pattern echo 2 > /proc/sys/fs/suid_dumpable + echo 10 > /proc/sys/kernel/core_pipe_limit } # @@ -70,6 +66,7 @@ # 2 if daemon could not be stopped # other if a failure occurred + echo 0 > /proc/sys/kernel/core_pipe_limit echo 0 > /proc/sys/fs/suid_dumpable # Check for a hung resume. If we find one try and grab everything diff -Nru apport-2.20.11/debian/changelog apport-2.20.11/debian/changelog --- apport-2.20.11/debian/changelog 2022-03-30 18:03:09.000000000 +0000 +++ apport-2.20.11/debian/changelog 2022-09-15 12:43:39.000000000 +0000 @@ -1,3 +1,70 @@ +apport (2.20.11-0ubuntu27.25) focal; urgency=medium + + * Point Vcs-* URIs to git + * whoopsie-upload-all: Catch FileNotFoundError during process_report + (LP: #1867204) + * Grab a slice of JournalErrors around the crash time (LP: #1962454) + * data/apport: + - Initialize error log as first step (LP: #1989467) + - Fix PermissionError for setuid programs inside container (LP: #1982487) + - Fix reading from stdin inside containers (LP: #1982555) + * Fix autopkgtest test case failures (LP: #1989467): + - Mark autopkgtest with isolation-container restriction + - Fix failure if kernel module isofs is not installed + - Do not check recommended dependencies + - Skip UI test if kernel thread is not found + - Fix race in test_crash_system_slice + - Fix check for not running test executable + - Use shadow in *_different_binary_source + - Mock kernel package version in UI test + - Fix test_kerneloops_nodetails if kernel is not installed + - Drop broken test_crash_setuid_drop_and_kill + - Expect linux-signed on arm64/s390x as well + - Skip SegvAnalysis for non x86 architectures + - Use unlimited core ulimit for SIGQUIT test + - Fix race with progress window in GTK UI tests + - Use sleep instead of yes for tests + - Fix test_add_gdb_info_script on armhf + - Fix wrong Ubuntu archive URI on ports + - Fix KeyError in test_install_packages_unversioned + - Depend on python3-systemd for container tests + - Depend on psmisc for killall binary + - Replace missing oxideqt-codecs + - Drop broken test_install_packages_from_launchpad + - Fix test_install_packages_permanent_sandbox* for s390x + + -- Benjamin Drung Thu, 15 Sep 2022 14:43:39 +0200 + +apport (2.20.11-0ubuntu27.24) focal-security; urgency=medium + + * SECURITY UPDATE: Fix multiple security issues + - test/test_report.py: Fix flaky test. + - data/apport: Fix too many arguments for error_log(). + - data/apport: Use proper argument variable name executable_path. + - etc/init.d/apport: Set core_pipe_limit to a non-zero value to make + sure the kernel waits for apport to finish before removing the /proc + information. + - apport/fileutils.py, data/apport: Search for executable name if one + wan't provided such as when being called in a container. + - data/apport: Limit memory and duration of gdbus call. (CVE-2022-28654, + CVE-2022-28656) + - data/apport, apport/fileutils.py, test/test_fileutils.py: Validate + D-Bus socket location. (CVE-2022-28655) + - apport/fileutils.py, test/test_fileutils.py: Turn off interpolation + in get_config() to prevent DoS attacks. (CVE-2022-28652) + - Refactor duplicate code into search_map() function. + - Switch from chroot to container to validating socket owner. + (CVE-2022-1242, CVE-2022-28657) + - data/apport: Clarify error message. + - apport/fileutils.py: Fix typo in comment. + - apport/fileutils.py: Do not call str in loop. + - data/apport, etc/init.d/apport: Switch to using non-positional + arguments. Get real UID and GID from the kernel and make sure they + match the process. Also fix executable name space handling in + argument parsing. (CVE-2022-28658, CVE-2021-3899) + + -- Marc Deslauriers Tue, 10 May 2022 09:23:35 -0400 + apport (2.20.11-0ubuntu27.23) focal; urgency=medium * Fix expanded symlinks from the previous build diff -Nru apport-2.20.11/debian/control apport-2.20.11/debian/control --- apport-2.20.11/debian/control 2020-08-05 08:55:40.000000000 +0000 +++ apport-2.20.11/debian/control 2022-09-15 12:43:39.000000000 +0000 @@ -32,7 +32,8 @@ Maintainer: Ubuntu Developers Standards-Version: 3.9.8 X-Python3-Version: >= 3.0 -Vcs-Bzr: https://code.launchpad.net/~ubuntu-core-dev/ubuntu/focal/apport/ubuntu +Vcs-Browser: https://code.launchpad.net/~ubuntu-core-dev/ubuntu/+source/apport/+git/apport +Vcs-Git: https://git.launchpad.net/~ubuntu-core-dev/ubuntu/+source/apport -b ubuntu/focal Homepage: https://wiki.ubuntu.com/Apport Package: apport diff -Nru apport-2.20.11/debian/tests/control apport-2.20.11/debian/tests/control --- apport-2.20.11/debian/tests/control 2020-05-06 00:23:52.000000000 +0000 +++ apport-2.20.11/debian/tests/control 2022-09-15 12:43:39.000000000 +0000 @@ -5,8 +5,10 @@ apport-gtk, apport-kde, build-essential, + psmisc, python3-twisted, python3-mock, + python3-systemd, xvfb, xterm, dbus-x11, @@ -14,4 +16,4 @@ gvfs-daemons, gnome-icon-theme, libglib2.0-dev, -Restrictions: needs-root, allow-stderr +Restrictions: allow-stderr, isolation-container, needs-root diff -Nru apport-2.20.11/etc/init.d/apport apport-2.20.11/etc/init.d/apport --- apport-2.20.11/etc/init.d/apport 2019-12-06 22:22:30.000000000 +0000 +++ apport-2.20.11/etc/init.d/apport 2022-06-29 13:52:31.000000000 +0000 @@ -50,13 +50,9 @@ rm -f /var/lib/pm-utils/resume-hang.log fi - # Old compatibility mode, switch later to second one - if true; then - echo "|$AGENT %p %s %c %d %P %E" > /proc/sys/kernel/core_pattern - else - echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -E%E" > /proc/sys/kernel/core_pattern - fi + echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E" > /proc/sys/kernel/core_pattern echo 2 > /proc/sys/fs/suid_dumpable + echo 10 > /proc/sys/kernel/core_pipe_limit } # @@ -70,6 +66,7 @@ # 2 if daemon could not be stopped # other if a failure occurred + echo 0 > /proc/sys/kernel/core_pipe_limit echo 0 > /proc/sys/fs/suid_dumpable # Check for a hung resume. If we find one try and grab everything diff -Nru apport-2.20.11/problem_report.py apport-2.20.11/problem_report.py --- apport-2.20.11/problem_report.py 2019-12-12 23:18:39.000000000 +0000 +++ apport-2.20.11/problem_report.py 2022-09-15 12:43:39.000000000 +0000 @@ -11,6 +11,7 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. +import locale import zlib, base64, time, sys, gzip, struct, os from email.encoders import encode_base64 from email.mime.multipart import MIMEMultipart @@ -269,6 +270,24 @@ raise ValueError('%s has no binary content' % [item for item, element in b64_block.items() if element is False]) + def get_timestamp(self) -> int: + '''Get timestamp (seconds since epoch) from Date field + + Return None if it is not present. + ''' + # report time is from asctime(), not in locale representation + orig_ctime = locale.getlocale(locale.LC_TIME) + try: + try: + locale.setlocale(locale.LC_TIME, 'C') + return int(time.mktime(time.strptime(self['Date']))) + except KeyError: + return None + finally: + locale.setlocale(locale.LC_TIME, orig_ctime) + except locale.Error: + return None + def has_removed_fields(self): '''Check if the report has any keys which were not loaded. diff -Nru apport-2.20.11/test/test_backend_apt_dpkg.py apport-2.20.11/test/test_backend_apt_dpkg.py --- apport-2.20.11/test/test_backend_apt_dpkg.py 2020-04-15 23:00:53.000000000 +0000 +++ apport-2.20.11/test/test_backend_apt_dpkg.py 2022-09-15 12:43:39.000000000 +0000 @@ -114,6 +114,9 @@ self.assertGreater(len(d), 2) self.assertIn('libc6', d) for dep in d: + if dep == "bash-completion": + # bash-completion is only in Recommends (maybe not installed) + continue self.assertTrue(impl.get_version(dep)) # Pre-Depends: only @@ -124,7 +127,7 @@ self.assertTrue(impl.get_version(dep)) # Depends: only - d = impl.get_dependencies('libc6') + d = impl.get_dependencies('libc-bin') self.assertGreaterEqual(len(d), 1) for dep in d: self.assertTrue(impl.get_version(dep)) @@ -732,10 +735,10 @@ def test_install_packages_permanent_sandbox(self): '''install_packages() with a permanent sandbox''' - self._setup_foonux_config() + release = self._setup_foonux_config(release='jammy') zonetab = os.path.join(self.rootdir, 'usr/share/zoneinfo/zone.tab') - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', + impl.install_packages(self.rootdir, self.configdir, release, [('tzdata', None)], False, self.cachedir, permanent_rootdir=True) # This will now be using a Cache with our rootdir. @@ -746,7 +749,7 @@ tzdata_written = os.path.getctime(tzdata[0]) zonetab_written = os.path.getctime(zonetab) - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', + impl.install_packages(self.rootdir, self.configdir, release, [('coreutils', None), ('tzdata', None)], False, self.cachedir, permanent_rootdir=True) @@ -771,24 +774,24 @@ orig_no_proxy = None self.assertRaises(SystemExit, impl.install_packages, self.rootdir, - self.configdir, 'Foonux 14.04', [('libc6', None)], False, + self.configdir, release, [('libc6', None)], False, self.cachedir, permanent_rootdir=True) # These packages exist, so attempting to install them should not fail. - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', + impl.install_packages(self.rootdir, self.configdir, release, [('coreutils', None), ('tzdata', None)], False, self.cachedir, permanent_rootdir=True) # even without cached debs, trying to install the same versions should # be a no-op and succeed - for f in glob.glob('%s/Foonux 14.04/apt/var/cache/apt/archives/coreutils*' % self.cachedir): + for f in glob.glob(f'{self.cachedir}/{release}/apt/var/cache/apt/archives/coreutils*'): os.unlink(f) - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', + impl.install_packages(self.rootdir, self.configdir, release, [('coreutils', None)], False, self.cachedir, permanent_rootdir=True) # trying to install another package should fail, though self.assertRaises(SystemExit, impl.install_packages, self.rootdir, - self.configdir, 'Foonux 14.04', [('aspell-doc', None)], False, + self.configdir, release, [('aspell-doc', None)], False, self.cachedir, permanent_rootdir=True) # restore original proxy settings @@ -802,22 +805,24 @@ @unittest.skipUnless(_has_internet(), 'online test') def test_install_packages_permanent_sandbox_repack(self): - self._setup_foonux_config() - include_path = os.path.join(self.rootdir, 'usr/include/krb5.h') - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('libkrb5-dev', None)], False, self.cachedir, + # Both packages needs to conflict with each other, because they + # ship the same file. + release = self._setup_foonux_config(release='jammy') + impl.install_packages(self.rootdir, self.configdir, release, + [("libcurl4-gnutls-dev", None)], False, self.cachedir, permanent_rootdir=True) - self.assertIn('mit-krb5/', os.readlink(include_path)) + curl_library = self._get_library_path("libcurl.so", self.rootdir) + self.assertEqual("libcurl-gnutls.so", os.readlink(curl_library)) - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('heimdal-dev', None)], False, self.cachedir, + impl.install_packages(self.rootdir, self.configdir, release, + [("libcurl4-nss-dev", None)], False, self.cachedir, permanent_rootdir=True) - self.assertIn('heimdal/', os.readlink(include_path)) + self.assertEqual("libcurl-nss.so", os.readlink(curl_library)) - impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('libkrb5-dev', None)], False, self.cachedir, + impl.install_packages(self.rootdir, self.configdir, release, + [("libcurl4-gnutls-dev", None)], False, self.cachedir, permanent_rootdir=True) - self.assertIn('mit-krb5/', os.readlink(include_path)) + self.assertEqual("libcurl-gnutls.so", os.readlink(curl_library)) @unittest.skipUnless(_has_internet(), 'online test') @unittest.skipIf(impl.get_system_architecture() == 'armhf', 'native armhf architecture') @@ -845,84 +850,14 @@ self.assertIn('libc6_2.23-0ubuntu3_armhf.deb', cache) @unittest.skipUnless(_has_internet(), 'online test') - def test_install_packages_from_launchpad(self): - '''install_packages() using packages only available on Launchpad''' - - self._setup_foonux_config() - obsolete = impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('oxideqt-codecs', - '1.6.6-0ubuntu0.14.04.1'), - ('distro-info-data', - '0.18ubuntu0.2'), - ('qemu-utils', - '2.0.0+dfsg-2ubuntu1.11'), - ('unity-services', - '7.2.5+14.04.20150521.1-0ubuntu1'), - ], False, self.cachedir) - - def sandbox_ver(pkg, debian=True): - if debian: - changelog = 'changelog.Debian.gz' - else: - changelog = 'changelog.gz' - with gzip.open(os.path.join(self.rootdir, 'usr/share/doc', pkg, - changelog)) as f: - return f.readline().decode().split()[1][1:-1] - - self.assertEqual(obsolete, '') - - # packages get installed - self.assertTrue(os.path.exists(os.path.join(self.rootdir, - 'usr/share/doc/oxideqt-codecs/copyright'))) - self.assertTrue(os.path.exists(os.path.join(self.rootdir, - 'usr/share/distro-info/ubuntu.csv'))) - - # their versions are as expected - self.assertEqual(sandbox_ver('oxideqt-codecs'), - '1.6.6-0ubuntu0.14.04.1') - self.assertEqual(sandbox_ver('oxideqt-codecs-dbg'), - '1.6.6-0ubuntu0.14.04.1') - self.assertEqual(sandbox_ver('distro-info-data', debian=False), - '0.18ubuntu0.2') - - # keeps track of package versions - with open(os.path.join(self.rootdir, 'packages.txt')) as f: - pkglist = f.read().splitlines() - self.assertIn('oxideqt-codecs 1.6.6-0ubuntu0.14.04.1', pkglist) - self.assertIn('oxideqt-codecs-dbg 1.6.6-0ubuntu0.14.04.1', pkglist) - self.assertIn('distro-info-data 0.18ubuntu0.2', pkglist) - self.assertIn('qemu-utils-dbgsym 2.0.0+dfsg-2ubuntu1.11', - pkglist) - # FIXME: doesn't work in autopkgtest - # self.assertIn('unity-services-dbgsym 7.2.5+14.04.20150521.1-0ubuntu1', - # pkglist) - - # caches packages, and their versions are as expected - cache = os.listdir(os.path.join(self.cachedir, 'Foonux 14.04', 'apt', - 'var', 'cache', 'apt', 'archives')) - - # archive and launchpad versions of packages exist in the cache, so use a list - cache_versions = [] - for p in cache: - try: - (name, ver) = p.split('_')[:2] - cache_versions.append((name, ver)) - except ValueError: - pass # not a .deb, ignore - self.assertIn(('oxideqt-codecs', '1.6.6-0ubuntu0.14.04.1'), cache_versions) - self.assertIn(('oxideqt-codecs-dbg', '1.6.6-0ubuntu0.14.04.1'), cache_versions) - self.assertIn(('distro-info-data', '0.18ubuntu0.2'), cache_versions) - self.assertIn(('qemu-utils-dbgsym', '2.0.0+dfsg-2ubuntu1.11'), cache_versions) - - @unittest.skipUnless(_has_internet(), 'online test') def test_install_old_packages(self): '''sandbox will install older package versions from launchpad''' - - self._setup_foonux_config() - obsolete = impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('oxideqt-codecs', - '1.7.8-0ubuntu0.14.04.1'), - ], False, self.cachedir) + release = self._setup_foonux_config(release='jammy') + wanted_package = "libcurl4" + wanted_version = "7.81.0-1" # pre-release version + obsolete = impl.install_packages(self.rootdir, self.configdir, release, + [(wanted_package, wanted_version)], + False, self.cachedir) self.assertEqual(obsolete, '') @@ -932,29 +867,27 @@ return f.readline().decode().split()[1][1:-1] # the version is as expected - self.assertEqual(sandbox_ver('oxideqt-codecs'), - '1.7.8-0ubuntu0.14.04.1') + self.assertEqual(sandbox_ver(wanted_package), wanted_version) # keeps track of package version with open(os.path.join(self.rootdir, 'packages.txt')) as f: pkglist = f.read().splitlines() - self.assertIn('oxideqt-codecs 1.7.8-0ubuntu0.14.04.1', pkglist) + self.assertIn(f"{wanted_package} {wanted_version}", pkglist) - obsolete = impl.install_packages(self.rootdir, self.configdir, 'Foonux 14.04', - [('oxideqt-codecs', - '1.6.6-0ubuntu0.14.04.1'), - ], False, self.cachedir) + wanted_version = "7.74.0-1.3ubuntu3" + obsolete = impl.install_packages(self.rootdir, self.configdir, release, + [(wanted_package, wanted_version)], + False, self.cachedir) self.assertEqual(obsolete, '') # the old version is installed - self.assertEqual(sandbox_ver('oxideqt-codecs'), - '1.6.6-0ubuntu0.14.04.1') + self.assertEqual(sandbox_ver(wanted_package), wanted_version) # the old versions is tracked with open(os.path.join(self.rootdir, 'packages.txt')) as f: pkglist = f.read().splitlines() - self.assertIn('oxideqt-codecs 1.6.6-0ubuntu0.14.04.1', pkglist) + self.assertIn(f"{wanted_package} {wanted_version}", pkglist) @unittest.skipUnless(_has_internet(), 'online test') def test_get_source_tree_sandbox(self): @@ -1059,6 +992,24 @@ self.assertEqual(sandbox_ver('apport'), '2.20.1-0ubuntu2.22~ppa1') + def _get_library_path(self, library_name: str, root_dir: str = "/") -> str: + """Find library path regardless of the architecture.""" + libraries = glob.glob( + os.path.join(root_dir, "usr/lib/*", library_name) + ) + self.assertEqual( + len(libraries), 1, f"glob for {library_name}: {libraries!r}" + ) + return libraries[0] + + def _ubuntu_archive_uri(self, arch=None): + """Return archive URI for the given architecture.""" + if arch is None: + arch = impl.get_system_architecture() + if arch in ("amd64", "i386"): + return "http://archive.ubuntu.com/ubuntu" + return "http://ports.ubuntu.com/ubuntu-ports" + def _setup_foonux_config(self, updates=False, release='trusty', ppa=False): '''Set up directories and configuration for install_packages() @@ -1069,7 +1020,8 @@ versions = {'trusty': '14.04', 'xenial': '16.04', 'boinic': '18.04', - 'cosmic': '18.10'} + 'cosmic': '18.10', + 'jammy': '22.04'} vers = versions[release] self.cachedir = os.path.join(self.workdir, 'cache') self.rootdir = os.path.join(self.workdir, 'root') @@ -1078,28 +1030,26 @@ os.mkdir(self.rootdir) os.mkdir(self.configdir) os.mkdir(os.path.join(self.configdir, 'Foonux %s' % vers)) - with open(os.path.join(self.configdir, 'Foonux %s' % vers, 'sources.list'), 'w') as f: - f.write('deb http://archive.ubuntu.com/ubuntu/ %s main\n' % release) - f.write('deb-src http://archive.ubuntu.com/ubuntu/ %s main\n' % release) - f.write('deb http://ddebs.ubuntu.com/ %s main\n' % release) - if updates: - f.write('deb http://archive.ubuntu.com/ubuntu/ %s-updates main\n' % release) - f.write('deb-src http://archive.ubuntu.com/ubuntu/ %s-updates main\n' % release) - f.write('deb http://ddebs.ubuntu.com/ %s-updates main\n' % release) + self._write_source_file( + os.path.join(self.configdir, 'Foonux %s' % vers, 'sources.list'), + self._ubuntu_archive_uri(), + release, + updates, + ) if ppa: os.mkdir(os.path.join(self.configdir, 'Foonux %s' % vers, 'sources.list.d')) with open(os.path.join(self.configdir, 'Foonux %s' % vers, 'sources.list.d', 'fooser-bar-ppa.list'), 'w') as f: - f.write('deb http://ppa.launchpad.net/fooser/bar-ppa/ubuntu %s main main/debug\n' % release) - f.write('deb-src http://ppa.launchpad.net/fooser/bar-ppa/ubuntu %s main\n' % release) + f.write( + f'deb http://ppa.launchpad.net/fooser/bar-ppa/ubuntu {release} main main/debug\n' + f'deb-src http://ppa.launchpad.net/fooser/bar-ppa/ubuntu {release} main\n' + ) os.mkdir(os.path.join(self.configdir, 'Foonux %s' % vers, 'armhf')) - with open(os.path.join(self.configdir, 'Foonux %s' % vers, 'armhf', 'sources.list'), 'w') as f: - f.write('deb http://ports.ubuntu.com/ %s main\n' % release) - f.write('deb-src http://ports.ubuntu.com/ %s main\n' % release) - f.write('deb http://ddebs.ubuntu.com/ %s main\n' % release) - if updates: - f.write('deb http://ports.ubuntu.com/ %s-updates main\n' % release) - f.write('deb-src http://ports.ubuntu.com/ %s-updates main\n' % release) - f.write('deb http://ddebs.ubuntu.com/ %s-updates main\n' % release) + self._write_source_file( + os.path.join(self.configdir, 'Foonux %s' % vers, 'armhf', 'sources.list'), + self._ubuntu_archive_uri("armhf"), + release, + updates, + ) with open(os.path.join(self.configdir, 'Foonux %s' % vers, 'codename'), 'w') as f: f.write('%s' % release) @@ -1116,6 +1066,22 @@ # that looks for trusted.gpg.d relative to sources.list. keyring_arch_dir = os.path.join(self.configdir, 'Foonux %s' % vers, 'armhf', 'trusted.gpg.d') os.symlink("../trusted.gpg.d", keyring_arch_dir) + return f'Foonux {vers}' + + def _write_source_file(self, sources_filename: str, uri: str, release: str, updates: bool) -> None: + '''Write sources.list file.''' + with open(sources_filename, 'w') as sources_file: + sources_file.write( + f'deb {uri} {release} main\n' + f'deb-src {uri} {release} main\n' + f'deb http://ddebs.ubuntu.com/ {release} main\n' + ) + if updates: + sources_file.write( + f'deb {uri} {release}-updates main\n' + f'deb-src {uri} {release}-updates main\n' + f'deb http://ddebs.ubuntu.com/ {release}-updates main\n' + ) def assert_elf_arch(self, path, expected): '''Assert that an ELF file is for an expected machine type. @@ -1123,9 +1089,12 @@ Expected is a Debian-style architecture (i386, amd64, armhf) ''' archmap = { - 'i386': '80386', - 'amd64': 'X86-64', - 'armhf': 'ARM', + "amd64": "X86-64", + "arm64": "AArch64", + "armhf": "ARM", + "i386": "80386", + "ppc64el": "PowerPC64", + "s390x": "IBM S/390", } # get ELF machine type diff -Nru apport-2.20.11/test/test_fileutils.py apport-2.20.11/test/test_fileutils.py --- apport-2.20.11/test/test_fileutils.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/test/test_fileutils.py 2022-09-15 12:43:39.000000000 +0000 @@ -367,8 +367,32 @@ self.assertEqual(apport.fileutils.get_config('spethial', 'nope', 'moo'), 'moo') apport.fileutils.get_config.config = None # trash cache + # interpolation + f.write(b'[inter]\none=1\ntwo = TWO\ntest = %(two)s\n') + f.flush() + self.assertEqual(apport.fileutils.get_config('inter', 'one'), '1') + self.assertEqual(apport.fileutils.get_config('inter', 'two'), 'TWO') + self.assertEqual(apport.fileutils.get_config('inter', 'test'), '%(two)s') + apport.fileutils.get_config.config = None # trash cache + f.close() + def test_get_dbus_socket(self): + '''get_dbus_socket()''' + + tests = [("unix:path=/run/user/1000/bus", "/run/user/1000/bus"), + ("unix:path=/run/user/1000/bus;unix:path=/run/user/0/bus", None), + ("unix:path=%2Frun/user/1000/bus", None), + ("unix:path=/run/user/1000/bus,path=/run/user/0/bus", None), + ("unix:path=/etc/passwd", None), + ("unix:path=/run/user/../../etc/passwd", None), + ("unix:path=/run/user/1000/bus=", None), + ("", None), + ("tcp:host=localhost,port=8100", None)] + + for (addr, result) in tests: + self.assertEqual(apport.fileutils.get_dbus_socket(addr), result) + def test_shared_libraries(self): '''shared_libraries()''' diff -Nru apport-2.20.11/test/test_hookutils.py apport-2.20.11/test/test_hookutils.py --- apport-2.20.11/test/test_hookutils.py 2021-08-25 13:42:27.000000000 +0000 +++ apport-2.20.11/test/test_hookutils.py 2022-09-15 12:43:39.000000000 +0000 @@ -1,5 +1,7 @@ # coding: UTF-8 +import time import unittest, tempfile, locale, subprocess, re, shutil, os, sys +import unittest.mock import apport.hookutils @@ -28,13 +30,11 @@ bad_ko = _build_ko('BAD') # test: - # - loaded real module # - unfindable module # - fake GPL module # - fake BAD module # direct license check - self.assertTrue('GPL' in apport.hookutils._get_module_license('isofs')) self.assertEqual(apport.hookutils._get_module_license('does-not-exist'), 'invalid') self.assertTrue('GPL' in apport.hookutils._get_module_license(good_ko.name)) self.assertTrue('BAD' in apport.hookutils._get_module_license(bad_ko.name)) @@ -45,11 +45,24 @@ (good_ko.name, bad_ko.name)).encode()) f.flush() nonfree = apport.hookutils.nonfree_kernel_modules(f.name) - self.assertFalse('isofs' in nonfree) self.assertTrue('does-not-exist' in nonfree) self.assertFalse(good_ko.name in nonfree) self.assertTrue(bad_ko.name in nonfree) + def test_real_module_license_evaluation(self): + '''module licenses can be validated correctly for real module''' + isofs_license = apport.hookutils._get_module_license('isofs') + if isofs_license == 'invalid': + self.skipTest("kernel module 'isofs' not available") + + self.assertIn("GPL", isofs_license) + + f = tempfile.NamedTemporaryFile() + f.write(b'isofs\n') + f.flush() + nonfree = apport.hookutils.nonfree_kernel_modules(f.name) + self.assertNotIn('isofs', nonfree) + def test_attach_dmesg(self): '''attach_dmesg()''' @@ -177,6 +190,54 @@ apport.hookutils.attach_file_if_exists(report, '/etc/../etc/passwd') self.assertEqual(list(report), []) + @unittest.mock.patch("subprocess.Popen") + @unittest.mock.patch( + "os.path.exists", unittest.mock.MagicMock(return_value=True) + ) + def test_attach_journal_errors_with_date(self, popen_mock): + popen_mock.return_value.returncode = 0 + popen_mock.return_value.communicate.return_value = ( + b"journalctl output", + b"", + ) + + report = apport.Report(date="Wed May 18 18:31:08 2022") + apport.hookutils.attach_journal_errors(report) + + self.assertEqual(popen_mock.call_count, 1) + self.assertEqual(report.get("JournalErrors"), "journalctl output") + self.assertEqual( + popen_mock.call_args[0][0], + [ + "journalctl", + "--priority=warning", + f"--since=@{1652898658 + time.altzone}", + f"--until=@{1652898678 + time.altzone}", + ], + ) + + @unittest.mock.patch("subprocess.Popen") + @unittest.mock.patch( + "os.path.exists", unittest.mock.MagicMock(return_value=True) + ) + def test_attach_journal_errors_without_date(self, popen_mock): + popen_mock.return_value.returncode = 0 + popen_mock.return_value.communicate.return_value = ( + b"journalctl output", + b"", + ) + + report = apport.Report() + del report["Date"] + apport.hookutils.attach_journal_errors(report) + + self.assertEqual(popen_mock.call_count, 1) + self.assertEqual(report.get("JournalErrors"), "journalctl output") + self.assertEqual( + popen_mock.call_args[0][0], + ["journalctl", "--priority=warning", "-b", "--lines=1000"], + ) + def test_path_to_key(self): '''transforming a file path to a valid report key''' diff -Nru apport-2.20.11/test/test_problem_report.py apport-2.20.11/test/test_problem_report.py --- apport-2.20.11/test/test_problem_report.py 2019-12-04 20:25:28.000000000 +0000 +++ apport-2.20.11/test/test_problem_report.py 2022-09-15 12:43:39.000000000 +0000 @@ -1,5 +1,6 @@ # vim: set encoding=UTF-8 fileencoding=UTF-8 : import unittest, tempfile, os, shutil, email, gzip, time, sys +import locale from io import BytesIO import problem_report @@ -45,6 +46,33 @@ pr = problem_report.ProblemReport(date='19801224 12:34') self.assertEqual(pr['Date'], '19801224 12:34') + def test_get_timestamp(self): + '''get_timestamp() returns timestamp.''' + r = problem_report.ProblemReport() + self.assertAlmostEqual(r.get_timestamp(), time.time(), delta=2) + + r['Date'] = 'Thu Jan 9 12:00:00 2014' + # delta is ±12 hours, as this depends on the timezone that the test is + # run in + self.assertAlmostEqual(r.get_timestamp(), 1389265200, delta=43200) + + def test_get_timestamp_locale_german(self): + '''get_timestamp() returns date when LC_TIME is set.''' + pr = problem_report.ProblemReport(date='Wed May 18 09:49:57 2022') + orig_ctime = locale.getlocale(locale.LC_TIME) + try: + locale.setlocale(locale.LC_TIME, "de_DE.UTF-8") + except locale.Error: + self.skipTest("Missing German locale support") + self.assertEqual(pr.get_timestamp(), 1652867397 + time.altzone) + locale.setlocale(locale.LC_TIME, orig_ctime) + + def test_get_timestamp_returns_none(self): + '''get_timestamp() returns None.''' + pr = problem_report.ProblemReport() + del pr['Date'] + self.assertEqual(pr.get_timestamp(), None) + def test_sanity_checks(self): '''various error conditions.''' diff -Nru apport-2.20.11/test/test_report.py apport-2.20.11/test/test_report.py --- apport-2.20.11/test/test_report.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/test/test_report.py 2022-09-15 12:43:39.000000000 +0000 @@ -584,9 +584,9 @@ self.assertNotIn('Core was generated by', pr['Stacktrace']) self.assertNotRegex(pr['Stacktrace'], r'(?s)(^|.*\n)#0 [^\n]+\n#0 ') self.assertIn('#0 ', pr['Stacktrace']) - self.assertIn('#1 0x', pr['Stacktrace']) + self.assertRegex(pr['Stacktrace'], r'#[123] 0x') self.assertIn('#0 ', pr['ThreadStacktrace']) - self.assertIn('#1 0x', pr['ThreadStacktrace']) + self.assertRegex(pr['ThreadStacktrace'], r'#[123] 0x') self.assertIn('Thread 1 (', pr['ThreadStacktrace']) self.assertLessEqual(len(pr['StacktraceTop'].splitlines()), 5) @@ -724,7 +724,9 @@ pr.add_proc_info() pr.add_hooks_info('fake_ui') - self.assertIn('not located in a known VMA region', pr['SegvAnalysis']) + if pr['Architecture'] in ['amd64', 'i386']: + # data/general-hooks/parse_segv.py only runs for x86 and x86_64 + self.assertIn('not located in a known VMA region', pr['SegvAnalysis']) def test_add_gdb_info_script(self): '''add_gdb_info() with a script.''' @@ -777,7 +779,7 @@ os.unlink(script) self._validate_gdb_fields(pr) - self.assertTrue('libc.so' in pr['Stacktrace'] or 'in execute_command' in pr['Stacktrace']) + self.assertIn('in kill_builtin', pr['Stacktrace']) def test_add_gdb_info_abort(self): '''add_gdb_info() with SIGABRT/assert() @@ -2394,18 +2396,6 @@ self.assertGreater(timestamp, time.mktime(time.strptime('2014-01-01', '%Y-%m-%d'))) - def test_get_timestamp(self): - r = apport.Report() - self.assertAlmostEqual(r.get_timestamp(), time.time(), delta=2) - - r['Date'] = 'Thu Jan 9 12:00:00 2014' - # delta is ±12 hours, as this depends on the timezone that the test is - # run in - self.assertAlmostEqual(r.get_timestamp(), 1389265200.0, delta=43200) - - del r['Date'] - self.assertEqual(r.get_timestamp(), None) - def test_command_output_passes_env(self): fake_env = {'GCONV_PATH': '/tmp'} out = apport.report._command_output(['env'], env=fake_env) diff -Nru apport-2.20.11/test/test_signal_crashes.py apport-2.20.11/test/test_signal_crashes.py --- apport-2.20.11/test/test_signal_crashes.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/test/test_signal_crashes.py 2022-09-15 12:43:39.000000000 +0000 @@ -8,10 +8,9 @@ # the full text of the license. import tempfile, shutil, os, subprocess, signal, time, stat, sys -import resource, errno, grp, unittest, socket, array, pwd +import resource, errno, grp, unittest, socket, array import apport.fileutils -test_executable = '/usr/bin/yes' test_package = 'coreutils' test_source = 'coreutils' @@ -42,7 +41,25 @@ pass +def pidof(program): + """Find the process ID of a running program. + + This function wraps the pidof command and returns a set of + process IDs. + """ + try: + stdout = subprocess.check_output(["/bin/pidof", "-nx", program]) + except subprocess.CalledProcessError as error: + if error.returncode == 1: + return set() + raise # pragma: no cover + return set(int(pid) for pid in stdout.decode().split()) + + class T(unittest.TestCase): + TEST_EXECUTABLE = os.path.realpath("/bin/sleep") + TEST_ARGS = ["86400"] + def setUp(self): # use local report dir self.report_dir = tempfile.mkdtemp() @@ -63,7 +80,7 @@ # expected report name for test executable report self.test_report = os.path.join( apport.fileutils.report_dir, '%s.%i.crash' % - (test_executable.replace('/', '_'), os.getuid())) + (self.TEST_EXECUTABLE.replace('/', '_'), os.getuid())) def tearDown(self): shutil.rmtree(self.report_dir) @@ -127,8 +144,11 @@ pr.load(f) self.assertTrue(set(required_fields).issubset(set(pr.keys())), 'report has required fields') - self.assertEqual(pr['ExecutablePath'], test_executable) - self.assertEqual(pr['ProcCmdline'], test_executable) + self.assertEqual(pr['ExecutablePath'], self.TEST_EXECUTABLE) + self.assertEqual( + pr['ProcCmdline'], + " ".join([self.TEST_EXECUTABLE] + self.TEST_ARGS), + ) self.assertEqual(pr['Signal'], '%i' % signal.SIGSEGV) # check safe environment subset @@ -157,7 +177,7 @@ '''only one apport instance is ran at a time''' test_proc = self.create_test_process() - test_proc2 = self.create_test_process(False, '/bin/dd') + test_proc2 = self.create_test_process(False, '/bin/dd', args=[]) try: app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'], stdin=subprocess.PIPE, stderr=subprocess.PIPE) @@ -199,7 +219,7 @@ local_exe = os.path.join(self.workdir, 'mybin') with open(local_exe, 'wb') as dest: - with open(test_executable, 'rb') as src: + with open(self.TEST_EXECUTABLE, 'rb') as src: dest.write(src.read()) os.chmod(local_exe, 0o755) self.do_crash(command=local_exe) @@ -212,14 +232,14 @@ with open(local_exe, 'w') as f: f.write('#!/bin/sh\nkill -SEGV $$') os.chmod(local_exe, 0o755) - self.do_crash(command=local_exe) + self.do_crash(command=local_exe, args=[]) # absolute path self.assertEqual(apport.fileutils.get_all_reports(), []) # relative path os.chdir(self.workdir) - self.do_crash(command='./myscript') + self.do_crash(command='./myscript', args=[]) self.assertEqual(apport.fileutils.get_all_reports(), []) def test_ignore_sigquit(self): @@ -235,7 +255,7 @@ with open(local_exe, 'w') as f: f.write('#!/usr/bin/perl\nsystem("mv $0 $0.exe");\nsystem("ln -sf /etc/shadow $0");\n$0="..$0";\nsleep(10);\n') os.chmod(local_exe, 0o755) - self.do_crash(check_running=False, command=local_exe, sleep=2) + self.do_crash(check_running=False, command=local_exe, args=[], sleep=2) leak = os.path.join(apport.fileutils.report_dir, '_usr_bin_perl.%i.crash' % (os.getuid())) @@ -289,7 +309,7 @@ # regards as likely packaged (fd, myexe) = tempfile.mkstemp(dir='/var/tmp') self.addCleanup(os.unlink, myexe) - with open(test_executable, 'rb') as f: + with open(self.TEST_EXECUTABLE, 'rb') as f: os.write(fd, f.read()) os.close(fd) os.chmod(myexe, 0o111) @@ -340,8 +360,9 @@ self.do_crash(expect_corefile=True) apport.fileutils.delete_report(self.test_report) - # for SIGQUIT we only expect core files, no report - resource.setrlimit(resource.RLIMIT_CORE, (1000000, -1)) + def test_core_dump_packaged_sigquit(self): + '''packaged executables create core files, no report for SIGQUIT''' + resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) self.do_crash(expect_corefile=True, sig=signal.SIGQUIT) self.assertEqual(apport.fileutils.get_all_reports(), []) @@ -350,7 +371,7 @@ local_exe = os.path.join(self.workdir, 'mybin') with open(local_exe, 'wb') as dest: - with open(test_executable, 'rb') as src: + with open(self.TEST_EXECUTABLE, 'rb') as src: dest.write(src.read()) os.chmod(local_exe, 0o755) @@ -385,7 +406,7 @@ # get the name of the core file (core_name, core_path) = apport.fileutils.get_core_path(pid, - test_executable) + self.TEST_EXECUTABLE) os.kill(pid, signal.SIGSEGV) @@ -459,12 +480,15 @@ os.unlink(reports[0]) self.assertEqual(pr['Signal'], '42') - self.assertEqual(pr['ExecutablePath'], test_executable) + self.assertEqual(pr['ExecutablePath'], self.TEST_EXECUTABLE) self.assertFalse('CoreDump' in pr) # FIXME: sometimes this is empty!? if err: self.assertRegex( - err, b'core dump exceeded.*dropped from .*yes\\..*\\.crash') + err.decode(), + f"core dump exceeded.*dropped from .*" + f"{os.path.basename(self.TEST_EXECUTABLE)}\\..*\\.crash", + ) def test_ignore(self): '''ignoring executables''' @@ -491,7 +515,7 @@ # likely packaged (fd, myexe) = tempfile.mkstemp(dir='/var/tmp') self.addCleanup(os.unlink, myexe) - with open(test_executable, 'rb') as f: + with open(self.TEST_EXECUTABLE, 'rb') as f: os.write(fd, f.read()) os.close(fd) os.chmod(myexe, 0o755) @@ -557,7 +581,7 @@ os.unlink(reports[0]) self.assertEqual(pr['Signal'], '42') - self.assertEqual(pr['ExecutablePath'], test_executable) + self.assertEqual(pr['ExecutablePath'], self.TEST_EXECUTABLE) self.assertEqual(pr['CoreDump'], b'hel\x01lo') def test_logging_stderr(self): @@ -592,7 +616,7 @@ os.unlink(reports[0]) self.assertEqual(pr['Signal'], '42') - self.assertEqual(pr['ExecutablePath'], test_executable) + self.assertEqual(pr['ExecutablePath'], self.TEST_EXECUTABLE) self.assertEqual(pr['CoreDump'], b'hel\x01lo') @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root') @@ -603,7 +627,7 @@ # regards as likely packaged (fd, myexe) = tempfile.mkstemp(dir='/var/tmp') self.addCleanup(os.unlink, myexe) - with open(test_executable, 'rb') as f: + with open(self.TEST_EXECUTABLE, 'rb') as f: os.write(fd, f.read()) os.close(fd) os.chmod(myexe, 0o4755) @@ -633,29 +657,26 @@ def test_crash_system_slice(self): '''report generation for a protected process running in the system slice''' + self.assertEqual(pidof(self.TEST_EXECUTABLE), set(), 'no running test executable processes') self.create_test_process(command='/usr/bin/systemd-run', + check_running=False, args=['-t', '-q', '--slice=system.slice', '-p', 'ProtectSystem=true', - '/usr/bin/yes']) - yes_pid = int(subprocess.check_output(['pidof', - '/usr/bin/yes']).strip()) - os.kill(yes_pid, signal.SIGSEGV) + self.TEST_EXECUTABLE] + self.TEST_ARGS) + pids = pidof(self.TEST_EXECUTABLE) + self.assertEqual(len(pids), 1) + os.kill(pids.pop(), signal.SIGSEGV) - # wait max 10 seconds for apport to finish - timeout = 50 - while timeout >= 0: - pidof = subprocess.Popen(['pidof', '-x', 'apport'], - stdout=subprocess.PIPE) - pidof.communicate() - if pidof.returncode != 0: - break - time.sleep(0.2) - timeout -= 1 + self.wait_for_no_instance_running(self.TEST_EXECUTABLE) + self.wait_for_apport_to_finish() # check crash report reports = apport.fileutils.get_all_reports() self.assertEqual(len(reports), 1) - self.assertEqual(reports[0], '/var/crash/_usr_bin_yes.0.crash') + self.assertEqual( + reports[0], + f"/var/crash/{self.TEST_EXECUTABLE.replace('/', '_')}.0.crash", + ) @unittest.skipUnless(os.path.exists('/bin/ping'), 'this test needs /bin/ping') @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root') @@ -684,43 +705,6 @@ uid=8) self.assertEqual(apport.fileutils.get_all_reports(), []) - @unittest.skipUnless(os.path.exists('/bin/ping'), 'this test needs /bin/ping') - @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root') - def test_crash_setuid_drop_and_kill(self): - '''process started by root as another user, killed by that user no core''' - # override expected report file name - self.test_report = os.path.join( - apport.fileutils.report_dir, '%s.%i.crash' % - ('_usr_bin_crontab', os.getuid())) - # edit crontab as user "mail" - resource.setrlimit(resource.RLIMIT_CORE, (-1, -1)) - - if suid_dumpable: - user = pwd.getpwuid(8) - # if a user can crash a suid root binary, it should not create core files - orig_editor = os.getenv('EDITOR') - os.environ['EDITOR'] = '/usr/bin/yes' - self.do_crash(command='/usr/bin/crontab', args=['-e', '-u', user[0]], - expect_corefile=False, core_location='/var/spool/cron/', - killer_id=8) - if orig_editor is not None: - os.environ['EDITOR'] = orig_editor - - # check crash report - reports = apport.fileutils.get_all_reports() - self.assertEqual(len(reports), 1) - report = reports[0] - st = os.stat(report) - os.unlink(report) - self.assertEqual(stat.S_IMODE(st.st_mode), 0o640, 'report has correct permissions') - # this must be owned by root as it is a setuid binary - self.assertEqual(st.st_uid, 0, 'report has correct owner') - else: - # no cores/dump if suid_dumpable == 0 - self.do_crash(False, command='/bin/ping', args=['127.0.0.1'], - uid=8) - self.assertEqual(apport.fileutils.get_all_reports(), []) - @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root') def test_crash_setuid_unpackaged(self): '''report generation for unpackaged setuid program''' @@ -729,7 +713,7 @@ # regards as not packaged (fd, myexe) = tempfile.mkstemp(dir='/tmp') self.addCleanup(os.unlink, myexe) - with open(test_executable, 'rb') as f: + with open(self.TEST_EXECUTABLE, 'rb') as f: os.write(fd, f.read()) os.close(fd) os.chmod(myexe, 0o4755) @@ -755,7 +739,7 @@ # regards as likely packaged (fd, myexe) = tempfile.mkstemp(dir='/var/tmp') self.addCleanup(os.unlink, myexe) - with open(test_executable, 'rb') as f: + with open(self.TEST_EXECUTABLE, 'rb') as f: os.write(fd, f.read()) os.close(fd) os.chmod(myexe, 0o4755) @@ -835,7 +819,7 @@ pr.load(f) os.unlink(reports[0]) self.assertEqual(pr['Signal'], '11') - self.assertEqual(pr['ExecutablePath'], test_executable) + self.assertEqual(pr['ExecutablePath'], self.TEST_EXECUTABLE) self.assertEqual(pr['CoreDump'], b'hel\x01lo') # should not create report on the host @@ -845,13 +829,17 @@ # Helper methods # - @classmethod - def create_test_process(klass, check_running=True, command=test_executable, - uid=None, args=[]): - '''Spawn test_executable. + def create_test_process(self, check_running=True, command=None, + uid=None, args=None): + '''Spawn test executable. Wait until it is fully running, and return its PID. ''' + if command is None: + command = self.TEST_EXECUTABLE + if args is None: + args = self.TEST_ARGS + assert os.access(command, os.X_OK), command + ' is not executable' if check_running: assert subprocess.call(['pidof', command]) == 1, 'no running test executable processes' @@ -881,13 +869,12 @@ def do_crash(self, expect_coredump=True, expect_corefile=False, sig=signal.SIGSEGV, check_running=True, sleep=0, - command=test_executable, uid=None, + command=None, uid=None, expect_corefile_owner=None, - core_location=None, - killer_id=False, args=[]): + args=None): '''Generate a test crash. - This runs command (by default test_executable) in cwd, lets it crash, + This runs command (by default TEST_EXECUTABLE) in cwd, lets it crash, and checks that it exits with the expected return code, leaving a core file behind if expect_corefile is set, and generating a crash report if expect_coredump is set. @@ -895,6 +882,10 @@ If check_running is set (default), this will abort if test_process is already running. ''' + if command is None: + command = self.TEST_EXECUTABLE + if args is None: + args = self.TEST_ARGS self.assertFalse(os.path.exists('core'), '%s/core already exists, please clean up first' % os.getcwd()) pid = self.create_test_process(check_running, command, uid=uid, args=args) @@ -904,21 +895,7 @@ if sleep > 0: time.sleep(sleep) - if killer_id: - user = pwd.getpwuid(killer_id) - # testing different editors via VISUAL= didn't help - kill = subprocess.Popen(['sudo', '-s', '/bin/bash', '-c', - "/bin/kill -s %i %s" % (sig, pid), - '-u', user[0]]) # 'mail']) - kill.communicate() - # need to clean up system state - if command == '/usr/bin/crontab': - os.system('stty sane') - if kill.returncode != 0: - self.fail("Couldn't kill process %s as user %s." % - (pid, user[0])) - else: - os.kill(pid, sig) + os.kill(pid, sig) # wait max 5 seconds for the process to die timeout = 50 while timeout >= 0: @@ -931,33 +908,17 @@ os.kill(pid, signal.SIGKILL) os.waitpid(pid, 0) self.fail('test process does not die on signal %i' % sig) - if command == '/usr/bin/crontab': - subprocess.Popen(['sudo', '-s', '/bin/bash', '-c', - "/usr/bin/pkill -9 -f crontab", - '-u', 'mail']) self.assertFalse(os.WIFEXITED(result), 'test process did not exit normally') self.assertTrue(os.WIFSIGNALED(result), 'test process died due to signal') self.assertEqual(os.WCOREDUMP(result), expect_coredump) self.assertEqual(os.WSTOPSIG(result), 0, 'test process was not signaled to stop') self.assertEqual(os.WTERMSIG(result), sig, 'test process died due to proper signal') - # wait max 10 seconds for apport to finish - timeout = 50 - while timeout >= 0: - pidof = subprocess.Popen(['pidof', '-x', 'apport'], stdout=subprocess.PIPE) - pidof.communicate() - if pidof.returncode != 0: - break - time.sleep(0.2) - timeout -= 1 - self.assertGreater(timeout, 0) + self.wait_for_apport_to_finish() if check_running: self.assertEqual(subprocess.call(['pidof', command]), 1, 'no running test executable processes') - if core_location: - core_path = '%s/core' % core_location - if expect_corefile: self.assertTrue(os.path.exists(core_path), 'leaves wanted core file') try: @@ -1010,6 +971,18 @@ self.assertTrue('\n#2' in r.get('Stacktrace', ''), r.get('Stacktrace', 'no Stacktrace field')) + def wait_for_apport_to_finish(self, timeout_sec=10.0): + self.wait_for_no_instance_running('apport', timeout_sec) + + def wait_for_no_instance_running(self, program, timeout_sec=10.0): + while timeout_sec > 0: + if not pidof(program): + break + time.sleep(0.2) + timeout_sec -= 0.2 + else: + self.fail(f"Timeout exceeded, but {program} is still running.") + # # main diff -Nru apport-2.20.11/test/test_ui_gtk.py apport-2.20.11/test/test_ui_gtk.py --- apport-2.20.11/test/test_ui_gtk.py 2019-12-04 20:25:28.000000000 +0000 +++ apport-2.20.11/test/test_ui_gtk.py 2022-09-15 12:43:39.000000000 +0000 @@ -571,22 +571,18 @@ def test_crash_nodetails(self, *args): '''Crash report without showing details''' - self.visible_progress = None - def cont(*args): if not self.app.w('continue_button').get_visible(): return True self.app.w('continue_button').clicked() - GLib.timeout_add(200, check_progress) - return False - - def check_progress(*args): - self.visible_progress = self.app.w( - 'window_information_collection').get_property('visible') return False GLib.timeout_add_seconds(60, cont) - self.app.run_crash(self.app.report_file) + info_collection_window = self.app.w("window_information_collection") + with patch.object( + info_collection_window, "show", wraps=info_collection_window.show + ) as info_collection_window_show_mock: + self.app.run_crash(self.app.report_file) # we should have reported one crash self.assertEqual(self.app.crashdb.latest_id(), 0) @@ -595,7 +591,7 @@ self.assertEqual(r['ExecutablePath'], '/bin/bash') # should show a progress bar for info collection - self.assertEqual(self.visible_progress, True) + info_collection_window_show_mock.assert_called_once_with() # data was collected self.assertTrue(r['Package'].startswith('bash ')) @@ -615,8 +611,6 @@ def test_crash_details(self, *args): '''Crash report with showing details''' - self.visible_progress = None - def show_details(*args): if not self.app.w('show_details').get_visible(): return True @@ -631,16 +625,14 @@ self.assertTrue(self.app.w('continue_button').get_visible()) self.app.w('continue_button').clicked() - GLib.timeout_add(200, check_progress) - return False - - def check_progress(*args): - self.visible_progress = self.app.w( - 'window_information_collection').get_property('visible') return False GLib.timeout_add(200, show_details) - self.app.run_crash(self.app.report_file) + info_collection_window = self.app.w("window_information_collection") + with patch.object( + info_collection_window, "show", wraps=info_collection_window.show + ) as info_collection_window_show_mock: + self.app.run_crash(self.app.report_file) # we should have reported one crash self.assertEqual(self.app.crashdb.latest_id(), 0) @@ -649,7 +641,7 @@ self.assertEqual(r['ExecutablePath'], '/bin/bash') # we already collected details, do not show the progress dialog again - self.assertNotEqual(self.visible_progress, True) + info_collection_window_show_mock.assert_not_called() # data was collected self.assertTrue(r['Package'].startswith('bash ')) @@ -727,30 +719,26 @@ def test_crash_noaccept(self, *args): '''Crash report with non-accepting crash DB''' - self.visible_progress = None - def cont(*args): if not self.app.w('continue_button').get_visible(): return True self.app.w('continue_button').clicked() - GLib.timeout_add(200, check_progress) - return False - - def check_progress(*args): - self.visible_progress = self.app.w( - 'window_information_collection').get_property('visible') return False GLib.timeout_add_seconds(60, cont) self.app.crashdb.options['problem_types'] = ['bug'] - self.app.run_crash(self.app.report_file) + info_collection_window = self.app.w("window_information_collection") + with patch.object( + info_collection_window, "show", wraps=info_collection_window.show + ) as info_collection_window_show_mock: + self.app.run_crash(self.app.report_file) # we should not have reported the crash self.assertEqual(self.app.crashdb.latest_id(), -1) self.assertEqual(self.app.open_url.call_count, 0) # no progress dialog for non-accepting DB - self.assertNotEqual(self.visible_progress, True) + info_collection_window_show_mock.assert_not_called() # data was collected for whoopsie r = self.app.report @@ -787,8 +775,9 @@ # data was collected self.assertTrue('linux' in r['Package']) - self.assertTrue('Dependencies' in r) self.assertTrue('Plasma conduit' in r['Title']) + if not r['Package'].endswith(' (not installed)'): + self.assertTrue('Dependencies' in r) # URL was opened self.assertEqual(self.app.open_url.call_count, 1) @@ -877,18 +866,13 @@ self.app.w('continue_button').clicked() return False - kernel_pkg = apport.packaging.get_kernel_package() - kernel_src = apport.packaging.get_source(kernel_pkg) - self.assertNotEqual(kernel_pkg, kernel_src, - 'this test assumes that the kernel binary package != kernel source package') - self.assertNotEqual(apport.packaging.get_version(kernel_pkg), '', - 'this test assumes that the kernel binary package %s is installed' % kernel_pkg) - # this test assumes that the kernel source package name is not an + # this test assumes that the source package name is not an # installed binary package - self.assertRaises(ValueError, apport.packaging.get_version, kernel_src) + source_pkg = "shadow" + self.assertRaises(ValueError, apport.packaging.get_version, source_pkg) # create source package hook, as otherwise there is nothing to collect - with open(os.path.join(self.hook_dir, 'source_%s.py' % kernel_src), 'w') as f: + with open(os.path.join(self.hook_dir, 'source_%s.py' % source_pkg), 'w') as f: f.write('def add_info(r, ui):\n r["MachineType"]="Laptop"\n') # upload empty report @@ -897,7 +881,7 @@ # run in update mode for that bug self.app.options.update_report = 0 - self.app.options.package = kernel_src + self.app.options.package = source_pkg GLib.timeout_add(200, cont) self.app.run_update_report() diff -Nru apport-2.20.11/test/test_ui_kde.py apport-2.20.11/test/test_ui_kde.py --- apport-2.20.11/test/test_ui_kde.py 2019-12-04 20:25:28.000000000 +0000 +++ apport-2.20.11/test/test_ui_kde.py 2022-09-15 12:43:39.000000000 +0000 @@ -584,18 +584,13 @@ # try again QTimer.singleShot(200, cont) - kernel_pkg = apport.packaging.get_kernel_package() - kernel_src = apport.packaging.get_source(kernel_pkg) - self.assertNotEqual(kernel_pkg, kernel_src, - 'this test assumes that the kernel binary package != kernel source package') - self.assertNotEqual(apport.packaging.get_version(kernel_pkg), '', - 'this test assumes that the kernel binary package %s is installed' % kernel_pkg) - # this test assumes that the kernel source package name is not an + # this test assumes that the source package name is not an # installed binary package - self.assertRaises(ValueError, apport.packaging.get_version, kernel_src) + source_pkg = "shadow" + self.assertRaises(ValueError, apport.packaging.get_version, source_pkg) # create source package hook, as otherwise there is nothing to collect - with open(os.path.join(self.hook_dir, 'source_%s.py' % kernel_src), 'w') as f: + with open(os.path.join(self.hook_dir, 'source_%s.py' % source_pkg), 'w') as f: f.write('def add_info(r, ui):\n r["MachineType"]="Laptop"\n') # upload empty report @@ -604,7 +599,7 @@ # run in update mode for that bug self.app.options.update_report = 0 - self.app.options.package = kernel_src + self.app.options.package = source_pkg QTimer.singleShot(200, cont) self.app.run_update_report() diff -Nru apport-2.20.11/test/test_ui.py apport-2.20.11/test/test_ui.py --- apport-2.20.11/test/test_ui.py 2021-10-18 11:48:31.000000000 +0000 +++ apport-2.20.11/test/test_ui.py 2022-09-15 12:43:39.000000000 +0000 @@ -9,6 +9,7 @@ from io import StringIO from io import BytesIO from importlib.machinery import SourceFileLoader +from unittest.mock import patch import apport.ui from apport.ui import _ @@ -677,10 +678,13 @@ self.assertEqual(self.ui.msg_severity, 'error') - def test_run_report_bug_kernel_thread(self): + @patch("apport.packaging.get_version") + def test_run_report_bug_kernel_thread(self, get_version_mock): '''run_report_bug() for a pid of a kernel thread''' + # The kernel package might not be installed in chroot environments. + # Therefore mock get_version for the kernel package. + get_version_mock.return_value = '5.15.0-33.34' - pid = None for path in glob.glob('/proc/[0-9]*/stat'): with open(path) as f: stat = f.read().split() @@ -688,8 +692,9 @@ if flags & apport.ui.PF_KTHREAD: pid = int(stat[0]) break + else: + self.skipTest("no kernel thread found") - self.assertFalse(pid is None) sys.argv = ['ui-test', '-f', '-P', str(pid)] self.ui = TestSuiteUserInterface() self.ui.present_details_response = {'report': True, @@ -699,7 +704,12 @@ 'remember': False} self.ui.run_argv() - self.assertTrue(self.ui.report['Package'].startswith(apport.packaging.get_kernel_package())) + kernel_package = apport.packaging.get_kernel_package() + self.assertEqual( + self.ui.report['Package'], + f"{kernel_package} {get_version_mock.return_value}", + ) + get_version_mock.assert_any_call(kernel_package) def test_run_report_bug_file(self): '''run_report_bug() with saving report into a file''' @@ -1295,7 +1305,7 @@ '''run_crash() for a kernel error''' sys_arch = impl.get_system_architecture() - if sys_arch in ['amd64', 'ppc64el']: + if sys_arch in ['amd64', 'arm64', 'ppc64el', 's390x']: src_pkg = 'linux-signed' else: src_pkg = 'linux' @@ -1739,19 +1749,12 @@ def test_run_update_report_different_binary_source(self): '''run_update_report() on a source package which does not have a binary of the same name''' - - kernel_pkg = apport.packaging.get_kernel_package() - kernel_src = apport.packaging.get_source(kernel_pkg) - self.assertNotEqual(kernel_pkg, kernel_src, - 'this test assumes that the kernel binary package != kernel source package') - self.assertNotEqual(apport.packaging.get_version(kernel_pkg), '', - 'this test assumes that the kernel binary package %s is installed' % kernel_pkg) - - # this test assumes that the kernel source package name is not an + # this test assumes that the source package name is not an # installed binary package - self.assertRaises(ValueError, apport.packaging.get_version, kernel_src) + source_pkg = "shadow" + self.assertRaises(ValueError, apport.packaging.get_version, source_pkg) - sys.argv = ['ui-test', '-p', kernel_src, '-u', '1'] + sys.argv = ['ui-test', '-p', source_pkg, '-u', '1'] self.ui = TestSuiteUserInterface() self.ui.present_details_response = {'report': True, 'blacklist': False, @@ -1759,7 +1762,7 @@ 'restart': False, 'remember': False} - with open(os.path.join(self.hookdir, 'source_%s.py' % kernel_src), 'w') as f: + with open(os.path.join(self.hookdir, 'source_%s.py' % source_pkg), 'w') as f: f.write('def add_info(r, ui):\n r["MachineType"]="Laptop"\n') self.assertEqual(self.ui.run_argv(), True, self.ui.report) @@ -1769,7 +1772,7 @@ self.assertTrue(self.ui.present_details_shown) self.assertTrue(self.ui.ic_progress_pulses > 0) - self.assertEqual(self.ui.report['Package'], '%s (not installed)' % kernel_src) + self.assertEqual(self.ui.report['Package'], '%s (not installed)' % source_pkg) self.assertEqual(self.ui.report['MachineType'], 'Laptop') self.assertTrue('ProcEnviron' in self.ui.report.keys())