diff -Nru apport-2.20.11/apport/report.py apport-2.20.11/apport/report.py --- apport-2.20.11/apport/report.py 2019-11-05 02:49:27.000000000 +0000 +++ apport-2.20.11/apport/report.py 2020-02-27 03:18:45.000000000 +0000 @@ -9,7 +9,7 @@ # 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 +import subprocess, tempfile, os.path, re, pwd, grp, os, time, io import fnmatch, glob, traceback, errno, sys, atexit, locale, imp import xml.dom, xml.dom.minidom @@ -64,13 +64,29 @@ _transitive_dependencies(d, depends_set) -def _read_file(path, dir_fd=None): +def _read_proc_link(path, pid=None, dir_fd=None): + '''Use readlink() to resolve link. + + Return a string representing the path to which the symbolic link points. + ''' + if not _python2 and dir_fd is not None: + return os.readlink(path, dir_fd=dir_fd) + + return os.readlink("/proc/%s/%s" % (pid, path)) + + +def _read_proc_file(path, pid=None, dir_fd=None): '''Read file content. Return its content, or return a textual error if it failed. ''' try: - with open(path, 'rb', opener=lambda path, mode: os.open(path, mode, dir_fd=dir_fd)) as fd: + if not _python2 and dir_fd is not None: + proc_file = os.open(path, os.O_RDONLY | os.O_CLOEXEC, dir_fd=dir_fd) + else: + proc_file = "/proc/%s/%s" % (pid, path) + + with io.open(proc_file, 'rb') as fd: return fd.read().strip().decode('UTF-8', errors='replace') except (OSError, IOError) as e: return 'Error: ' + str(e) @@ -83,6 +99,10 @@ process, detect this, and attempt to attach/detach. ''' maps = 'Error: unable to read /proc maps file' + + if _python2: + return 'Error: python2 does not provide a secure way to read /proc maps file' + try: with open('maps', opener=lambda path, mode: os.open(path, mode, dir_fd=proc_pid_fd)) as fd: maps = fd.read().strip() @@ -541,25 +561,35 @@ if not self.pid: self.pid = int(pid) pid = str(pid) - proc_pid_fd = os.open('/proc/%s' % pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + if not _python2: + try: + proc_pid_fd = os.open('/proc/%s' % pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + except OSError as e: + if e.errno in (errno.EPERM, errno.EACCES): + raise ValueError('not accessible') + if e.errno == errno.ENOENT: + raise ValueError('invalid process') + else: + raise try: - self['ProcCwd'] = os.readlink('cwd', dir_fd=proc_pid_fd) + self['ProcCwd'] = _read_proc_link('cwd', pid, proc_pid_fd) except OSError: pass - self.add_proc_environ(proc_pid_fd=proc_pid_fd, extraenv=extraenv) - self['ProcStatus'] = _read_file('status', dir_fd=proc_pid_fd) - self['ProcCmdline'] = _read_file('cmdline', dir_fd=proc_pid_fd).rstrip('\0') + self.add_proc_environ(pid=pid, proc_pid_fd=proc_pid_fd, extraenv=extraenv) + self['ProcStatus'] = _read_proc_file('status', pid, proc_pid_fd) + self['ProcCmdline'] = _read_proc_file('cmdline', pid, proc_pid_fd).rstrip('\0') self['ProcMaps'] = _read_maps(proc_pid_fd) - try: - self['ExecutablePath'] = os.readlink('exe', dir_fd=proc_pid_fd) - except (PermissionError, OSError, FileNotFoundError) as e: - if e.errno in (errno.EPERM, errno.EACCES): - raise ValueError('not accessible') - if e.errno == errno.ENOENT: - raise ValueError('invalid process') - else: - raise + if 'ExecutablePath' not in self: + try: + self['ExecutablePath'] = _read_proc_link('exe', pid, proc_pid_fd) + except (PermissionError, OSError, FileNotFoundError) as e: + if e.errno in (errno.EPERM, errno.EACCES): + raise ValueError('not accessible') + if e.errno == errno.ENOENT: + raise ValueError('invalid process') + else: + raise for p in ('rofs', 'rwfs', 'squashmnt', 'persistmnt'): if self['ExecutablePath'].startswith('/%s/' % p): self['ExecutablePath'] = self['ExecutablePath'][len('/%s' % p):] @@ -581,14 +611,13 @@ # On Linux 2.6.28+, 'current' is world readable, but read() gives # EPERM; Python 2.5.3+ crashes on that (LP: #314065) if os.getuid() == 0: - with open('attr/current', opener=lambda path, mode: os.open(path, mode, dir_fd=proc_pid_fd)) as fd: - val = fd.read().strip() + val = _read_proc_file('attr/current', pid, proc_pid_fd) if val != 'unconfined': self['ProcAttrCurrent'] = val except (IOError, OSError): pass - ret = self.get_logind_session(proc_pid_fd) + ret = self.get_logind_session(pid, proc_pid_fd) if ret: self['_LogindSession'] = ret[0] @@ -613,10 +642,11 @@ if not pid: pid = os.getpid() pid = str(pid) - proc_pid_fd = os.open('/proc/%s' % pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + if not _python2: + proc_pid_fd = os.open('/proc/%s' % pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) self['ProcEnviron'] = '' - env = _read_file('environ', dir_fd=proc_pid_fd).replace('\n', '\\n') + env = _read_proc_file('environ', pid, proc_pid_fd).replace('\n', '\\n') if env.startswith('Error:'): self['ProcEnviron'] = env else: @@ -1686,15 +1716,20 @@ int(m.group(2), 16), m.group(3))) @classmethod - def get_logind_session(klass, proc_pid_fd): + def get_logind_session(klass, pid=None, proc_pid_fd=None): '''Get logind session path and start time. Return (session_id, session_start_timestamp) if process is in a logind session, or None otherwise. ''' + if not _python2 and proc_pid_fd is not None: + cgroup_file = os.open('cgroup', os.O_RDONLY | os.O_CLOEXEC, dir_fd=proc_pid_fd) + else: + cgroup_file = "/proc/%s/cgroup" % pid + # determine cgroup try: - with open('cgroup', opener=lambda path, mode: os.open(path, mode, dir_fd=proc_pid_fd)) as f: + with io.open(cgroup_file) as f: for line in f: line = line.strip() if 'name=systemd:' in line and line.endswith('.scope') and '/session-' in line: diff -Nru apport-2.20.11/apport/ui.py apport-2.20.11/apport/ui.py --- apport-2.20.11/apport/ui.py 2019-10-29 05:23:08.000000000 +0000 +++ apport-2.20.11/apport/ui.py 2020-02-27 03:18:45.000000000 +0000 @@ -13,7 +13,7 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import glob, sys, os.path, optparse, traceback, locale, gettext, re +import glob, sys, os.path, optparse, traceback, locale, gettext, re, io import errno, zlib, gzip import subprocess, threading, webbrowser import signal @@ -243,8 +243,11 @@ logind_session = None else: reports = apport.fileutils.get_new_reports() - proc_pid_fd = os.open('/proc/%s' % os.getpid(), os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) - logind_session = apport.Report.get_logind_session(proc_pid_fd) + if PY3: + proc_pid_fd = os.open('/proc/%s' % os.getpid(), os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + logind_session = apport.Report.get_logind_session(proc_pid_fd=proc_pid_fd) + else: + logind_session = apport.Report.get_logind_session(os.getpid()) for f in reports: if not self.load_report(f): @@ -464,15 +467,20 @@ # if PID is given, add info if self.options.pid: try: - proc_pid_fd = os.open('/proc/%s' % self.options.pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) - with open('stat', opener=lambda path, mode: os.open(path, mode, dir_fd=proc_pid_fd)) as f: + proc_pid_fd = None + if PY3: + proc_pid_fd = os.open('/proc/%s' % self.options.pid, os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + stat_file = os.open('stat', os.O_RDONLY | os.O_CLOEXEC, dir_fd=proc_pid_fd) + else: + stat_file = '/proc/%s/stat' % self.options.pid + with io.open(stat_file) as f: stat = f.read().split() flags = int(stat[8]) if flags & PF_KTHREAD: # this PID is a kernel thread self.options.package = 'linux' else: - self.report.add_proc_info(proc_pid_fd=proc_pid_fd) + self.report.add_proc_info(pid=self.options.pid, proc_pid_fd=proc_pid_fd) except (ValueError, IOError, OSError) as e: if hasattr(e, 'errno'): # silently ignore nonexisting PIDs; the user must not close the diff -Nru apport-2.20.11/data/apport apport-2.20.11/data/apport --- apport-2.20.11/data/apport 2019-10-29 05:23:08.000000000 +0000 +++ apport-2.20.11/data/apport 2020-02-27 03:18:45.000000000 +0000 @@ -211,7 +211,7 @@ if limit > 0 and core_size > limit: error_log('aborting core dump writing, size %i exceeds current limit' % core_size) os.close(core_file) - os.unlink(core_path) + os.unlink(core_path, dir_fd=cwd) return error_log('writing core dump %s of size %i' % (core_path, core_size)) os.write(core_file, r['CoreDump']) @@ -227,17 +227,16 @@ if limit > 0 and written > limit: error_log('aborting core dump writing, size exceeds current limit %i' % limit) os.close(core_file) - os.unlink(core_path) + os.unlink(core_path, dir_fd=cwd) 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) + os.unlink(core_path, dir_fd=cwd) return block = os.read(0, 1048576) os.close(core_file) - return core_path def usable_ram(): @@ -340,7 +339,7 @@ def parse_arguments(): parser = argparse.ArgumentParser(epilog=""" Alternatively, the following command line is understood for legacy hosts: - [global pid] + [global pid] [exe path] """) # TODO: Use type=int @@ -349,6 +348,7 @@ 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)") options, rest = parser.parse_known_args() @@ -356,7 +356,7 @@ for arg in rest: error_log("Unknown argument: %s", arg) - elif len(rest) in (4, 5): + elif len(rest) in (4, 5, 6): # Translate legacy command line options.pid = rest[0] options.signal_number = rest[1] @@ -366,6 +366,10 @@ options.global_pid = rest[4] except IndexError: options.global_pid = None + try: + options.exe_path = rest[5].replace('!', '/') + except IndexError: + options.exe_path = None else: parser.print_usage() sys.exit(1) @@ -512,7 +516,7 @@ sys.exit(0) # Send all arguments except for the first (exec path) and last (global pid) - args = ' '.join(sys.argv[1:-1]) + args = ' '.join(sys.argv[1:5]) try: sock.sendmsg([args.encode()], [ # Send a ucred containing the global pid @@ -599,6 +603,9 @@ # 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 + euid = os.geteuid() egid = os.getegid() try: 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 2019-05-16 19:18:33.000000000 +0000 +++ apport-2.20.11/data/whoopsie-upload-all 2020-02-24 17:38:30.000000000 +0000 @@ -89,8 +89,10 @@ os.unlink(report) return None - # write updated report - with open(report, 'ab') as f: + # write updated report, we use os.open and os.fdopen as + # /proc/sys/fs/protected_regular is set to 1 (LP: #1848064) + fd = os.open(report, os.O_WRONLY | os.O_APPEND) + with os.fdopen(fd, 'wb') as f: os.chmod(report, 0) r.write(f, only_new=True) os.chmod(report, 0o640) diff -Nru apport-2.20.11/debian/apport.init apport-2.20.11/debian/apport.init --- apport-2.20.11/debian/apport.init 2019-05-16 19:18:33.000000000 +0000 +++ apport-2.20.11/debian/apport.init 2020-02-24 17:38:55.000000000 +0000 @@ -52,9 +52,9 @@ # Old compatibility mode, switch later to second one if true; then - echo "|$AGENT %p %s %c %d %P" > /proc/sys/kernel/core_pattern + 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" > /proc/sys/kernel/core_pattern + echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -E%E" > /proc/sys/kernel/core_pattern fi echo 2 > /proc/sys/fs/suid_dumpable } diff -Nru apport-2.20.11/debian/apport.links apport-2.20.11/debian/apport.links --- apport-2.20.11/debian/apport.links 2019-05-16 19:18:33.000000000 +0000 +++ apport-2.20.11/debian/apport.links 2020-02-11 00:43:52.000000000 +0000 @@ -1,4 +1,10 @@ +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-meta-oem-osp1.py +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-meta-oem.py /usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-meta.py +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-oem-osp1.py +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-oem.py +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-signed-oem-osp1.py +/usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-signed-oem.py /usr/share/apport/package-hooks/source_linux.py /usr/share/apport/package-hooks/source_linux-signed.py /usr/bin/apport-bug /usr/bin/ubuntu-bug /usr/share/man/man1/apport-bug.1.gz /usr/share/man/man1/ubuntu-bug.1.gz diff -Nru apport-2.20.11/debian/changelog apport-2.20.11/debian/changelog --- apport-2.20.11/debian/changelog 2019-11-05 02:49:27.000000000 +0000 +++ apport-2.20.11/debian/changelog 2020-02-27 03:18:45.000000000 +0000 @@ -1,3 +1,49 @@ +apport (2.20.11-0ubuntu8.6) eoan-security; urgency=medium + + * SECURITY REGRESSION: 'module' object has no attribute 'O_PATH' + (LP: #1851806) + - apport/report.py, apport/ui.py: use file descriptors for /proc/pid + directory access only when running under python 3; prevent reading /proc + maps under python 2 as it does not provide a secure way to do so; use + io.open for better compatibility between python 2 and 3. + * data/apport: fix number of arguments passed through socks into a container. + * test/test_report.py: test login session with both pid and proc_pid_fd. + + -- Tiago Stürmer Daitx Thu, 27 Feb 2020 03:18:45 +0000 + +apport (2.20.11-0ubuntu8.5) eoan; urgency=medium + + * data/whoopsie-upload-all: append to the crash report using fdopen and open + from os to cope with protected_regular being set to 1. (LP: #1848064) + + [ Michael Hudson-Doyle ] + * Fix autopkgtest failures since recent security update: (LP: #1854237) + - Fix regression in creating report for crashing setuid process by getting + kernel to tell us the executable path rather than reading + /proc/[pid]/exe. + - Fix deletion of partially written core files. + - Fix test_get_logind_session to use new API. + - Restore add_proc_info raising ValueError for a dead process. + - Delete test_lock_symlink, no longer applicable now that the lock is + created in a directory only root can write to. + + -- Brian Murray Mon, 24 Feb 2020 09:38:55 -0800 + +apport (2.20.11-0ubuntu8.4) eoan; urgency=medium + + * Create additional symlinks to the source_linux.py apport package hook for + many OEM kernels. Thanks to You-Sheng Yang for the patch. (LP: #1847967) + + -- Brian Murray Mon, 10 Feb 2020 16:44:01 -0800 + +apport (2.20.11-0ubuntu8.3) eoan; urgency=medium + + * Use an SRU-safe substring when checking for the available version of + aspell-doc in xenial, since aspell *did* have an SRU. Backported + from apport 2.20.11-0ubuntu9. (LP: #1851542) + + -- dann frazier Wed, 13 Nov 2019 14:12:04 -0800 + apport (2.20.11-0ubuntu8.2) eoan-security; urgency=medium * SECURITY REGRESSION: missing argument in Report.add_proc_environ 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-05-16 19:18:33.000000000 +0000 +++ apport-2.20.11/etc/init.d/apport 2020-02-24 17:38:55.000000000 +0000 @@ -52,9 +52,9 @@ # Old compatibility mode, switch later to second one if true; then - echo "|$AGENT %p %s %c %d %P" > /proc/sys/kernel/core_pattern + 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" > /proc/sys/kernel/core_pattern + echo "|$AGENT -p%p -s%s -c%c -d%d -P%P -E%E" > /proc/sys/kernel/core_pattern fi echo 2 > /proc/sys/fs/suid_dumpable } 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 2019-07-09 18:14:46.000000000 +0000 +++ apport-2.20.11/test/test_backend_apt_dpkg.py 2019-11-11 21:50:38.000000000 +0000 @@ -554,7 +554,7 @@ # complains about obsolete packages result = impl.install_packages(self.rootdir, self.configdir, 'Foonux 16.04', [('aspell-doc', '1.1')]) - self.assertIn(result, 'aspell-doc version 1.1 required, but 0.60.7~20110707-3build1 is available\n') + self.assertIn('aspell-doc version 1.1 required, but 0.60.7~20110707-3', result) # ... but installs the current version anyway self.assertTrue(os.path.exists( os.path.join(self.rootdir, 'usr/share/info/aspell.info.gz'))) 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 2019-05-16 21:28:22.000000000 +0000 +++ apport-2.20.11/test/test_report.py 2020-02-27 03:18:45.000000000 +0000 @@ -2287,7 +2287,28 @@ (session, timestamp) = ret self.assertNotEqual(session, '') - # session start must be >= 2014-01-01 and "now" + # session start must be >= 2014-01-01 and <= "now" + self.assertLess(timestamp, time.time()) + self.assertGreater(timestamp, + time.mktime(time.strptime('2014-01-01', '%Y-%m-%d'))) + + def test_get_logind_session_fd(self): + proc_pid_fd = os.open('/proc/%s' % os.getpid(), os.O_RDONLY | os.O_PATH | os.O_DIRECTORY) + self.addCleanup(os.close, proc_pid_fd) + ret = apport.Report.get_logind_session(proc_pid_fd=proc_pid_fd) + if ret is None: + # ensure that we don't run under logind, and thus the None is + # justified + with open('/proc/self/cgroup') as f: + contents = f.read() + sys.stdout.write('[not running under logind] ') + sys.stdout.flush() + self.assertNotIn('name=systemd:/user', contents) + return + + (session, timestamp) = ret + self.assertNotEqual(session, '') + # session start must be >= 2014-01-01 and <= "now" self.assertLess(timestamp, time.time()) self.assertGreater(timestamp, time.mktime(time.strptime('2014-01-01', '%Y-%m-%d'))) 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 2019-05-16 19:18:33.000000000 +0000 +++ apport-2.20.11/test/test_signal_crashes.py 2020-02-24 17:38:55.000000000 +0000 @@ -192,34 +192,6 @@ os.kill(test_proc2, 9) os.waitpid(test_proc2, 0) - def test_lock_symlink(self): - '''existing .lock file as dangling symlink does not create the file - - This would be a vulnerability, as users could overwrite system files. - ''' - # prepare a symlink trap - lockpath = os.path.join(self.report_dir, '.lock') - trappath = os.path.join(self.report_dir, '0wned') - os.symlink(trappath, lockpath) - - # now call apport - test_proc = self.create_test_process() - - try: - app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'], - stdin=subprocess.PIPE, stderr=subprocess.PIPE) - app.stdin.write(b'boo') - app.stdin.close() - - self.assertNotEqual(app.wait(), 0, app.stderr.read()) - app.stderr.close() - finally: - os.kill(test_proc, 9) - os.waitpid(test_proc, 0) - - self.assertEqual(self.get_temp_all_reports(), []) - self.assertFalse(os.path.exists(trappath)) - def test_unpackaged_binary(self): '''unpackaged binaries do not create a report'''