~brian-murray/apport/overlay-ppa

« back to all changes in this revision

Viewing changes to test/test_signal_crashes.py

  • Committer: Brian Murray
  • Date: 2018-01-05 18:43:19 UTC
  • mfrom: (2972.1.207 trunk)
  • Revision ID: brian@canonical.com-20180105184319-fq8hn2hewt05zibd
Tags: no-stacktracsource, speedier
merge with upstream r3179

Show diffs side-by-side

added added

removed removed

Lines of Context:
8
8
# the full text of the license.
9
9
 
10
10
import tempfile, shutil, os, subprocess, signal, time, stat, sys
11
 
import resource, errno, grp, unittest
 
11
import resource, errno, grp, unittest, socket, array, pwd
12
12
import apport.fileutils
13
13
 
14
14
test_executable = '/usr/bin/yes'
89
89
 
90
90
        test_proc = self.create_test_process()
91
91
        try:
92
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
92
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
93
93
                                   stdin=subprocess.PIPE, stderr=subprocess.PIPE)
94
94
            app.stdin.close()
95
95
            assert app.wait() == 0, app.stderr.read()
156
156
        test_proc = self.create_test_process()
157
157
        test_proc2 = self.create_test_process(False, '/bin/dd')
158
158
        try:
159
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
159
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
160
160
                                   stdin=subprocess.PIPE, stderr=subprocess.PIPE)
161
161
 
162
162
            time.sleep(0.5)  # give it some time to grab the lock
163
163
 
164
 
            app2 = subprocess.Popen([apport_path, str(test_proc2), '42', '0'],
 
164
            app2 = subprocess.Popen([apport_path, str(test_proc2), '42', '0', '1'],
165
165
                                    stdin=subprocess.PIPE, stderr=subprocess.PIPE)
166
166
 
167
167
            # app should wait indefinitely for stdin, while app2 should terminate
205
205
        test_proc = self.create_test_process()
206
206
 
207
207
        try:
208
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
208
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
209
209
                                   stdin=subprocess.PIPE, stderr=subprocess.PIPE)
210
210
            app.stdin.write(b'boo')
211
211
            app.stdin.close()
446
446
 
447
447
        test_proc = self.create_test_process()
448
448
        try:
449
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
449
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
450
450
                                   stdin=subprocess.PIPE, stderr=subprocess.PIPE)
451
451
            # pipe an entire total memory size worth of spaces into it, which must be
452
452
            # bigger than the 'usable' memory size. apport should digest that and the
528
528
            time.sleep(1.1)
529
529
            os.utime(myexe, None)
530
530
 
531
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
531
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
532
532
                                   stdin=subprocess.PIPE, stderr=subprocess.PIPE)
533
 
            app.stdin.write(b'boo')
534
 
            app.stdin.close()
535
 
            err = app.stderr.read().decode()
536
 
            self.assertNotEqual(app.wait(), 0, err)
537
 
            app.stderr.close()
 
533
            err = app.communicate(b'foo')[1]
 
534
            self.assertEqual(app.returncode, 0, err)
 
535
            if os.getuid() > 0:
 
536
                self.assertIn(b'executable was modified after program start', err)
 
537
            else:
 
538
                with open('/var/log/apport.log') as f:
 
539
                    lines = f.readlines()
 
540
                self.assertIn('executable was modified after program start', lines[-1])
538
541
        finally:
539
542
            os.kill(test_proc, 9)
540
543
            os.waitpid(test_proc, 0)
549
552
        try:
550
553
            env = os.environ.copy()
551
554
            env['APPORT_LOG_FILE'] = log
552
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
555
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
553
556
                                   stdin=subprocess.PIPE, env=env,
554
557
                                   stdout=subprocess.PIPE,
555
558
                                   stderr=subprocess.PIPE)
586
589
        try:
587
590
            env = os.environ.copy()
588
591
            env['APPORT_LOG_FILE'] = '/not/existing/apport.log'
589
 
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0'],
 
