Skip to content

Commit 95895a4

Browse files
committed
Reject wrong default_dispvm and management_dispvm
Validation on pre-events is not called when using "app.add_new_vm()", a setter is necessary for those cases. Setter plus pre-set is unnecessary when validating only newvalue. Also prohibit removing "template_for_dispvms" when disposable template is referenced as a property by the system or any other qube, including itself. Fixes: QubesOS/qubes-issues#10881
1 parent 71b5ab2 commit 95895a4

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
@@ -735,6 +735,45 @@ def validate_kernel(obj, property_name: str, kernel: str) -> None:
735735
)
736736

737737

738+
def get_qube_prop_deps(
739+
qube,
740+
system_properties: list | None = None,
741+
qube_properties: list | None = None,
742+
):
743+
if not system_properties:
744+
system_properties = []
745+
if not qube_properties:
746+
qube_properties = []
747+
system_deps: list = []
748+
qube_deps: list = []
749+
for obj in itertools.chain(qube.app.domains, (qube.app,)):
750+
if obj is qube:
751+
continue
752+
for prop in obj.property_list():
753+
if not (
754+
isinstance(prop, qubes.vm.VMProperty)
755+
and getattr(obj, prop.__name__, None) == qube
756+
):
757+
continue
758+
if getattr(obj, "is_preload", False) and (
759+
prop.__name__ == "template"
760+
or (
761+
prop.__name__ in ["default_dispvm", "management_dispvm"]
762+
and getattr(obj, "template", None) == qube
763+
)
764+
):
765+
continue
766+
if isinstance(obj, qubes.app.Qubes):
767+
if system_properties and prop.__name__ not in system_properties:
768+
continue
769+
system_deps.append(prop.__name__)
770+
elif not obj.property_is_default(prop):
771+
if qube_properties and prop.__name__ not in qube_properties:
772+
continue
773+
qube_deps.append((obj.name, prop.__name__))
774+
return system_deps, qube_deps
775+
776+
738777
class Qubes(qubes.PropertyHolder):
739778
"""Main Qubes application
740779
@@ -890,16 +929,20 @@ class Qubes(qubes.PropertyHolder):
890929
"default_dispvm",
891930
load_stage=3,
892931
default=None,
893-
doc="Default DispVM base for service calls",
932+
setter=qubes.vm.setter_disposable_template,
894933
allow_none=True,
934+
doc="""Default disposable template to be used for spawning disposable
935+
qubes for service calls in this system.""",
895936
)
896937

897938
management_dispvm = qubes.VMProperty(
898939
"management_dispvm",
899940
load_stage=3,
900941
default=None,
901-
doc="Default DispVM base for managing VMs",
902942
allow_none=True,
943+
setter=qubes.vm.setter_disposable_template,
944+
doc="""Default disposable template to be used for spawning disposable
945+
qubes for managing a qube in this system.""",
903946
)
904947

905948
default_pool = qubes.property(
@@ -1046,7 +1089,10 @@ def store(self):
10461089
return self._store
10471090

10481091
def _migrate_global_properties(self):
1049-
"""Migrate renamed/dropped properties"""
1092+
"""Migrate renamed/dropped properties or properties that had weak or no
1093+
setter to a stricter setter, that would make current value invalid,
1094+
requiring setting it to None, to avoid failure to load XML and qubesd.
1095+
"""
10501096
if self.xml is None:
10511097
return
10521098

@@ -1091,6 +1137,21 @@ def _migrate_global_properties(self):
10911137
pass
10921138
node_default_fw_netvm.getparent().remove(node_default_fw_netvm)
10931139

1140+
# Drop invalid setting.
1141+
for prop in ["default_dispvm", "management_dispvm"]:
1142+
for obj in [self] + list(self.domains):
1143+
if obj.xml is None:
1144+
continue
1145+
node_dispvm = obj.xml.find(
1146+
f"./properties/property[@name='{prop}']"
1147+
)
1148+
if node_dispvm is None or node_dispvm.text is None:
1149+
continue
1150+
dispvm = self.domains[node_dispvm.text]
1151+
if getattr(dispvm, "template_for_dispvms", None):
1152+
continue
1153+
node_dispvm.text = ""
1154+
10941155
def _migrate_labels(self):
10951156
"""Migrate changed labels"""
10961157
if self.xml is None:
@@ -1649,28 +1710,11 @@ def on_domain_pre_deleted(self, event, vm):
16491710
"""
16501711
# pylint: disable=unused-argument
16511712
dependencies = []
1652-
for obj in itertools.chain(self.domains, (self,)):
1653-
if obj is vm:
1654-
# allow removed VM to reference itself
1655-
continue
1656-
for prop in obj.property_list():
1657-
with suppress(AttributeError):
1658-
if (
1659-
isinstance(prop, qubes.vm.VMProperty)
1660-
and getattr(obj, prop.__name__) == vm
1661-
):
1662-
if getattr(obj, "is_preload", False) and (
1663-
prop.__name__ == "template"
1664-
or (
1665-
prop.__name__ == "default_dispvm"
1666-
and getattr(obj, "template", None) == vm
1667-
)
1668-
):
1669-
continue
1670-
if isinstance(obj, qubes.app.Qubes):
1671-
dependencies.insert(0, ("@GLOBAL", prop.__name__))
1672-
elif not obj.property_is_default(prop):
1673-
dependencies.append((obj.name, prop.__name__))
1713+
system_deps, qube_deps = get_qube_prop_deps(qube=vm)
1714+
if system_deps:
1715+
dependencies = [("@GLOBAL", dep) for dep in system_deps]
1716+
if qube_deps:
1717+
dependencies.extend(qube_deps)
16741718
if dependencies:
16751719
self.log.error(
16761720
"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)