core-admin/tests/vm_qrexec_gui.py

523 lines
20 KiB
Python
Raw Normal View History

#!/usr/bin/python
2015-02-05 15:46:40 +01:00
# vim: fileencoding=utf-8
#
2015-02-05 15:46:40 +01:00
# The Qubes OS Project, https://www.qubes-os.org/
#
2015-02-05 15:46:40 +01:00
# Copyright (C) 2014-2015
# Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
# Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
#
2015-02-05 15:46:40 +01:00
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
2015-02-05 15:46:40 +01:00
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
2015-02-05 15:46:40 +01:00
import multiprocessing
import os
import subprocess
import unittest
import time
2015-02-09 06:18:57 +01:00
from qubes.qubes import QubesVmCollection, defaults, QubesException
2015-02-05 15:46:40 +01:00
import qubes.tests
TEST_DATA = "0123456789" * 1024
class TC_00_AppVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
def setUp(self):
2015-02-05 15:46:40 +01:00
super(TC_00_AppVM, self).setUp()
self.testvm1 = self.qc.add_new_vm("QubesAppVm",
2015-02-05 15:46:40 +01:00
name=self.make_vm_name('vm1'),
template=self.qc.get_default_template())
self.testvm1.create_on_disk(verbose=False)
self.testvm2 = self.qc.add_new_vm("QubesAppVm",
2015-02-05 15:46:40 +01:00
name=self.make_vm_name('vm2'),
template=self.qc.get_default_template())
self.testvm2.create_on_disk(verbose=False)
self.qc.save()
self.qc.unlock_db()
def test_000_start_shutdown(self):
self.testvm1.start()
self.assertEquals(self.testvm1.get_power_state(), "Running")
self.testvm1.shutdown()
shutdown_counter = 0
while self.testvm1.is_running():
if shutdown_counter > defaults["shutdown_counter_max"]:
self.fail("VM hanged during shutdown")
shutdown_counter += 1
time.sleep(1)
time.sleep(1)
self.assertEquals(self.testvm1.get_power_state(), "Halted")
2015-02-05 15:46:40 +01:00
def test_010_run_gui_app(self):
self.testvm1.start()
self.assertEquals(self.testvm1.get_power_state(), "Running")
self.testvm1.run("gnome-terminal")
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', 'user@%s' %
self.testvm1.name], stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for gnome-terminal window")
time.sleep(0.1)
time.sleep(0.5)
subprocess.check_call(['xdotool', 'search', '--name', 'user@%s' %
self.testvm1.name, 'windowactivate', 'type', 'exit\n'])
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', 'user@%s' %
self.testvm1.name], stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) == 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for gnome-terminal "
"termination")
time.sleep(0.1)
2015-02-05 15:46:40 +01:00
def test_050_qrexec_simple_eof(self):
"""Test for data and EOF transmission dom0->VM"""
result = multiprocessing.Value('i', 0)
def run(self, result):
p = self.testvm1.run("cat", passio_popen=True,
passio_stderr=True)
(stdout, stderr) = p.communicate(TEST_DATA)
if stdout != TEST_DATA:
result.value = 1
if len(stderr) > 0:
result.value = 2
self.testvm1.start()
t = multiprocessing.Process(target=run, args=(self, result))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
self.fail("Timeout, probably EOF wasn't transferred to the VM "
"process")
if result.value == 1:
self.fail("Received data differs from what was sent")
elif result.value == 2:
self.fail("Some data was printed to stderr")
2015-02-05 15:46:40 +01:00
def test_051_qrexec_simple_eof_reverse(self):
"""Test for EOF transmission VM->dom0"""
result = multiprocessing.Value('i', 0)
def run(self, result):
p = self.testvm1.run("echo test; exec >&-; cat > /dev/null",
passio_popen=True, passio_stderr=True)
# this will hang on test failure
stdout = p.stdout.read()
p.stdin.write(TEST_DATA)
p.stdin.close()
if stdout.strip() != "test":
result.value = 1
# this may hang in some buggy cases
elif len(p.stderr.read()) > 0:
result.value = 2
elif p.poll() is None:
time.sleep(1)
if p.poll() is None:
result.value = 3
self.testvm1.start()
t = multiprocessing.Process(target=run, args=(self, result))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
self.fail("Timeout, probably EOF wasn't transferred from the VM "
"process")
if result.value == 1:
self.fail("Received data differs from what was expected")
elif result.value == 2:
self.fail("Some data was printed to stderr")
elif result.value == 3:
self.fail("VM proceess didn't terminated on EOF")
2015-02-05 15:46:40 +01:00
def test_052_qrexec_vm_service_eof(self):
"""Test for EOF transmission VM(src)->VM(dst)"""
result = multiprocessing.Value('i', 0)
def run(self, result):
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm %s test.EOF "
"/bin/sh -c 'echo test; exec >&-; cat "
">&$SAVED_FD_1'" % self.testvm2.name,
passio_popen=True)
(stdout, stderr) = p.communicate()
if stdout != "test\n":
result.value = 1
self.testvm1.start()
self.testvm2.start()
p = self.testvm2.run("cat > /etc/qubes-rpc/test.EOF", user="root",
passio_popen=True)
p.stdin.write("/bin/cat")
p.stdin.close()
p.wait()
policy = open("/etc/qubes-rpc/policy/test.EOF", "w")
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
policy.close()
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.EOF")
t = multiprocessing.Process(target=run, args=(self, result))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
self.fail("Timeout, probably EOF wasn't transferred")
if result.value == 1:
self.fail("Received data differs from what was expected")
2015-02-05 15:46:40 +01:00
@unittest.expectedFailure
def test_053_qrexec_vm_service_eof_reverse(self):
"""Test for EOF transmission VM(src)<-VM(dst)"""
result = multiprocessing.Value('i', 0)
def run(self, result):
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm %s test.EOF "
"/bin/sh -c 'cat >&$SAVED_FD_1'"
% self.testvm1.name,
passio_popen=True)
(stdout, stderr) = p.communicate()
if stdout != "test\n":
result.value = 1
self.testvm1.start()
self.testvm2.start()
p = self.testvm2.run("cat > /etc/qubes-rpc/test.EOF", user="root",
passio_popen=True)
p.stdin.write("echo test; exec >&-; cat >/dev/null")
p.stdin.close()
p.wait()
policy = open("/etc/qubes-rpc/policy/test.EOF", "w")
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
policy.close()
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.EOF")
t = multiprocessing.Process(target=run, args=(self, result))
t.start()
t.join(timeout=10)
if t.is_alive():
t.terminate()
self.fail("Timeout, probably EOF wasn't transferred")
if result.value == 1:
self.fail("Received data differs from what was expected")
2015-02-05 15:46:40 +01:00
2015-02-09 06:08:48 +01:00
def test_060_qrexec_exit_code_dom0(self):
self.testvm1.start()
p = self.testvm1.run("exit 0", passio_popen=True)
p.wait()
self.assertEqual(0, p.returncode)
p = self.testvm1.run("exit 3", passio_popen=True)
p.wait()
self.assertEqual(3, p.returncode)
2015-02-05 15:46:40 +01:00
2015-02-09 06:08:48 +01:00
@unittest.expectedFailure
def test_065_qrexec_exit_code_vm(self):
self.testvm1.start()
self.testvm2.start()
policy = open("/etc/qubes-rpc/policy/test.Retcode", "w")
policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
policy.close()
self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.Retcode")
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Retcode", user="root",
passio_popen=True)
p.stdin.write("exit 0")
p.stdin.close()
p.wait()
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm %s test.Retcode "
"/bin/sh -c 'cat >/dev/null'; echo $?"
% self.testvm1.name,
passio_popen=True)
(stdout, stderr) = p.communicate()
self.assertEqual(stdout, "0\n")
p = self.testvm2.run("cat > /etc/qubes-rpc/test.Retcode", user="root",
passio_popen=True)
p.stdin.write("exit 3")
p.stdin.close()
p.wait()
p = self.testvm1.run("/usr/lib/qubes/qrexec-client-vm %s test.Retcode "
"/bin/sh -c 'cat >/dev/null'; echo $?"
% self.testvm1.name,
passio_popen=True)
(stdout, stderr) = p.communicate()
self.assertEqual(stdout, "3\n")
2015-02-05 15:46:40 +01:00
def test_100_qrexec_filecopy(self):
self.testvm1.start()
self.testvm2.start()
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
self.testvm2.name, passio_popen=True,
passio_stderr=True)
# Confirm transfer
subprocess.check_call(['xdotool', 'search', '--sync', '--name', 'Question',
'key', 'y'])
p.wait()
self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
p.stderr.read())
retcode = self.testvm2.run("diff /etc/passwd "
"/home/user/QubesIncoming/%s/passwd" % self.testvm1.name, wait=True)
self.assertEqual(retcode, 0, "file differs")
2015-02-05 15:46:40 +01:00
def test_110_qrexec_filecopy_deny(self):
self.testvm1.start()
self.testvm2.start()
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
self.testvm2.name, passio_popen=True)
# Deny transfer
subprocess.check_call(['xdotool', 'search', '--sync', '--name', 'Question',
'key', 'n'])
p.wait()
2015-02-09 06:08:48 +01:00
self.assertNotEqual(p.returncode, 0, "qvm-copy-to-vm unexpectedly "
"succeeded")
retcode = self.testvm1.run("ls /home/user/QubesIncoming/%s" %
self.testvm1.name, wait=True,
ignore_stderr=True)
2015-02-09 06:08:48 +01:00
self.assertNotEqual(retcode, 0, "QubesIncoming exists although file "
"copy was "
"denied")
tests: disable qrexec_filecopy_self test When vchan connection is established back to the source domain, gntalloc crashes with this message: [ 9.937990] BUG: Bad page map in process qrexec-agent pte:80000000f9d41167 pmd:131c3067 [ 9.938010] page:ffffea00036a6638 count:1 mapcount:-1 mapping: (null) index:0xffffffffffffffff [ 9.938018] page flags: 0x4000000000000c14(referenced|dirty|reserved|private) [ 9.938033] addr:00007fa856d47000 vm_flags:140400fb anon_vma: (null) mapping:ffff880011efe940 index:11 [ 9.938042] vma->vm_ops->fault: (null) [ 9.938057] vma->vm_file->f_op->mmap: gntalloc_mmap+0x0/0x1c0 [xen_gntalloc] [ 9.938066] CPU: 0 PID: 1108 Comm: qrexec-agent Tainted: G O 3.12.23-1.pvops.qubes.x86_64 #1 [ 9.938074] ffff8800131f3818 ffff88001316fc78 ffffffff814db550 00007fa856d47000 [ 9.938085] ffff88001316fcb8 ffffffff81139413 ffff880011efe940 ffff8800131c3a38 [ 9.938096] ffffea00036a6638 00007fa856d47000 00007fa856d57000 ffff88001316fe18 [ 9.938107] Call Trace: [ 9.938117] [<ffffffff814db550>] dump_stack+0x45/0x56 [ 9.938126] [<ffffffff81139413>] print_bad_pte+0x1a3/0x240 [ 9.938133] [<ffffffff8113ac9e>] unmap_page_range+0x6ee/0x7d0 [ 9.938142] [<ffffffff8113adf6>] unmap_single_vma+0x76/0xa0 [ 9.938149] [<ffffffff8113be09>] unmap_vmas+0x49/0x90 [ 9.938157] [<ffffffff8114443c>] exit_mmap+0x9c/0x170 [ 9.938166] [<ffffffff8105950c>] mmput+0x5c/0x110 [ 9.938175] [<ffffffff8105d74c>] do_exit+0x27c/0xa20 [ 9.938184] [<ffffffff810908ef>] ? vtime_account_user+0x4f/0x60 [ 9.938194] [<ffffffff81116502>] ? context_tracking_user_exit+0x52/0xc0 [ 9.938203] [<ffffffff8105ed2a>] do_group_exit+0x3a/0xa0 [ 9.938211] [<ffffffff8105ed9f>] SyS_exit_group+0xf/0x10 [ 9.938220] [<ffffffff814ea907>] tracesys+0xdd/0xe2
2015-02-09 22:09:15 +01:00
@unittest.skip("Xen gntalloc driver crashes when page is mapped in the "
"same domain")
def test_120_qrexec_filecopy_self(self):
self.testvm1.start()
p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
self.testvm1.name, passio_popen=True,
passio_stderr=True)
# Confirm transfer
subprocess.check_call(['xdotool', 'search', '--sync', '--name', 'Question',
'key', 'y'])
p.wait()
self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
p.stderr.read())
retcode = self.testvm1.run("diff /etc/passwd "
"/home/user/QubesIncoming/%s/passwd" % self.testvm1.name, wait=True)
self.assertEqual(retcode, 0, "file differs")
2015-02-05 15:46:40 +01:00
class TC_10_HVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
2015-02-09 06:18:57 +01:00
# TODO: test with some OS inside
# TODO: windows tools tests
def test_000_create_start(self):
2015-02-05 15:46:40 +01:00
self.testvm1 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm1'))
self.testvm1.create_on_disk(verbose=False)
self.qc.save()
2015-02-09 06:18:57 +01:00
self.testvm1.start()
self.assertEquals(self.testvm1.get_power_state(), "Running")
def test_010_create_start_template(self):
2015-02-05 15:46:40 +01:00
self.templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
self.templatevm.create_on_disk(verbose=False)
2015-02-09 06:18:57 +01:00
self.templatevm.start()
self.assertEquals(self.templatevm.get_power_state(), "Running")
def test_020_create_start_template_vm(self):
2015-02-05 15:46:40 +01:00
self.templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
self.templatevm.create_on_disk(verbose=False)
self.testvm2 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm2'),
template=self.templatevm)
self.testvm2.create_on_disk(verbose=False)
self.qc.save()
2015-02-09 06:18:57 +01:00
self.testvm2.start()
self.assertEquals(self.testvm2.get_power_state(), "Running")
def test_030_prevent_simultaneus_start(self):
2015-02-05 15:46:40 +01:00
self.templatevm = self.qc.add_new_vm("QubesTemplateHVm",
name=self.make_vm_name('template'))
self.templatevm.create_on_disk(verbose=False)
self.testvm2 = self.qc.add_new_vm("QubesHVm",
name=self.make_vm_name('vm2'),
template=self.templatevm)
self.testvm2.create_on_disk(verbose=False)
self.qc.save()
2015-02-09 06:18:57 +01:00
self.templatevm.start()
self.assertEquals(self.templatevm.get_power_state(), "Running")
self.assertRaises(QubesException, self.testvm2.start)
self.templatevm.force_shutdown()
self.testvm2.start()
self.assertEquals(self.testvm2.get_power_state(), "Running")
self.assertRaises(QubesException, self.templatevm.start)
class TC_20_DispVM(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
2014-11-10 11:42:30 +01:00
def test_000_prepare_dvm(self):
2015-02-05 15:46:40 +01:00
self.qc.unlock_db()
2014-11-10 11:42:30 +01:00
retcode = subprocess.call(['/usr/bin/qvm-create-default-dvm',
'--default-template'],
stderr=open(os.devnull, 'w'))
self.assertEqual(retcode, 0)
2015-02-05 15:46:40 +01:00
self.qc.lock_db_for_writing()
2014-11-10 11:42:30 +01:00
self.qc.load()
self.assertIsNotNone(self.qc.get_vm_by_name(
self.qc.get_default_template().name + "-dvm"))
# TODO: check mtime of snapshot file
def test_010_simple_dvm_run(self):
2015-02-05 15:46:40 +01:00
self.qc.unlock_db()
2014-11-10 11:42:30 +01:00
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
'qubes.VMShell', 'dom0', 'DEFAULT'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=open(os.devnull, 'w'))
(stdout, _) = p.communicate(input="echo test")
self.assertEqual(stdout, "test\n")
# TODO: check if DispVM is destroyed
2015-02-05 15:46:40 +01:00
2014-11-10 11:42:30 +01:00
def test_020_gui_app(self):
2015-02-05 15:46:40 +01:00
self.qc.unlock_db()
2014-11-10 11:42:30 +01:00
p = subprocess.Popen(['/usr/lib/qubes/qfile-daemon-dvm',
'qubes.VMShell', 'dom0', 'DEFAULT'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=open(os.devnull, 'w'))
2015-02-05 15:46:40 +01:00
2014-11-10 11:42:30 +01:00
# wait for DispVM startup:
p.stdin.write("echo test\n")
p.stdin.flush()
l = p.stdout.readline()
self.assertEqual(l, "test\n")
2015-02-05 15:46:40 +01:00
2014-11-10 11:42:30 +01:00
# potential race condition, but our tests are supposed to be
# running on dedicated machine, so should not be a problem
self.qc.lock_db_for_reading()
self.qc.load()
self.qc.unlock_db()
2015-02-05 15:46:40 +01:00
2014-11-10 11:42:30 +01:00
max_qid = 0
for vm in self.qc.values():
if not vm.is_disposablevm():
continue
if vm.qid > max_qid:
max_qid = vm.qid
dispvm = self.qc[max_qid]
self.assertNotEqual(dispvm.qid, 0, "DispVM not found in qubes.xml")
self.assertTrue(dispvm.is_running())
2015-02-05 15:46:40 +01:00
2014-11-10 11:42:30 +01:00
p.stdin.write("gnome-terminal\n")
wait_count = 0
window_title = 'user@%s' % (dispvm.template.name + "-dvm")
while subprocess.call(['xdotool', 'search', '--name', window_title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for gnome-terminal window")
time.sleep(0.1)
time.sleep(0.5)
subprocess.check_call(['xdotool', 'search', '--name', window_title,
'windowactivate', 'type', 'exit\n'])
wait_count = 0
while subprocess.call(['xdotool', 'search', '--name', window_title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) == 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for gnome-terminal "
"termination")
time.sleep(0.1)
p.stdin.close()
wait_count = 0
while dispvm.is_running():
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for DispVM destruction")
time.sleep(0.1)
wait_count = 0
while p.poll() is None:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for qfile-daemon-dvm "
"termination")
time.sleep(0.1)
self.assertEqual(p.returncode, 0)
self.qc.lock_db_for_reading()
self.qc.load()
self.qc.unlock_db()
self.assertIsNone(self.qc.get_vm_by_name(dispvm.name),
"DispVM not removed from qubes.xml")
def test_030_edit_file(self):
self.testvm1 = self.qc.add_new_vm("QubesAppVm",
2015-02-05 15:46:40 +01:00
name=self.make_vm_name('vm1'),
template=self.qc.get_default_template())
2014-11-10 11:42:30 +01:00
self.testvm1.create_on_disk(verbose=False)
self.qc.save()
self.testvm1.start()
self.testvm1.run("echo test1 > /home/user/test.txt", wait=True)
2015-02-05 15:46:40 +01:00
self.qc.unlock_db()
2014-11-10 11:42:30 +01:00
p = self.testvm1.run("qvm-open-in-dvm /home/user/test.txt",
passio_popen=True)
wait_count = 0
# TODO: ensure that gedit is default editor?
window_title = '(/tmp/%s)' % self.testvm1.name
while subprocess.call(['xdotool', 'search', '--name', window_title],
stdout=open(os.path.devnull, 'w'),
stderr=subprocess.STDOUT) > 0:
wait_count += 1
if wait_count > 100:
self.fail("Timeout while waiting for editor window")
time.sleep(0.3)
time.sleep(0.5)
subprocess.check_call(['xdotool', 'search', '--name', window_title,
'windowactivate', 'type', 'test test 2\n'])
2014-11-10 11:42:30 +01:00
subprocess.check_call(['xdotool', 'search', '--name', window_title,
'key', 'ctrl+s', 'ctrl+q'])
p.wait()
p = self.testvm1.run("cat /home/user/test.txt",
passio_popen=True)
(test_txt_content, _) = p.communicate()
self.assertEqual(test_txt_content, "test test 2\ntest1\n")
2014-11-10 11:42:30 +01:00