592
            app = subprocess.Popen([apport_path, str(test_proc), '42', '0', '1'],
590
593
                                   stdin=subprocess.PIPE, env=env,
591
594
                                   stdout=subprocess.PIPE,
592
595
                                   stderr=subprocess.PIPE)
675
678
                          uid=8)
676
679
            self.assertEqual(apport.fileutils.get_all_reports(), [])
677
680
 
 
681
    @unittest.skipUnless(os.path.exists('/bin/ping'), 'this test needs /bin/ping')
 
682
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
 
683
    def test_crash_setuid_drop_and_kill(self):
 
684
        '''process started by root as another user, killed by that user no core'''
 
685
        # override expected report file name
 
686
        self.test_report = os.path.join(
 
687
            apport.fileutils.report_dir, '%s.%i.crash' %
 
688
            ('_usr_bin_crontab', os.getuid()))
 
689
        # edit crontab as user "mail"
 
690
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
 
691
 
 
692
        if suid_dumpable:
 
693
            user = pwd.getpwuid(8)
 
694
            # if a user can crash a suid root binary, it should not create core files
 
695
            orig_editor = os.getenv('EDITOR')
 
696
            os.environ['EDITOR'] = '/usr/bin/yes'
 
697
            self.do_crash(command='/usr/bin/crontab', args=['-e', '-u', user[0]],
 
698
                          expect_corefile=False, core_location='/var/spool/cron/',
 
699
                          killer_id=8)
 
700
            if orig_editor is not None:
 
701
                os.environ['EDITOR'] = orig_editor
 
702
 
 
703
            # check crash report
 
704
            reports = apport.fileutils.get_all_reports()
 
705
            self.assertEqual(len(reports), 1)
 
706
            report = reports[0]
 
707
            st = os.stat(report)
 
708
            os.unlink(report)
 
709
            self.assertEqual(stat.S_IMODE(st.st_mode), 0o640, 'report has correct permissions')
 
710
            # this must be owned by root as it is a setuid binary
 
711
            self.assertEqual(st.st_uid, 0, 'report has correct owner')
 
712
        else:
 
713
            # no cores/dump if suid_dumpable == 0
 
714
            self.do_crash(False, command='/bin/ping', args=['127.0.0.1'],
 
715
                          uid=8)
 
716
            self.assertEqual(apport.fileutils.get_all_reports(), [])
 
717
 
678
718
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
679
719
    def test_crash_setuid_unpackaged(self):
680
720
        '''report generation for unpackaged setuid program'''
737
777
            self.do_crash(False, command=myexe, expect_corefile=False, uid=8)
738
778
            self.assertEqual(apport.fileutils.get_all_reports(), [])
739
779
 
740
 
    @unittest.skipUnless(os.path.exists('/usr/bin/lxc-usernsexec'), 'this test needs lxc')
741
 
    @unittest.skipUnless(os.path.exists('/bin/busybox'), 'this test needs busybox')
742
 
    @unittest.skipIf(os.access('/etc/shadow', os.R_OK), 'this test needs to be run as user')
743
 
    def test_ns_forward_privilege(self):
744
 
        c = os.path.join(self.workdir, 'c')
745
 
        os.makedirs(os.path.join(c, 'dev'))
746
 
        os.mkdir(os.path.join(c, 'mnt'))
747
 
        os.makedirs(os.path.join(c, 'usr/share/apport'))
748
 
        shutil.copy('/bin/busybox', c)
749
 
        with open(os.path.join(c, 'usr/share/apport/apport'), 'w') as f:
750
 
            f.write('''#!/busybox sh
751
 
set -x
752
 
exec 2>/apport.trace
753
 
cat /mnt/1/root/etc/shadow > /mnt/1/root/tmp/pwned
754
 
chmod 644 /mnt/1/root/tmp/pwned
755
 
''')
756
 
            os.fchmod(f.fileno(), 0o755)
757
 
 
758
 
        ns_apport = subprocess.Popen(
759
 
            ['lxc-usernsexec', '-m', 'u:0:%i:1' % os.getuid(),
760
 
             '-m', 'g:0:%i:1' % os.getgid(), '--',
761
 
             'lxc-unshare', '-s', 'MOUNT|PID|NETWORK|UTSNAME|IPC', '--', '/bin/sh'],
762
 
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
763
 
            stderr=subprocess.STDOUT)
