diff --git a/qubes/ext/core_features.py b/qubes/ext/core_features.py index e314f82f3..e04a43bdd 100644 --- a/qubes/ext/core_features.py +++ b/qubes/ext/core_features.py @@ -194,6 +194,16 @@ async def qubes_features_request(self, vm, event, untrusted_features): ) and hasattr(vm, "appvm_default_bootmode"): bootmode_value = untrusted_feature_value vm.features["boot-mode.appvm-default"] = bootmode_value + if "boot-mode.standalone-default" in untrusted_features: + untrusted_feature_value = untrusted_features[ + "boot-mode.standalone-default" + ] + if ( + f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features + or untrusted_feature_value == "default" + ) and hasattr(vm, "standalone_default_bootmode"): + bootmode_value = untrusted_feature_value + vm.features["boot-mode.standalone-default"] = bootmode_value # allow VMs to switch on anon-timezone, but do not permit dropping it if "anon-timezone" in untrusted_features: diff --git a/qubes/storage/__init__.py b/qubes/storage/__init__.py index a83c401e2..0efa40576 100644 --- a/qubes/storage/__init__.py +++ b/qubes/storage/__init__.py @@ -929,9 +929,13 @@ async def import_volume(self, dst_volume: Volume, src_volume: Volume): f"data" ) - return await qubes.utils.coro_maybe( + import_rslt = await qubes.utils.coro_maybe( dst_volume.import_volume(src_volume) ) + await self.vm.fire_event_async( + "domain-import-volume", name=dst_volume.name, source=src_volume + ) + return import_rslt class VolumesCollection: diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index b154c2ba3..87d98343c 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -1677,6 +1677,85 @@ def test_057_bootmode_default_user_default_bootmode(self): ], ) + def test_058_bootmode_standalone_default_good(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.kernelopts.orig-mode": "test1", + "boot-mode.kernelopts.new-mode": "test2", + "boot-mode.standalone-default": "new-mode", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.orig-mode", "test1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.new-mode", "test2"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.new-mode",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.standalone-default", "new-mode"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_059_bootmode_standalone_default_bad(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.kernelopts.orig-mode": "test1", + "boot-mode.kernelopts.new-mode": "test2", + "boot-mode.standalone-default": "missing-mode", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.orig-mode", "test1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.new-mode", "test2"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.missing-mode",), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + def test_060_anon_timezone_set(self): del self.vm.template self.loop.run_until_complete( diff --git a/qubes/tests/integ/storage.py b/qubes/tests/integ/storage.py index 3aa4877cb..275501895 100644 --- a/qubes/tests/integ/storage.py +++ b/qubes/tests/integ/storage.py @@ -31,6 +31,7 @@ import qubes.tests.storage_zfs import qubes.utils import qubes.vm.appvm +import qubes.vm.standalonevm class StorageTestMixin(object): @@ -46,6 +47,20 @@ def setUp(self): qubes.vm.appvm.AppVM, name=self.make_vm_name("vm2"), label="red" ) self.loop.run_until_complete(self.vm2.create_on_disk()) + self.standalone_vm1 = self.app.add_new_vm( + qubes.vm.standalonevm.StandaloneVM, + name=self.make_vm_name("standalone-vm1"), + label="red", + ) + self.standalone_vm1.clone_properties(self.app.domains[self.template]) + self.standalone_vm1.features.update( + self.app.domains[self.template].features + ) + self.loop.run_until_complete( + self.standalone_vm1.clone_disk_files( + self.app.domains[self.template] + ) + ) self.pool = None self.init_pool() self.app.save() @@ -485,6 +500,25 @@ async def _test_006_no_revisions(self): ) self.assertEqual(stdout, b"test123") + def test_007_standalone_clone_boot_mode_transition(self): + return self.loop.run_until_complete( + self._test_007_standalone_clone_boot_mode_transition() + ) + + async def _test_007_standalone_clone_boot_mode_transition(self): + standalone_vm1.features["boot-mode.kernelopts.orig-mode"] = "test1" + standalone_vm1.features["boot-mode.kernelopts.new-mode"] = "test2" + standalone_vm1.features["boot-mode.active"] = "orig-mode" + standalone_vm1.features["boot-mode.standalone-default"] = "new-mode" + template_rootvol = self.template.storage.get_volume("root") + standalone_vm1_rootvol = self.standalone_vm1.storage.get_volume("root") + await qubes.utils.coro_maybe( + standalone_vm1_rootvol.import_volume(template_rootvol) + ) + self.assertEqual( + standalone_vm1.features["boot-mode.active"], "new-mode" + ) + class StorageFile(StorageTestMixin, qubes.tests.SystemTestCase): def init_pool(self): diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 22654f99b..4b6ee11d2 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -668,6 +668,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.LocalVM): :param event: Event name (``'domain-tag-delete:' tag``) :param tag: tag name + .. event:: domain-import-volume (subject, event, name, source) + + A volume has been imported. + + :param subject: Event emitter (the qube object) + :param event: Event name (``'domain-import-volume'``) + :param name: Destination volume name + :param source: Source volume + .. event:: features-request (subject, event, *, untrusted_features) The domain is performing a features request. diff --git a/qubes/vm/standalonevm.py b/qubes/vm/standalonevm.py index c300a24b6..a51ccbae7 100644 --- a/qubes/vm/standalonevm.py +++ b/qubes/vm/standalonevm.py @@ -62,3 +62,15 @@ def __init__(self, *args, **kwargs): }, } super().__init__(*args, **kwargs) + + @qubes.events.handler("domain-import-volume") + def on_domain_import_volume(self, event, name, source): + # pylint: disable=unused-argument + if name != "root": + return + if "boot-mode.standalone-default" in self.features: + bootmode_value = self.features["boot-mode.standalone-default"] + if bootmode_value == "default": + self.features["boot-mode.active"] = "default" + if f"boot-mode.kernelopts.{bootmode_value}" in self.features: + self.features["boot-mode.active"] = bootmode_value