Skip to content

Commit bb2ea80

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 bb2ea80

11 files changed

Lines changed: 381 additions & 34 deletions

File tree

qubes/app.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -890,16 +890,20 @@ class Qubes(qubes.PropertyHolder):
890890
"default_dispvm",
891891
load_stage=3,
892892
default=None,
893-
doc="Default DispVM base for service calls",
893+
setter=qubes.vm.setter_disposable_template,
894894
allow_none=True,
895+
doc="""Default disposable template to be used for spawning disposable
896+
qubes for service calls in this system.""",
895897
)
896898

897899
management_dispvm = qubes.VMProperty(
898900
"management_dispvm",
899901
load_stage=3,
900902
default=None,
901-
doc="Default DispVM base for managing VMs",
902903
allow_none=True,
904+
setter=qubes.vm.setter_disposable_template,
905+
doc="""Default disposable template to be used for spawning disposable
906+
qubes for managing a qube in this system.""",
903907
)
904908

905909
default_pool = qubes.property(
@@ -1046,7 +1050,10 @@ def store(self):
10461050
return self._store
10471051

10481052
def _migrate_global_properties(self):
1049-
"""Migrate renamed/dropped properties"""
1053+
"""Migrate renamed/dropped properties or properties that had weak or no
1054+
setter to a stricter setter, that would make current value invalid,
1055+
requiring setting it to None, to avoid failure to load XML and qubesd.
1056+
"""
10501057
if self.xml is None:
10511058
return
10521059

@@ -1091,6 +1098,21 @@ def _migrate_global_properties(self):
10911098
pass
10921099
node_default_fw_netvm.getparent().remove(node_default_fw_netvm)
10931100

1101+
# Drop invalid setting.
1102+
for prop in ["default_dispvm", "management_dispvm"]:
1103+
for obj in [self] + list(self.domains):
1104+
if obj.xml is None:
1105+
continue
1106+
node_dispvm = obj.xml.find(
1107+
f"./properties/property[@name='{prop}']"
1108+
)
1109+
if node_dispvm is None or node_dispvm.text is None:
1110+
continue
1111+
dispvm = self.domains[node_dispvm.text]
1112+
if getattr(dispvm, "template_for_dispvms", None):
1113+
continue
1114+
node_dispvm.text = ""
1115+
10941116
def _migrate_labels(self):
10951117
"""Migrate changed labels"""
10961118
if self.xml is None:

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

qubes/tests/vm/dispvm.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,3 +725,29 @@ def test_024_is_preload_outdated(self, _mock_makedirs, _mock_symlink):
725725
sorted(dispvm.is_preload_outdated()["properties"]),
726726
sorted(["netvm", "dns", "visible_netmask"]),
727727
)
728+
729+
def test_030_set_disposable_template(self):
730+
self.appvm.template_for_dispvms = True
731+
self.appvm_alt.template_for_dispvms = False
732+
orig_getitem = self.app.domains.__getitem__
733+
with mock.patch.object(
734+
self.app, "domains", wraps=self.app.domains
735+
) as mock_domains:
736+
mock_domains.configure_mock(
737+
**{
738+
"get_new_unused_dispid": mock.Mock(return_value=42),
739+
"__getitem__.side_effect": orig_getitem,
740+
}
741+
)
742+
dispvm = self.app.add_new_vm(
743+
qubes.vm.dispvm.DispVM,
744+
name="test-dispvm",
745+
template=self.appvm,
746+
)
747+
with self.assertRaises(qubes.exc.QubesPropertyValueError):
748+
dispvm.default_dispvm = self.appvm_alt
749+
with self.assertRaises(qubes.exc.QubesPropertyValueError):
750+
dispvm.management_dispvm = self.appvm_alt
751+
self.appvm_alt.template_for_dispvms = True
752+
dispvm.default_dispvm = self.appvm_alt
753+
dispvm.management_dispvm = self.appvm_alt

0 commit comments

Comments
 (0)