764
 
        ns_apport.stdin.write(('''set -x
765
 
cd %s
766
 
mount -o bind . .
767
 
cd .
768
 
mount --rbind /proc mnt
769
 
touch dev/null
770
 
pivot_root . .
771
 
./busybox ls -lh /
772
 
./busybox sh -c 'kill -SEGV $$'
773
 
./busybox sleep 5
774
 
if [ -e /apport.trace ]; then
775
 
    echo "=== apport trace ===="
776
 
    ./busybox cat /apport.trace
777
 
fi
778
 
''' % c).encode())
779
 
        out = ns_apport.communicate()[0].decode()
780
 
        self.assertEqual(ns_apport.returncode, 0, out)
781
 
        self.assertFalse(os.path.exists('/tmp/pwned'), out)
 
780
    def test_coredump_from_socket(self):
 
781
        '''forwarding of a core dump through socket
 
782
 
 
783
        This is being used in a container via systemd activation, where the
 
784
        core dump gets read from /run/apport.socket.
 
785
        '''
 
786
        socket_path = os.path.join(self.workdir, 'apport.socket')
 
787
        test_proc = self.create_test_process()
 
788
        try:
 
789
            # emulate apport on the host which forwards the crash to the apport
 
790
            # socket in the container
 
791
            server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 
792
            server.bind(socket_path)
 
793
            server.listen(1)
 
794
 
 
795
            if os.fork() == 0:
 
796
                client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 
797
                client.connect(socket_path)
 
798
                with tempfile.TemporaryFile() as fd:
 
799
                    fd.write(b'hel\x01lo')
 
800
                    fd.flush()
 
801
                    fd.seek(0)
 
802
                    args = '%s 11 0 1' % test_proc
 
803
                    fd_msg = (socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array('i', [fd.fileno()]))
 
804
                    client.sendmsg([args.encode()], [fd_msg])
 
805
                os._exit(0)
 
806
 
 
807
            # call apport like systemd does via socket activation
 
808
            def child_setup():
 
809
                os.environ['LISTEN_FDNAMES'] = 'connection'
 
810
                os.environ['LISTEN_FDS'] = '1'
 
811
                os.environ['LISTEN_PID'] = str(os.getpid())
 
812
                # socket from server becomes fd 3 (SD_LISTEN_FDS_START)
 
813
                conn = server.accept()[0]
 
814
                os.dup2(conn.fileno(), 3)
 
815
 
 
816
            app = subprocess.Popen([apport_path], preexec_fn=child_setup,
 
817
                                   pass_fds=[3], stderr=subprocess.PIPE)
 
818
            log = app.communicate()[1]
 
819
            self.assertEqual(app.returncode, 0, log)
 
820
            server.close()
 
821
        finally:
 
822
            os.kill(test_proc, 9)
 
823
            os.waitpid(test_proc, 0)
 
824
 
 
825
        reports = self.get_temp_all_reports()
 
826
        self.assertEqual(len(reports), 1)
 
827
        pr = apport.Report()
 
828
        with open(reports[0], 'rb') as f:
 
829
            pr.load(f)
 
830
        os.unlink(reports[0])
 
831
        self.assertEqual(pr['Signal'], '11')
 
832
        self.assertEqual(pr['ExecutablePath'], test_executable)
 
833
        self.assertEqual(pr['CoreDump'], b'hel\x01lo')
 
834
 
 
835
        # should not create report on the host
 
836
        self.assertEqual(apport.fileutils.get_all_system_reports(), [])
782
837
 
783
838
    #
784
839
    # Helper methods
821
876
    def do_crash(self, expect_coredump=True, expect_corefile=False,
822
877
                 sig=signal.SIGSEGV, check_running=True, sleep=0,
823
878
                 command=test_executable, uid=None,
824
 
                 expect_corefile_owner=None, args=[]):
 
879
                 expect_corefile_owner=None,
 
880
                 core_location=None,
 
