Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions qubes-rpc-policy/90-default.policy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
165 changes: 82 additions & 83 deletions qubes/ext/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -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"):
Expand All @@ -93,54 +126,45 @@ 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):
attached_vms = [
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)
)
Expand All @@ -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)),
)
1 change: 1 addition & 0 deletions qubes/tests/api_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions qubes/tests/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions qubes/tests/integ/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions qubes/tests/vm/dispvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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])
Expand Down
6 changes: 6 additions & 0 deletions qubes/tests/vm/mix/dvmtemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
18 changes: 16 additions & 2 deletions qubes/vm/dispvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
Loading