diff --git a/.travis.yml b/.travis.yml index 2e11ea15..d957f443 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ services: install: - sudo apt-get -y install python3-gi gir1.2-gtk-3.0 - pip3 install --quiet -r ci/requirements.txt - - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-builder ~/qubes-builder - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-core-qrexec ~/qubes-core-qrexec script: - PYTHONPATH=test-packages:~/qubes-core-qrexec pylint qubes diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 9ff81651..3dab8ceb 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -21,8 +21,6 @@ # License along with this library; if not, see . # -import re - import qubes.config import qubes.ext import qubes.exc @@ -89,14 +87,12 @@ class GUI(qubes.ext.Extension): # Add GuiVM Xen ID for gui-daemon if getattr(vm, 'guivm', None): - if vm != vm.guivm and vm.guivm.is_running(): - vm.untrusted_qdb.write('/qubes-gui-domain-xid', - str(vm.guivm.xid)) + if vm != vm.guivm: + vm.untrusted_qdb.write('/keyboard-layout', vm.keyboard_layout) - # Add keyboard layout from that of GuiVM - kbd_layout = vm.guivm.features.get('keyboard-layout', None) - if kbd_layout: - vm.untrusted_qdb.write('/keyboard-layout', kbd_layout) + if vm.guivm.is_running(): + vm.untrusted_qdb.write('/qubes-gui-domain-xid', + str(vm.guivm.xid)) # Set GuiVM prefix guivm_windows_prefix = vm.features.get('guivm-windows-prefix', 'GuiVM') @@ -121,22 +117,21 @@ class GUI(qubes.ext.Extension): attached_vm.untrusted_qdb.write('/qubes-gui-domain-xid', str(vm.xid)) - @qubes.ext.handler('domain-feature-pre-set:keyboard-layout') - def on_feature_pre_set(self, subject, event, feature, value, oldvalue=None): - untrusted_xkb_layout = value.split('+') - if len(untrusted_xkb_layout) != 3: - raise qubes.exc.QubesValueError("Invalid number of parameters") + @qubes.ext.handler('property-reset:keyboard_layout') + def on_keyboard_reset(self, vm, event, name, oldvalue=None): + if not vm.is_running(): + return + kbd_layout = vm.keyboard_layout - untrusted_layout = untrusted_xkb_layout[0] - untrusted_variant = untrusted_xkb_layout[1] - untrusted_options = untrusted_xkb_layout[2] + vm.untrusted_qdb.write('/keyboard-layout', kbd_layout) - re_variant = r'^[a-zA-Z0-9-_]*$' - re_options = r'^[a-zA-Z0-9-_:,]*$' + @qubes.ext.handler('property-set:keyboard_layout') + def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): + for domain in vm.app.domains: + if getattr(domain, 'guivm', None) == vm and \ + domain.property_is_default('keyboard_layout'): + domain.fire_event('property-reset:keyboard_layout', + name='keyboard_layout', oldvalue=oldvalue) - if not untrusted_layout.isalpha(): - raise qubes.exc.QubesValueError("Invalid layout provided") - if not re.match(re_variant, untrusted_variant): - raise qubes.exc.QubesValueError("Invalid variant provided") - if not re.match(re_options, untrusted_options): - raise qubes.exc.QubesValueError("Invalid options provided") + if vm.is_running(): + vm.untrusted_qdb.write('/keyboard-layout', newvalue) diff --git a/qubes/tests/vm/__init__.py b/qubes/tests/vm/__init__.py index 9850a459..afecfb34 100644 --- a/qubes/tests/vm/__init__.py +++ b/qubes/tests/vm/__init__.py @@ -99,6 +99,7 @@ class TestApp(qubes.tests.TestEmitter): self.default_pool_kernel = 'linux-kernel' self.default_qrexec_timeout = 60 self.default_netvm = None + self.default_guivm = None self.domains = TestVMsCollection() #: jinja2 environment for libvirt XML templates self.env = jinja2.Environment( diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 3d3c5234..a003d707 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -1877,7 +1877,8 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): name='appvm', qid=3) vm.netvm = None vm.guivm = guivm - guivm.features['keyboard-layout'] = 'fr++' + vm.is_running = lambda: True + guivm.keyboard_layout = 'fr++' guivm.is_running = lambda: True vm.events_enabled = True test_qubesdb = TestQubesDB() @@ -1925,6 +1926,7 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): name='appvm', qid=3) vm.netvm = None vm.audiovm = audiovm + vm.is_running = lambda: True audiovm.is_running = lambda: True vm.events_enabled = True test_qubesdb = TestQubesDB() @@ -1968,8 +1970,87 @@ class TC_90_QubesVM(QubesVMTestsMixin, qubes.tests.QubesTestCase): name='sys-gui', qid=2, provides_network=False) guivm.is_running = lambda: True guivm.events_enabled = True - with self.assertRaises(qubes.exc.QubesValueError): - guivm.features['keyboard-layout'] = 'fr123++' + with self.assertRaises(qubes.exc.QubesPropertyValueError): + guivm.keyboard_layout = 'fr123++' + + with self.assertRaises(qubes.exc.QubesPropertyValueError): + guivm.keyboard_layout = 'fr+???+' + + with self.assertRaises(qubes.exc.QubesPropertyValueError): + guivm.keyboard_layout = 'fr++variant?' + + with self.assertRaises(qubes.exc.QubesPropertyValueError): + guivm.keyboard_layout = 'fr' + + @unittest.mock.patch('qubes.utils.get_timezone') + @unittest.mock.patch('qubes.utils.urandom') + @unittest.mock.patch('qubes.vm.qubesvm.QubesVM.untrusted_qdb') + def test_625_qdb_keyboard_layout_change(self, mock_qubesdb, mock_urandom, + mock_timezone): + mock_urandom.return_value = b'A' * 64 + mock_timezone.return_value = 'UTC' + template = self.get_vm( + cls=qubes.vm.templatevm.TemplateVM, name='template') + template.netvm = None + guivm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template, + name='sys-gui', qid=2, provides_network=False) + vm = self.get_vm(cls=qubes.vm.appvm.AppVM, template=template, + name='appvm', qid=3) + vm.netvm = None + vm.guivm = guivm + vm.is_running = lambda: True + guivm.keyboard_layout = 'fr++' + guivm.is_running = lambda: True + vm.events_enabled = True + test_qubesdb = TestQubesDB() + mock_qubesdb.write.side_effect = test_qubesdb.write + mock_qubesdb.rm.side_effect = test_qubesdb.rm + vm.create_qdb_entries() + self.maxDiff = None + + expected = { + '/name': 'test-inst-appvm', + '/type': 'AppVM', + '/default-user': 'user', + '/keyboard-layout': 'fr++', + '/qubes-vm-type': 'AppVM', + '/qubes-gui-domain-xid': '{}'.format(guivm.xid), + '/qubes-debug-mode': '0', + '/qubes-base-template': 'test-inst-template', + '/qubes-timezone': 'UTC', + '/qubes-random-seed': base64.b64encode(b'A' * 64), + '/qubes-vm-persistence': 'rw-only', + '/qubes-vm-updateable': 'False', + '/qubes-block-devices': '', + '/qubes-usb-devices': '', + '/qubes-iptables': 'reload', + '/qubes-iptables-error': '', + '/qubes-iptables-header': unittest.mock.ANY, + '/qubes-service/qubes-update-check': '0', + '/qubes-service/meminfo-writer': '1', + '/connected-ips': '', + '/connected-ips6': '', + } + + with self.subTest('default'): + self.assertEqual(test_qubesdb.data, expected) + + with self.subTest('value_change'): + vm.keyboard_layout = 'de++' + expected['/keyboard-layout'] = 'de++' + self.assertEqual(test_qubesdb.data, expected) + + with self.subTest('value_revert'): + vm.keyboard_layout = qubes.property.DEFAULT + expected['/keyboard-layout'] = 'fr++' + self.assertEqual(test_qubesdb.data, expected) + + with self.subTest('no_default'): + guivm.keyboard_layout = qubes.property.DEFAULT + vm.keyboard_layout = qubes.property.DEFAULT + expected['/keyboard-layout'] = 'us++' + self.assertEqual(test_qubesdb.data, expected) + @asyncio.coroutine def coroutine_mock(self, mock, *args, **kwargs): diff --git a/qubes/vm/adminvm.py b/qubes/vm/adminvm.py index f02021c3..1cfd8813 100644 --- a/qubes/vm/adminvm.py +++ b/qubes/vm/adminvm.py @@ -28,6 +28,7 @@ import libvirt import qubes import qubes.exc import qubes.vm +from qubes.vm.qubesvm import _setter_kbd_layout class AdminVM(qubes.vm.BaseVM): @@ -61,6 +62,14 @@ class AdminVM(qubes.vm.BaseVM): setter=qubes.property.forbidden, doc='True if this machine may be updated on its own.') + # for changes in keyboard_layout, see also the same property in QubesVM + keyboard_layout = qubes.property( + 'keyboard_layout', + type=str, + setter=_setter_kbd_layout, + default='us++', + doc='Keyboard layout for this VM') + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index aca16905..aad3ddaa 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -23,6 +23,7 @@ import asyncio import base64 import grp +import re import os import os.path import shutil @@ -115,6 +116,32 @@ def _setter_virt_mode(self, prop, value): return value +def _setter_kbd_layout(self, prop, value): + untrusted_xkb_layout = value.split('+') + if len(untrusted_xkb_layout) != 3: + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "invalid number of keyboard layout parameters") + + untrusted_layout = untrusted_xkb_layout[0] + untrusted_variant = untrusted_xkb_layout[1] + untrusted_options = untrusted_xkb_layout[2] + + re_variant = r'^[a-zA-Z0-9-_]*$' + re_options = r'^[a-zA-Z0-9-_:,]*$' + + if not untrusted_layout.isalpha(): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid keyboard layout provided") + if not re.match(re_variant, untrusted_variant): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid layout variant provided") + if not re.match(re_options, untrusted_options): + raise qubes.exc.QubesPropertyValueError( + self, prop, value, "Invalid layout options provided") + + return value + + def _default_virt_mode(self): if self.devices['pci'].persistent(): return 'hvm' @@ -690,6 +717,14 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): setter=qubes.property.forbidden, doc='True if this machine may be updated on its own.') + # for changes in keyboard_layout, see also the same property in AdminVM + keyboard_layout = qubes.property( + 'keyboard_layout', + default=(lambda self: getattr(self.guivm, 'keyboard_layout', 'us++')), + type=str, + setter=_setter_kbd_layout, + doc='Keyboard layout for this VM') + # # static, class-wide properties #