881
                 killer_id=False, args=[]):
825
882
        '''Generate a test crash.
826
883
 
827
884
        This runs command (by default test_executable) in cwd, lets it crash,
836
893
        pid = self.create_test_process(check_running, command, uid=uid, args=args)
837
894
        if sleep > 0:
838
895
            time.sleep(sleep)
839
 
        os.kill(pid, sig)
 
896
        if killer_id:
 
897
            user = pwd.getpwuid(killer_id)
 
898
            # testing different editors via VISUAL= didn't help
 
899
            kill = subprocess.Popen(['sudo', '-s', '/bin/bash', '-c',
 
900
                                     "/bin/kill -s %i %s" % (sig, pid),
 
901
                                     '-u', user[0]])  # 'mail'])
 
902
            kill.communicate()
 
903
            # need to clean up system state
 
904
            if command == '/usr/bin/crontab':
 
905
                os.system('stty sane')
 
906
            if kill.returncode != 0:
 
907
                self.fail("Couldn't kill process %s as user %s." %
 
908
                          (pid, user[0]))
 
909
        else:
 
910
            os.kill(pid, sig)
840
911
        # wait max 5 seconds for the process to die
841
912
        timeout = 50
842
913
        while timeout >= 0:
849
920
            os.kill(pid, signal.SIGKILL)
850
921
            os.waitpid(pid, 0)
851
922
            self.fail('test process does not die on signal %i' % sig)
852
 
 
 
923
        if command == '/usr/bin/crontab':
 
924
            subprocess.Popen(['sudo', '-s', '/bin/bash', '-c',
 
925
                              "/usr/bin/pkill -9 -f crontab",
 
926
                              '-u', 'mail'])
853
927
        self.assertFalse(os.WIFEXITED(result), 'test process did not exit normally')
854
928
        self.assertTrue(os.WIFSIGNALED(result), 'test process died due to signal')
855
929
        self.assertEqual(os.WCOREDUMP(result), expect_coredump)
870
944
            self.assertEqual(subprocess.call(['pidof', command]), 1,
871
945
                             'no running test executable processes')
872
946
 
 
947
        core_path = '%s/' % os.getcwd()
 
948
        if core_location:
 
949
            core_path = '%s/' % core_location
 
950
        core_path += 'core'
873
951
        if expect_corefile:
874
 
            self.assertTrue(os.path.exists('core'), 'leaves wanted core file')
 
952
            self.assertTrue(os.path.exists(core_path), 'leaves wanted core file')
875
953
            try:
876
954
                # check core file permissions
877
 
                st = os.stat('core')
 
955
                st = os.stat(core_path)
878
956
                self.assertEqual(stat.S_IMODE(st.st_mode), 0o600, 'core file has correct permissions')
879
957
                if expect_corefile_owner is not None:
880
958
                    self.assertEqual(st.st_uid, expect_corefile_owner, 'core file has correct owner')
881
959
 
882
960
                # check that core file is valid
883
961
                gdb = subprocess.Popen(['gdb', '--batch', '--ex', 'bt',
884
 
                                        command, 'core'],
 
962
                                        command, core_path],
885
963
                                       stdout=subprocess.PIPE,
886
964
                                       stderr=subprocess.PIPE)
887
965
                (out, err) = gdb.communicate()
889
967
                out = out.decode()
890
968
                err = err.decode().strip()
891
969
            finally:
892
 
                os.unlink('core')
 
970
                os.unlink(core_path)
893
971
        else:
894
 
            if os.path.exists('core'):
 
972
            if os.path.exists(core_path):
895
973
                try:
896
 
                    os.unlink('core')
 
974
                    os.unlink(core_path)
897
975
                except OSError as e:
898
976
                    sys.stderr.write(
899
 
                        'WARNING: cannot clean up core file %s/core: %s\n' %
900
 
                        (os.getcwd(), str(e)))
 
977
                        'WARNING: cannot clean up core file %s: %s\n' %
 
978
                        (core_path, str(e)))
901
979
 
902
980
                self.fail('leaves unexpected core file behind')
903
981