Skip to content

Commit b4626b6

Browse files
committed
Merge remote-tracking branch 'origin/pr/809'
* origin/pr/809: Reject wrong default_dispvm and management_dispvm
2 parents 0860aba + 95895a4 commit b4626b6

11 files changed

Lines changed: 493 additions & 86 deletions

File tree

qubes/app.py

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,45 @@ def validate_kernel(obj, property_name: str, kernel: str) -> None:
746746
)
747747

748748

749+
def get_qube_prop_deps(
750+
qube,
751+
system_properties: list | None = None,
752+
qube_properties: list | None = None,
753+
):
754+
if not system_properties:
755+
system_properties = []
756+
if not qube_properties:
757+
qube_properties = []
758+
system_deps: list = []
759+
qube_deps: list = []
760+
for obj in itertools.chain(qube.app.domains, (qube.app,)):
761+
if obj is qube:
762+
continue
763+
for prop in obj.property_list():
764+
if not (
765+
isinstance(prop, qubes.vm.VMProperty)
766+
and getattr(obj, prop.__name__, None) == qube
767+
):
768+
continue
769+
if getattr(obj, "is_preload", False) and (
770+
prop.__name__ == "template"
771+
or (
772+
prop.__name__ in ["default_dispvm", "management_dispvm"]
773+
and getattr(obj, "template", None) == qube
774+
)
775+
):
776+
continue
777+
if isinstance(obj, qubes.app.Qubes):
778+
if system_properties and prop.__name__ not in system_properties:
779+
continue
780+
system_deps.append(prop.__name__)
781+
elif not obj.property_is_default(prop):
782+
if qube_properties and prop.__name__ not in qube_properties:
783+
continue
784+
qube_deps.append((obj.name, prop.__name__))
785+
return system_deps, qube_deps
786+
787+
749788
class Qubes(qubes.PropertyHolder):
750789
"""Main Qubes application
751790
@@ -901,16 +940,20 @@ class Qubes(qubes.PropertyHolder):
901940
"default_dispvm",
902941
load_stage=3,
903942
default=None,
904-
doc="Default DispVM base for service calls",
943+
setter=qubes.vm.setter_disposable_template,
905944
allow_none=True,
945+
doc="""Default disposable template to be used for spawning disposable
946+
qubes for service calls in this system.""",
906947
)
907948

908949
management_dispvm = qubes.VMProperty(
909950
"management_dispvm",
910951
load_stage=3,
911952
default=None,
912-
doc="Default DispVM base for managing VMs",
913953
allow_none=True,
954+
setter=qubes.vm.setter_disposable_template,
955+
doc="""Default disposable template to be used for spawning disposable
956+
qubes for managing a qube in this system.""",
914957
)
915958

916959
default_pool = qubes.property(
@@ -1057,7 +1100,10 @@ def store(self):
10571100
return self._store
10581101

10591102
def _migrate_global_properties(self):
1060-
"""Migrate renamed/dropped properties"""
1103+
"""Migrate renamed/dropped properties or properties that had weak or no
1104+
setter to a stricter setter, that would make current value invalid,
1105+
requiring setting it to None, to avoid failure to load XML and qubesd.
1106+
"""
10611107
if self.xml is None:
10621108
return
10631109

@@ -1102,6 +1148,21 @@ def _migrate_global_properties(self):
11021148
pass
11031149
node_default_fw_netvm.getparent().remove(node_default_fw_netvm)
11041150

1151+
# Drop invalid setting.
1152+
for prop in ["default_dispvm", "management_dispvm"]:
1153+
for obj in [self] + list(self.domains):
1154+
if obj.xml is None:
1155+
continue
1156+
node_dispvm = obj.xml.find(
1157+
f"./properties/property[@name='{prop}']"
1158+
)
1159+
if node_dispvm is None or node_dispvm.text is None:
1160+
continue
1161+
dispvm = self.domains[node_dispvm.text]
1162+
if getattr(dispvm, "template_for_dispvms", None):
1163+
continue
1164+
node_dispvm.text = ""
1165+
11051166
def _migrate_labels(self):
11061167
"""Migrate changed labels"""
11071168
if self.xml is None:
@@ -1660,28 +1721,11 @@ def on_domain_pre_deleted(self, event, vm):
16601721
"""
16611722
# pylint: disable=unused-argument
16621723
dependencies = []
1663-
for obj in itertools.chain(self.domains, (self,)):
1664-
if obj is vm:
1665-
# allow removed VM to reference itself
1666-
continue
1667-
for prop in obj.property_list():
1668-
with suppress(AttributeError):
1669-
if (
1670-
isinstance(prop, qubes.vm.VMProperty)
1671-
and getattr(obj, prop.__name__) == vm
1672-
):
1673-
if getattr(obj, "is_preload", False) and (
1674-
prop.__name__ == "template"
1675-
or (
1676-
prop.__name__ == "default_dispvm"
1677-
and getattr(obj, "template", None) == vm
1678-
)
1679-
):
1680-
continue
1681-
if isinstance(obj, qubes.app.Qubes):
1682-
dependencies.insert(0, ("@GLOBAL", prop.__name__))
1683-
elif not obj.property_is_default(prop):
1684-
dependencies.append((obj.name, prop.__name__))
1724+
system_deps, qube_deps = get_qube_prop_deps(qube=vm)
1725+
if system_deps:
1726+
dependencies = [("@GLOBAL", dep) for dep in system_deps]
1727+
if qube_deps:
1728+
dependencies.extend(qube_deps)
16851729
if dependencies:
16861730
self.log.error(
16871731
"Cannot remove %s as it is used by %s",

qubes/tests/app.py

Lines changed: 188 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,169 @@ def test_100_property_migrate_default_fw_netvm(self):
637637
app.close()
638638
del app
639639

640-
def test_101_property_migrate_label(self):
640+
def test_101_property_migrate_disposable_template(self):
641+
xml_template = """<?xml version="1.0" encoding="utf-8" ?>
642+
<qubes version="3.0">
643+
<properties>
644+
<property name="default_template"></property>
645+
<property name="default_dispvm">{app_default_dispvm}</property>
646+
<property name="management_dispvm">{app_management_dispvm}</property>
647+
</properties>
648+
<labels>
649+
<label id="label-1" color="#cc0000">red</label>
650+
</labels>
651+
<pools>
652+
<pool driver="file" dir_path="/tmp/qubes-test" name="default"/>
653+
</pools>
654+
<domains>
655+
656+
<domain class="AdminVM" id="domain-0">
657+
<properties>
658+
<property name="default_dispvm">{adminvm_default_dispvm}</property>
659+
</properties>
660+
</domain>
661+
662+
<domain class="StandaloneVM" id="domain-1">
663+
<properties>
664+
<property name="qid">1</property>
665+
<property name="name">work</property>
666+
<property name="label" ref="label-1" />
667+
<property name="uuid">2fcfc1f4-b2fe-4361-931a-c5294b35edfa</property>
668+
<property name="management_dispvm">{work_management_dispvm}</property>
669+
</properties>
670+
<features/>
671+
<devices class="pci"/>
672+
</domain>
673+
674+
<domain class="StandaloneVM" id="domain-2">
675+
<properties>
676+
<property name="qid">3</property>
677+
<property name="name">disp-template</property>
678+
<property name="label" ref="label-1" />
679+
<property name="uuid">2ccfc1f4-b2fe-4361-931a-c5294b35edfa</property>
680+
<property name="template_for_dispvms">True</property>
681+
</properties>
682+
</domain>
683+
684+
</domains>
685+
</qubes>
686+
"""
687+
688+
with self.subTest("default"):
689+
with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file:
690+
xml_file.write(
691+
xml_template.format(
692+
app_default_dispvm="disp-template",
693+
app_management_dispvm="disp-template",
694+
adminvm_default_dispvm="disp-template",
695+
work_management_dispvm="disp-template",
696+
)
697+
)
698+
app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True)
699+
adminvm = app.domains["dom0"]
700+
work = app.domains["work"]
701+
disp_template = app.domains["disp-template"]
702+
703+
self.assertFalse(app.property_is_default("default_dispvm"))
704+
self.assertEqual(app.default_dispvm, disp_template)
705+
self.assertFalse(app.property_is_default("management_dispvm"))
706+
self.assertEqual(app.management_dispvm, disp_template)
707+
708+
self.assertFalse(adminvm.property_is_default("default_dispvm"))
709+
self.assertEqual(adminvm.default_dispvm, disp_template)
710+
711+
self.assertTrue(work.property_is_default("default_dispvm"))
712+
self.assertEqual(work.default_dispvm, disp_template)
713+
self.assertFalse(work.property_is_default("management_dispvm"))
714+
self.assertEqual(work.management_dispvm, disp_template)
715+
716+
self.assertTrue(disp_template.property_is_default("default_dispvm"))
717+
self.assertEqual(disp_template.default_dispvm, disp_template)
718+
self.assertTrue(
719+
disp_template.property_is_default("management_dispvm")
720+
)
721+
self.assertEqual(disp_template.management_dispvm, disp_template)
722+
723+
app.close()
724+
del app
725+
726+
with self.subTest("invalid-system"):
727+
with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file:
728+
xml_file.write(
729+
xml_template.format(
730+
app_default_dispvm="work",
731+
app_management_dispvm="work",
732+
adminvm_default_dispvm="disp-template",
733+
work_management_dispvm="disp-template",
734+
)
735+
)
736+
app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True)
737+
adminvm = app.domains["dom0"]
738+
work = app.domains["work"]
739+
disp_template = app.domains["disp-template"]
740+
741+
self.assertFalse(app.property_is_default("default_dispvm"))
742+
self.assertEqual(app.default_dispvm, None)
743+
self.assertFalse(app.property_is_default("management_dispvm"))
744+
self.assertEqual(app.management_dispvm, None)
745+
746+
self.assertFalse(adminvm.property_is_default("default_dispvm"))
747+
self.assertEqual(adminvm.default_dispvm, disp_template)
748+
749+
self.assertTrue(work.property_is_default("default_dispvm"))
750+
self.assertEqual(work.default_dispvm, None)
751+
self.assertFalse(work.property_is_default("management_dispvm"))
752+
self.assertEqual(work.management_dispvm, disp_template)
753+
754+
self.assertTrue(disp_template.property_is_default("default_dispvm"))
755+
self.assertEqual(disp_template.default_dispvm, None)
756+
self.assertTrue(
757+
disp_template.property_is_default("management_dispvm")
758+
)
759+
self.assertEqual(disp_template.management_dispvm, None)
760+
761+
app.close()
762+
del app
763+
764+
with self.subTest("invalid-per-qube"):
765+
with open("/tmp/qubestest.xml", "w", encoding="ascii") as xml_file:
766+
xml_file.write(
767+
xml_template.format(
768+
app_default_dispvm="disp-template",
769+
app_management_dispvm="disp-template",
770+
adminvm_default_dispvm="work",
771+
work_management_dispvm="work",
772+
)
773+
)
774+
app = qubes.Qubes("/tmp/qubestest.xml", offline_mode=True)
775+
adminvm = app.domains["dom0"]
776+
work = app.domains["work"]
777+
disp_template = app.domains["disp-template"]
778+
779+
self.assertFalse(app.property_is_default("default_dispvm"))
780+
self.assertEqual(app.default_dispvm, disp_template)
781+
self.assertFalse(app.property_is_default("management_dispvm"))
782+
self.assertEqual(app.management_dispvm, disp_template)
783+
784+
self.assertFalse(adminvm.property_is_default("default_dispvm"))
785+
self.assertEqual(adminvm.default_dispvm, None)
786+
787+
self.assertTrue(work.property_is_default("default_dispvm"))
788+
self.assertEqual(work.default_dispvm, disp_template)
789+
self.assertFalse(work.property_is_default("management_dispvm"))
790+
self.assertEqual(work.management_dispvm, None)
791+
792+
self.assertTrue(disp_template.property_is_default("default_dispvm"))
793+
self.assertEqual(disp_template.default_dispvm, disp_template)
794+
self.assertTrue(
795+
disp_template.property_is_default("management_dispvm")
796+
)
797+
self.assertEqual(disp_template.management_dispvm, disp_template)
798+
799+
app.close()
800+
del app
801+
802+
def test_102_property_migrate_label(self):
641803
xml_template = """<?xml version="1.0" encoding="utf-8" ?>
642804
<qubes version="3.0">
643805
<labels>
@@ -1053,27 +1215,32 @@ def test_203_remove_default_dispvm(self):
10531215
appvm = self.app.add_new_vm(
10541216
"AppVM", name="test-appvm", template=self.template, label="red"
10551217
)
1218+
appvm.template_for_dispvms = True
10561219
self.app.default_dispvm = appvm
10571220
with mock.patch.object(self.app, "vmm"):
10581221
with self.assertRaises(qubes.exc.QubesVMInUseError):
10591222
del self.app.domains[appvm]
10601223

1061-
def test_204_remove_appvm_dispvm(self):
1062-
dispvm = self.app.add_new_vm(
1063-
"AppVM", name="test-appvm", template=self.template, label="red"
1224+
def test_204_remove_appvm_used_as_default_dispvm(self):
1225+
appvm = self.app.add_new_vm(
1226+
"AppVM",
1227+
name="test-appvm",
1228+
template=self.template,
1229+
label="red",
1230+
template_for_dispvms=True,
10641231
)
10651232
self.app.add_new_vm(
10661233
"AppVM",
10671234
name="test-appvm2",
10681235
template=self.template,
1069-
default_dispvm=dispvm,
1236+
default_dispvm=appvm,
10701237
label="red",
10711238
)
10721239
with mock.patch.object(self.app, "vmm"):
10731240
with self.assertRaises(qubes.exc.QubesVMInUseError):
1074-
del self.app.domains[dispvm]
1241+
del self.app.domains[appvm]
10751242

1076-
def test_205_remove_appvm_dispvm(self):
1243+
def test_205_remove_appvm_used_as_template_of_dispvm(self):
10771244
appvm = self.app.add_new_vm(
10781245
"AppVM",
10791246
name="test-appvm",
@@ -1257,6 +1424,20 @@ def test_303_preload_default_dispvm_change_partial_noop(self):
12571424
"domain-preload-dispvm-start", reason=mock.ANY
12581425
)
12591426

1427+
def test_304_event_default_dispvm(self):
1428+
self.appvm.template_for_dispvms = False
1429+
with self.assertRaises(qubes.exc.QubesPropertyValueError):
1430+
self.app.default_dispvm = self.appvm
1431+
self.appvm.template_for_dispvms = True
1432+
self.app.default_dispvm = self.appvm
1433+
1434+
def test_305_event_management_dispvm(self):
1435+
self.appvm.template_for_dispvms = False
1436+
with self.assertRaises(qubes.exc.QubesPropertyValueError):
1437+
self.app.management_dispvm = self.appvm
1438+
self.appvm.template_for_dispvms = True
1439+
self.app.management_dispvm = self.appvm
1440+
12601441
@qubes.tests.skipUnlessGit
12611442
def test_900_example_xml_in_doc(self):
12621443
path = os.path.join(qubes.tests.in_git, "doc/example.xml")

qubes/tests/vm/adminvm.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def test_700_run_service(self, mock_subprocess):
131131
"test.service",
132132
"dom0",
133133
"name",
134-
"dom0"
134+
"dom0",
135135
)
136136

137137
mock_subprocess.reset_mock()
@@ -162,7 +162,7 @@ def test_700_run_service(self, mock_subprocess):
162162
"test.service",
163163
self.appvm.name,
164164
"name",
165-
"dom0"
165+
"dom0",
166166
)
167167

168168
@unittest.mock.patch("qubes.vm.adminvm.AdminVM.run_service")
@@ -297,3 +297,13 @@ def test_803_preload_set_delay(self):
297297
for value in cases_valid:
298298
with self.subTest(value=value):
299299
self.vm.features["preload-dispvm-delay"] = value
300+
301+
def test_901_prop_event_disposable_template(self):
302+
appvm = self.app.add_new_vm(
303+
"AppVM",
304+
name="test-1",
305+
template=self.template,
306+
label="red",
307+
)
308+
with self.assertRaises(qubes.exc.QubesPropertyValueError):
309+
self.vm.default_dispvm = appvm

0 commit comments

Comments
 (0)