Skip to content

Commit 202d550

Browse files
committed
Add support for default standalone boot modes
When a standalone is cloned from a template, the usual default boot mode of the standalone is the same as the boot mode the template originally used. In some instances this is undesirable; Kicksecure for instance puts its templates into a "sysmaint" boot mode that impairs standalone functionality. To fix this, add a new qvm-feature, "boot-mode.standalone-default", which states which boot mode a standalone should be switched to when its root volume is cloned from any other VM. (Note that cloning a standalone from another standalone will switch it to the boot mode specified by "boot-mode.standalone-default", cloning a template isn't the only way to trigger this.) Allows fixing QubesOS/qubes-issues#10514, but does not fix this bug by itself.
1 parent b4626b6 commit 202d550

6 files changed

Lines changed: 139 additions & 1 deletion

File tree

qubes/ext/core_features.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ async def qubes_features_request(self, vm, event, untrusted_features):
194194
) and hasattr(vm, "appvm_default_bootmode"):
195195
bootmode_value = untrusted_feature_value
196196
vm.features["boot-mode.appvm-default"] = bootmode_value
197+
if "boot-mode.standalone-default" in untrusted_features:
198+
untrusted_feature_value = untrusted_features[
199+
"boot-mode.standalone-default"
200+
]
201+
if (
202+
f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features
203+
or untrusted_feature_value == "default"
204+
) and hasattr(vm, "standalone_default_bootmode"):
205+
bootmode_value = untrusted_feature_value
206+
vm.features["boot-mode.standalone-default"] = bootmode_value
197207

198208
# allow VMs to switch on anon-timezone, but do not permit dropping it
199209
if "anon-timezone" in untrusted_features:

qubes/storage/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,9 +929,13 @@ async def import_volume(self, dst_volume: Volume, src_volume: Volume):
929929
f"data"
930930
)
931931

932-
return await qubes.utils.coro_maybe(
932+
import_rslt = await qubes.utils.coro_maybe(
933933
dst_volume.import_volume(src_volume)
934934
)
935+
await self.vm.fire_event_async(
936+
"domain-import-volume", name=dst_volume.name, source=src_volume
937+
)
938+
return import_rslt
935939

936940

937941
class VolumesCollection:

qubes/tests/ext.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,6 +1677,75 @@ def test_057_bootmode_default_user_default_bootmode(self):
16771677
],
16781678
)
16791679

1680+
def test_058_bootmode_standalone_default_good(self):
1681+
del self.vm.template
1682+
self.loop.run_until_complete(
1683+
self.ext.qubes_features_request(
1684+
self.vm,
1685+
"features-request",
1686+
untrusted_features={
1687+
"boot-mode.kernelopts.orig-mode": "test1",
1688+
"boot-mode.kernelopts.new-mode": "test2",
1689+
"boot-mode.standalone-default": "new-mode",
1690+
}
1691+
)
1692+
)
1693+
self.assertListEqual(
1694+
self.vm.mock_calls,
1695+
[
1696+
("features.items", (), {}),
1697+
(
1698+
"features.__setitem__",
1699+
("boot-mode.kernelopts.orig-mode", "test1"),
1700+
{},
1701+
),
1702+
(
1703+
"features.__setitem__",
1704+
("boot-mode.kernelopts.new-mode", "test2"),
1705+
{},
1706+
),
1707+
(
1708+
"features.__setitem__",
1709+
("boot-mode.standalone-default", "new-mode"),
1710+
{},
1711+
),
1712+
("features.get", ("qrexec", False), {}),
1713+
("features.get", ("qrexec", False), {}),
1714+
]
1715+
)
1716+
1717+
def test_059_bootmode_standalone_default_bad(self):
1718+
del self.vm.template
1719+
self.loop.run_until_complete(
1720+
self.ext.qubes_features_request(
1721+
self.vm,
1722+
"features-request",
1723+
untrusted_features={
1724+
"boot-mode.kernelopts.orig-mode": "test1",
1725+
"boot-mode.kernelopts.new-mode": "test2",
1726+
"boot-mode.standalone-default": "missing-mode",
1727+
}
1728+
)
1729+
)
1730+
self.assertListEqual(
1731+
self.vm.mock_calls,
1732+
[
1733+
("features.items", (), {}),
1734+
(
1735+
"features.__setitem__",
1736+
("boot-mode.kernelopts.orig-mode", "test1"),
1737+
{},
1738+
),
1739+
(
1740+
"features.__setitem__",
1741+
("boot-mode.kernelopts.new-mode", "test2"),
1742+
{},
1743+
),
1744+
("features.get", ("qrexec", False), {}),
1745+
("features.get", ("qrexec", False), {}),
1746+
]
1747+
)
1748+
16801749
def test_060_anon_timezone_set(self):
16811750
del self.vm.template
16821751
self.loop.run_until_complete(

qubes/tests/integ/storage.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import qubes.tests.storage_zfs
3232
import qubes.utils
3333
import qubes.vm.appvm
34+
import qubes.vm.standalonevm
3435

3536

3637
class StorageTestMixin(object):
@@ -46,6 +47,20 @@ def setUp(self):
4647
qubes.vm.appvm.AppVM, name=self.make_vm_name("vm2"), label="red"
4748
)
4849
self.loop.run_until_complete(self.vm2.create_on_disk())
50+
self.standalone_vm1 = self.app.add_new_vm(
51+
qubes.vm.standalonevm.StandaloneVM,
52+
name=self.make_vm_name("standalone-vm1"),
53+
label="red",
54+
)
55+
self.standalone_vm1.clone_properties(self.app.domains[self.template])
56+
self.standalone_vm1.features.update(
57+
self.app.domains[self.template].features
58+
)
59+
self.loop.run_until_complete(
60+
self.standalone_vm1.clone_disk_files(
61+
self.app.domains[self.template]
62+
)
63+
)
4964
self.pool = None
5065
self.init_pool()
5166
self.app.save()
@@ -485,6 +500,25 @@ async def _test_006_no_revisions(self):
485500
)
486501
self.assertEqual(stdout, b"test123")
487502

