156
156
test_proc = self.create_test_process()
157
157
test_proc2 = self.create_test_process(False, '/bin/dd')
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)
162
162
time.sleep(0.5) # give it some time to grab the lock
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)
167
167
# app should wait indefinitely for stdin, while app2 should terminate
529
529
os.utime(myexe, None)
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')
535
err = app.stderr.read().decode()
536
self.assertNotEqual(app.wait(), 0, err)
533
err = app.communicate(b'foo')[1]
534
self.assertEqual(app.returncode, 0, err)
536
self.assertIn(b'executable was modified after program start', err)
538
with open('/var/log/apport.log') as f:
539
lines = f.readlines()
540
self.assertIn('executable was modified after program start', lines[-1])
539
542
os.kill(test_proc, 9)
540
543
os.waitpid(test_proc, 0)
676
679
self.assertEqual(apport.fileutils.get_all_reports(), [])
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))
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/',
700
if orig_editor is not None:
701
os.environ['EDITOR'] = orig_editor
704
reports = apport.fileutils.get_all_reports()
705
self.assertEqual(len(reports), 1)
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')
713
# no cores/dump if suid_dumpable == 0
714
self.do_crash(False, command='/bin/ping', args=['127.0.0.1'],
716
self.assertEqual(apport.fileutils.get_all_reports(), [])
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(), [])
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
753
cat /mnt/1/root/etc/shadow > /mnt/1/root/tmp/pwned
754
chmod 644 /mnt/1/root/tmp/pwned
756
os.fchmod(f.fileno(), 0o755)
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
768
mount --rbind /proc mnt
772
./busybox sh -c 'kill -SEGV $$'
774
if [ -e /apport.trace ]; then
775
echo "=== apport trace ===="
776
./busybox cat /apport.trace
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
783
This is being used in a container via systemd activation, where the
784
core dump gets read from /run/apport.socket.
786
socket_path = os.path.join(self.workdir, 'apport.socket')
787
test_proc = self.create_test_process()
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)
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')
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])
807
# call apport like systemd does via socket activation
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)
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)
822
os.kill(test_proc, 9)
823
os.waitpid(test_proc, 0)
825
reports = self.get_temp_all_reports()
826
self.assertEqual(len(reports), 1)
828
with open(reports[0], 'rb') as 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')
835
# should not create report on the host
836
self.assertEqual(apport.fileutils.get_all_system_reports(), [])
836
893
pid = self.create_test_process(check_running, command, uid=uid, args=args)
838
895
time.sleep(sleep)
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'])
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." %
840
911
# wait max 5 seconds for the process to die
842
913
while timeout >= 0:
870
944
self.assertEqual(subprocess.call(['pidof', command]), 1,
871
945
'no running test executable processes')
947
core_path = '%s/' % os.getcwd()
949
core_path = '%s/' % core_location
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')
876
954
# check core file permissions
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')
882
960
# check that core file is valid
883
961
gdb = subprocess.Popen(['gdb', '--batch', '--ex', 'bt',
885
963
stdout=subprocess.PIPE,
886
964
stderr=subprocess.PIPE)
887
965
(out, err) = gdb.communicate()