diff --git a/qubes-rpc-policy/90-default.policy b/qubes-rpc-policy/90-default.policy index dbc48a9d9..88213b7c2 100644 --- a/qubes-rpc-policy/90-default.policy +++ b/qubes-rpc-policy/90-default.policy @@ -67,6 +67,11 @@ qubes.SyncAppMenus * @anyvm dom0 allow # redirect it qubes.Notifications * @anyvm @default allow target=dom0 +# Services on GUIVM that requires the session to have started, otherwise, +# earlier policy will redirect it to another GUIVM. +qubes.WaitForSession * @tag:guivm-dom0 @adminvm allow +qubes.WaitForSession * @tag:guivm-dom0 @default allow target=dom0 + # HTTP proxy for downloading updates # Upgrade all TemplateVMs through sys-whonix. #qubes.UpdatesProxy * @type:TemplateVM @default allow target=sys-whonix diff --git a/qubes/ext/gui.py b/qubes/ext/gui.py index 01027c66b..36f4a1d0d 100644 --- a/qubes/ext/gui.py +++ b/qubes/ext/gui.py @@ -32,21 +32,9 @@ class GUI(qubes.ext.Extension): @staticmethod def attached_vms(vm): for domain in vm.app.domains: - if getattr(domain, "guivm", None) and domain.guivm == vm: + if hasattr(domain, "guivm") and domain.guivm is vm: yield domain - @qubes.ext.handler("domain-pre-shutdown") - def on_domain_pre_shutdown(self, vm, event, **kwargs): - attached_vms = [ - domain for domain in self.attached_vms(vm) if domain.is_running() - ] - if attached_vms and not kwargs.get("force", False): - raise qubes.exc.QubesVMError( - self, - "There are running VMs using this VM as GuiVM: " - "{}".format(", ".join(vm.name for vm in attached_vms)), - ) - @staticmethod def send_gui_mode(vm): vm.run_service( @@ -58,12 +46,6 @@ def send_gui_mode(vm): ), ) - @qubes.ext.handler("domain-init", "domain-load") - def on_domain_init_load(self, vm, event): - if getattr(vm, "guivm", None): - if "guivm-" + vm.guivm.name not in vm.tags: - self.on_property_set(vm, event, name="guivm", newvalue=vm.guivm) - @qubes.ext.handler("property-reset:guivm") def on_property_reset(self, subject, event, name, oldvalue=None): newvalue = getattr(subject, "guivm", None) @@ -74,14 +56,65 @@ def on_property_set(self, subject, event, name, newvalue, oldvalue=None): # Clean other 'guivm-XXX' tags. # gui-daemon can connect to only one domain tags_list = list(subject.tags) + found = False for tag in tags_list: if tag.startswith("guivm-"): + if newvalue and tag == "guivm-" + newvalue.name: + found = True + continue subject.tags.remove(tag) - + if found: + return if newvalue: guivm = "guivm-" + newvalue.name subject.tags.add(guivm) + @qubes.ext.handler("property-set:default_guivm", system=True) + def on_property_set_default_guivm( + self, app, event, name, newvalue, oldvalue=None + ): + for vm in app.domains: + if hasattr(vm, "guivm") and vm.property_is_default("guivm"): + vm.fire_event( + "property-set:guivm", + name="guivm", + newvalue=newvalue, + oldvalue=oldvalue, + ) + + @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 + vm.untrusted_qdb.write("/keyboard-layout", kbd_layout) + + @qubes.ext.handler("property-set:keyboard_layout") + def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): + if newvalue == oldvalue: + return + if vm.is_running(): + vm.untrusted_qdb.write("/keyboard-layout", newvalue) + attached_vms = [ + domain + for domain in self.attached_vms(vm) + if domain.property_is_default("keyboard_layout") + ] + for domain in attached_vms: + domain.fire_event( + "property-reset:keyboard_layout", + name="keyboard_layout", + oldvalue=oldvalue, + ) + + @qubes.ext.handler("domain-init", "domain-load") + def on_domain_init_load(self, vm, event): + guivm = getattr(vm, "guivm", None) + if not guivm: + return + if "guivm-" + guivm.name not in vm.tags: + self.on_property_set(vm, event, name="guivm", newvalue=guivm) + @qubes.ext.handler("domain-qdb-create") def on_domain_qdb_create(self, vm, event): for feature in ("gui-videoram-overhead", "gui-videoram-min"): @@ -93,43 +126,36 @@ def on_domain_qdb_create(self, vm, event): except KeyError: pass - vm.untrusted_qdb.write( - "/qubes-gui-enabled", - str( - bool( - getattr(vm, "guivm", None) and vm.features.get("gui", True) - ) - ), - ) + guivm = getattr(vm, "guivm", None) + gui = bool(guivm and vm.features.get("gui", True)) + vm.untrusted_qdb.write("/qubes-gui-enabled", str(gui)) # Add GuiVM Xen ID for gui-daemon - if getattr(vm, "guivm", None): + if guivm: if vm != vm.guivm: vm.untrusted_qdb.write("/keyboard-layout", vm.keyboard_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") if vm.features.get("service.guivm", None): + guivm_windows_prefix = vm.features.get( + "guivm-windows-prefix", "GuiVM" + ) vm.untrusted_qdb.write( "/guivm-windows-prefix", guivm_windows_prefix ) - @qubes.ext.handler("property-set:default_guivm", system=True) - def on_property_set_default_guivm( - self, app, event, name, newvalue, oldvalue=None - ): - for vm in app.domains: - if hasattr(vm, "guivm") and vm.property_is_default("guivm"): - vm.fire_event( - "property-set:guivm", - name="guivm", - newvalue=newvalue, - oldvalue=oldvalue, - ) + @qubes.ext.handler("domain-tag-add:created-by-*") + def set_guivm_on_created_by(self, vm, event, tag, **kwargs): + """Set GuiVM based on 'tag-created-vm-with' and 'set-created-guivm' + features + """ + # pylint: disable=unused-argument + created_by = vm.app.domains[tag.partition("created-by-")[2]] + if created_guivm := created_by.features.get("set-created-guivm", None): + vm.guivm = vm.app.domains[created_guivm] @qubes.ext.handler("domain-start") async def on_domain_start(self, vm, event, **kwargs): @@ -137,10 +163,8 @@ async def on_domain_start(self, vm, event, **kwargs): domain for domain in self.attached_vms(vm) if domain.is_running() ] for attached_vm in attached_vms: - attached_vm.untrusted_qdb.write( - "/qubes-gui-enabled", - str(bool(attached_vm.features.get("gui", True))), - ) + gui = bool(attached_vm.features.get("gui", True)) + attached_vm.untrusted_qdb.write("/qubes-gui-enabled", str(gui)) attached_vm.untrusted_qdb.write( "/qubes-gui-domain-xid", str(vm.xid) ) @@ -149,39 +173,14 @@ async def on_domain_start(self, vm, event, **kwargs): "/usr/bin/qubes-input-trigger", "--all", "--dom0" ) - @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 - - vm.untrusted_qdb.write("/keyboard-layout", kbd_layout) - - @qubes.ext.handler("property-set:keyboard_layout") - def on_keyboard_set(self, vm, event, name, newvalue, oldvalue=None): - if newvalue == oldvalue: - return - - if vm.is_running(): - vm.untrusted_qdb.write("/keyboard-layout", newvalue) - - 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, - ) - - @qubes.ext.handler("domain-tag-add:created-by-*") - def set_guivm_on_created_by(self, vm, event, tag, **kwargs): - """Set GuiVM based on 'tag-created-vm-with' and 'set-created-guivm' - features - """ - # pylint: disable=unused-argument - created_by = vm.app.domains[tag.partition("created-by-")[2]] - if created_by.features.get("set-created-guivm", None): - guivm = vm.app.domains[created_by.features["set-created-guivm"]] - vm.guivm = guivm + @qubes.ext.handler("domain-pre-shutdown") + def on_domain_pre_shutdown(self, vm, event, **kwargs): + attached_vms = [ + domain for domain in self.attached_vms(vm) if domain.is_running() + ] + if attached_vms and not kwargs.get("force", False): + raise qubes.exc.QubesVMError( + self, + "There are running VMs using this VM as GuiVM: " + "{}".format(", ".join(vm.name for vm in attached_vms)), + ) diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 7dee1cb2d..a03f6277c 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -4022,6 +4022,7 @@ def test_643_vm_create_disposable_preload_autostart( ) self.vm.features["qrexec"] = "1" self.vm.features["supported-rpc.qubes.WaitForRunningSystem"] = "1" + self.vm.features["supported-rpc.qubes.WaitForSession"] = "1" self.vm.features["preload-dispvm-max"] = "1" for _ in range(10): if len(self.vm.get_feat_preload()) == 1: diff --git a/qubes/tests/app.py b/qubes/tests/app.py index a3ab4a4b7..09341606e 100644 --- a/qubes/tests/app.py +++ b/qubes/tests/app.py @@ -695,6 +695,7 @@ def setUp(self): self.template.features["supported-rpc.qubes.WaitForRunningSystem"] = ( True ) + self.template.features["supported-rpc.qubes.WaitForSession"] = True self.appvm = self.app.add_new_vm( "AppVM", name="test-dvm", diff --git a/qubes/tests/integ/backup.py b/qubes/tests/integ/backup.py index 9ae0c1bb0..7ace16013 100644 --- a/qubes/tests/integ/backup.py +++ b/qubes/tests/integ/backup.py @@ -188,6 +188,7 @@ def create_backup_vms(self, pool=None): self.loop.run_until_complete(testvm5.create_on_disk(pool=pool)) testvm5.features["qrexec"] = True testvm5.features["supported-rpc.qubes.WaitForRunningSystem"] = True + testvm5.features["supported-rpc.qubes.WaitForSession"] = True testvm5.features["preload-dispvm-max"] = 0 testvm5.features["preload-dispvm"] = "" vms.append(testvm5) diff --git a/qubes/tests/vm/dispvm.py b/qubes/tests/vm/dispvm.py index 5b868cfde..864a6ef82 100644 --- a/qubes/tests/vm/dispvm.py +++ b/qubes/tests/vm/dispvm.py @@ -155,6 +155,7 @@ def test_000_from_appvm_preload_reject_max(self, mock_storage): self.appvm.template_for_dispvms = True orig_getitem = self.app.domains.__getitem__ self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "0" with mock.patch.object( self.app, "domains", wraps=self.app.domains @@ -186,6 +187,7 @@ def test_000_from_appvm_preload_use( self.appvm.template_for_dispvms = True self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "1" orig_getitem = self.app.domains.__getitem__ with mock.patch.object( @@ -252,6 +254,7 @@ def test_000_from_appvm_preload_fill_gap( mock_start.side_effect = self.mock_coro self.appvm.template_for_dispvms = True self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True orig_getitem = self.app.domains.__getitem__ with mock.patch("qubes.events.Emitter.fire_event_async") as mock_events: self.appvm.features["preload-dispvm-max"] = "1" @@ -295,6 +298,7 @@ def test_000_from_appvm_preload_fill_gap( def test_000_get_preload_max(self): self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), None) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1) self.assertEqual(qubes.vm.dispvm.get_preload_max(self.adminvm), None) @@ -311,9 +315,11 @@ def test_000_get_preload_templates(self): self.assertEqual(get_preload_templates(self.app), []) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm_alt.features["supported-rpc.qubes.WaitForRunningSystem"] = ( True ) + self.appvm_alt.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.appvm_alt.features["preload-dispvm-max"] = 0 self.assertEqual(get_preload_templates(self.app), [self.appvm]) diff --git a/qubes/tests/vm/mix/dvmtemplate.py b/qubes/tests/vm/mix/dvmtemplate.py index 9412c5300..cebc5ab72 100755 --- a/qubes/tests/vm/mix/dvmtemplate.py +++ b/qubes/tests/vm/mix/dvmtemplate.py @@ -84,6 +84,7 @@ def setUp(self): self.appvm.features["qrexec"] = True self.appvm.features["gui"] = False self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.app.domains[self.appvm.name] = self.appvm self.app.domains[self.appvm] = self.appvm self.app.default_dispvm = self.appvm @@ -163,9 +164,13 @@ def test_010_dvm_preload_get_max(self): self.appvm.features["qrexec"] = True self.appvm.features["gui"] = False self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = False + self.appvm.features["supported-rpc.qubes.WaitForSession"] = False with self.assertRaises(qubes.exc.QubesValueError): self.appvm.features["preload-dispvm-max"] = "1" self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + with self.assertRaises(qubes.exc.QubesValueError): + self.appvm.features["preload-dispvm-max"] = "1" + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = "1" cases_invalid = ["a", "-1", "1 1"] for value in cases_invalid: @@ -458,5 +463,6 @@ def test_040_dvm_preload_set_template_for_dispvms( def test_100_get_preload_templates(self): print(qubes.vm.dispvm.get_preload_templates(self.app)) self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True + self.appvm.features["supported-rpc.qubes.WaitForSession"] = True self.appvm.features["preload-dispvm-max"] = 1 self.assertEqual(qubes.vm.dispvm.get_preload_max(self.appvm), 1) diff --git a/qubes/vm/dispvm.py b/qubes/vm/dispvm.py index 4ee3b61d6..f806ae09e 100644 --- a/qubes/vm/dispvm.py +++ b/qubes/vm/dispvm.py @@ -451,14 +451,20 @@ async def wait_operational_preload( ) self.log.info("Preload startup completed '%s'", rpc) except asyncio.TimeoutError: - debug_msg = "systemd-analyze blame" + if rpc == "qubes.WaitForSession": + debug_msg = "systemd-analyze --user blame" + else: + debug_msg = "systemd-analyze blame" raise qubes.exc.QubesException( "Timed out call to '%s' after '%d' seconds during preload " "startup. To debug, run the following on a new disposable of " "'%s': %s" % (rpc, timeout, self.template, debug_msg) ) except (subprocess.CalledProcessError, qubes.exc.QubesException): - debug_msg = "systemctl --failed" + if rpc == "qubes.WaitForSession": + debug_msg = "systemctl --user --failed" + else: + debug_msg = "systemctl --failed" raise qubes.exc.QubesException( "Error on call to '%s' during preload startup. To debug, " "run the following on a new disposable of '%s': %s" @@ -486,6 +492,14 @@ async def on_domain_started_dispvm( # https://github.com/QubesOS/qubes-issues/issues/9964 path = "/run/qubes-rpc:/usr/local/etc/qubes-rpc:/etc/qubes-rpc" rpcs = ["qubes.WaitForRunningSystem"] + if ( + self.guivm + and self.features.check_with_template("gui", False) + and self.features.check_with_template( + "supported-feature.late-gui-daemon", False + ) + ): + rpcs.append("qubes.WaitForSession") start_tasks = [] for rpc in rpcs: service = '$(PATH="{}" command -v "{}")'.format(path, rpc) diff --git a/qubes/vm/mix/dvmtemplate.py b/qubes/vm/mix/dvmtemplate.py index 8a8098743..49de35615 100644 --- a/qubes/vm/mix/dvmtemplate.py +++ b/qubes/vm/mix/dvmtemplate.py @@ -19,7 +19,7 @@ # with this program; if not, see . import asyncio -from typing import Optional, Union, Iterator +from typing import Optional, Union, Iterator, Tuple import qubes.config import qubes.events @@ -204,10 +204,11 @@ def on_feature_pre_set_preload_dispvm_max( if not self.features.check_with_template("qrexec", None): raise qubes.exc.QubesValueError("Qube does not support qrexec") - service = "qubes.WaitForRunningSystem" - if not self.supports_preload(): + supported, missing_services = self.supports_preload() + if not supported: raise qubes.exc.QubesValueError( - "Qube does not support the RPC '%s'" % service + "Qube does not support the RPC(s) '%s'" + % ", ".join(missing_services) ) value = value or "0" @@ -465,11 +466,12 @@ async def on_domain_preload_dispvm_used( if delay: event_log += " with a delay of %s second(s)" % f"{delay:.1f}" self.log.info(event_log) - service = "qubes.WaitForRunningSystem" - if not self.supports_preload(): + + supported, missing_services = self.supports_preload() + if not supported: raise qubes.exc.QubesValueError( - "Qube does not support the RPC '%s' but tried to preload, " - "check if template is outdated" % service + "Qube does not support the RPC(s) '%s' but tried to preload, " + "check if template is outdated" % ", ".join(missing_services) ) if delay: await asyncio.sleep(abs(delay)) @@ -761,15 +763,21 @@ def remove_preload_excess( dispvm = self.app.domains[unwanted_disp] asyncio.ensure_future(dispvm.cleanup()) - def supports_preload(self) -> bool: + def supports_preload(self) -> Tuple[bool, list]: """ - Check if the necessary RPC is supported. + Check if the necessary RPCs are supported. - :rtype: bool + The first returned value indicates success while the second value is + non empty and contains the missing services if they are not supported. + + :rtype: (bool, list) """ assert isinstance(self, qubes.vm.BaseVM) - service = "qubes.WaitForRunningSystem" - supported_service = "supported-rpc." + service - if self.features.check_with_template(supported_service, False): - return True - return False + supported = True + missing_services = [] + for service in ["qubes.WaitForRunningSystem", "qubes.WaitForSession"]: + feature = "supported-rpc." + service + if not self.features.check_with_template(feature, False): + missing_services.append(service) + supported = False + return (supported, missing_services) diff --git a/tests/dispvm_perf.py b/tests/dispvm_perf.py index 1d8284f73..16fdf3347 100755 --- a/tests/dispvm_perf.py +++ b/tests/dispvm_perf.py @@ -928,6 +928,8 @@ async def run_test(self, test: TestConfig): self.dvm.features["preload-dispvm-delay"] = str( test.preload_delay ) + if not test.gui: + self.dvm.guivm = None if test.preload_max: preload_max = test.preload_max logger.info("Setting local max feature: '%s'", preload_max)