503+
def test_007_standalone_clone_boot_mode_transition(self):
504+
return self.loop.run_until_complete(
505+
self._test_007_standalone_clone_boot_mode_transition()
506+
)
507+
508+
async def _test_007_standalone_clone_boot_mode_transition(self):
509+
standalone_vm1.features["boot-mode.kernelopts.orig-mode"] = "test1"
510+
standalone_vm1.features["boot-mode.kernelopts.new-mode"] = "test2"
511+
standalone_vm1.features["boot-mode.active"] = "orig-mode"
512+
standalone_vm1.features["boot-mode.standalone-default"] = "new-mode"
513+
template_rootvol = self.template.storage.get_volume("root")
514+
standalone_vm1_rootvol = self.standalone_vm1.storage.get_volume("root")
515+
await qubes.utils.coro_maybe(
516+
standalone_vm1_rootvol.import_volume(template_rootvol)
517+
)
518+
self.assertEqual(
519+
standalone_vm1.features["boot-mode.active"], "new-mode"
520+
)
521+
488522

489523
class StorageFile(StorageTestMixin, qubes.tests.SystemTestCase):
490524
def init_pool(self):

qubes/vm/qubesvm.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,15 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.LocalVM):
668668
:param event: Event name (``'domain-tag-delete:' tag``)
669669
:param tag: tag name
670670
671+
.. event:: domain-import-volume (subject, event, name, source)
672+
673+
A volume has been imported.
674+
675+
:param subject: Event emitter (the qube object)
676+
:param event: Event name (``'domain-import-volume'``)
677+
:param name: Destination volume name
678+
:param source: Source volume
679+
671680
.. event:: features-request (subject, event, *, untrusted_features)
672681
673682
The domain is performing a features request.

qubes/vm/standalonevm.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,15 @@ def __init__(self, *args, **kwargs):
6262
},
6363
}
6464
super().__init__(*args, **kwargs)
65+
66+
@qubes.events.handler("domain-import-volume")
67+
def on_domain_import_volume(self, event, name, source):
68+
# pylint: disable=unused-argument
69+
if name != "root":
70+
return
71+
if "boot-mode.standalone-default" in self.features:
72+
bootmode_value = self.features["boot-mode.standalone-default"]
73+
if bootmode_value == "default":
74+
self.features["boot-mode.active"] = "default"
75+
if f"boot-mode.kernelopts.{bootmode_value}" in self.features:
76+
self.features["boot-mode.active"] = bootmode_value

0 commit comments

Comments
 (0)