diff --git a/qubes/ext/core_features.py b/qubes/ext/core_features.py
index 92920d109..ddf1296c1 100644
--- a/qubes/ext/core_features.py
+++ b/qubes/ext/core_features.py
@@ -104,6 +104,86 @@ async def qubes_features_request(self, vm, event, untrusted_features):
untrusted_value = untrusted_features["qubes-agent-version"]
if _version_re.fullmatch(untrusted_value):
vm.features["qubes-agent-version"] = untrusted_value
+
+ # handle boot mode advertisement
+ old_bootmode_info = {}
+ for feature_key, feature_val in vm.features.items():
+ if feature_key.startswith(
+ "boot-mode.kernelopts."
+ ) or feature_key.startswith("boot-mode.name."):
+ old_bootmode_info[feature_key] = feature_val
+ new_bootmode_info = {}
+ new_bootmode_names = []
+ for (
+ untrusted_feature_key,
+ untrusted_feature_value,
+ ) in untrusted_features.items():
+ if untrusted_feature_key.startswith("boot-mode.kernelopts."):
+ bootmode_key_parts = untrusted_feature_key.split(".")
+ if len(bootmode_key_parts) != 3:
+ # Boot mode key contains unexpected data, reject it
+ continue
+ bootmode_name = bootmode_key_parts[2]
+ if bootmode_name == "":
+ continue
+ if bootmode_name == "default":
+ # "default" is reserved, cannot set kernelopts for it
+ continue
+ bootmode_feature = untrusted_feature_key
+ bootmode_value = untrusted_feature_value
+ new_bootmode_info[bootmode_feature] = bootmode_value
+ for (
+ untrusted_feature_key,
+ untrusted_feature_value,
+ ) in untrusted_features.items():
+ if untrusted_feature_key.startswith("boot-mode.name."):
+ bootmode_key_parts = untrusted_feature_key.split(".")
+ if len(bootmode_key_parts) != 3:
+ # Boot mode key contains unexpected data, reject it
+ continue
+ bootmode_name = bootmode_key_parts[2]
+ if bootmode_name == "":
+ continue
+ if (
+ f"boot-mode.kernelopts.{bootmode_name}"
+ not in new_bootmode_info
+ ) and bootmode_name != "default":
+ continue
+ bootmode_feature = untrusted_feature_key
+ bootmode_value = untrusted_feature_value
+ new_bootmode_info[bootmode_feature] = bootmode_value
+ new_bootmode_names.append(bootmode_value)
+ if (
+ # Disallow duplicate boot mode names
+ len(new_bootmode_names) == len(set(new_bootmode_names))
+ # Don't allow more than 64 boot modes
+ and len(new_bootmode_info) <= 64
+ # Don't allow wiping all boot modes
+ and len(new_bootmode_info) > 0
+ ):
+ for feature_key in old_bootmode_info:
+ if feature_key not in new_bootmode_info:
+ del vm.features[feature_key]
+ for feature_key, feature_val in new_bootmode_info.items():
+ vm.features[feature_key] = feature_val
+ if "boot-mode.active" in untrusted_features:
+ untrusted_feature_value = untrusted_features["boot-mode.active"]
+ if (
+ f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features
+ or untrusted_feature_value == "default"
+ ):
+ bootmode_value = untrusted_feature_value
+ vm.features["boot-mode.active"] = bootmode_value
+ if "boot-mode.appvm-default" in untrusted_features:
+ untrusted_feature_value = untrusted_features[
+ "boot-mode.appvm-default"
+ ]
+ if (
+ f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features
+ or untrusted_feature_value == "default"
+ ) and hasattr(vm, "appvm_default_bootmode"):
+ bootmode_value = untrusted_feature_value
+ vm.features["boot-mode.appvm-default"] = bootmode_value
del untrusted_features
# default user for qvm-run etc
diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py
index 7bf07ae19..7606d9281 100644
--- a/qubes/tests/ext.py
+++ b/qubes/tests/ext.py
@@ -72,6 +72,7 @@ def test_010_notify_tools(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("qrexec",), {}),
("features.__setitem__", ("qrexec", True), {}),
@@ -101,6 +102,7 @@ def test_011_notify_tools_uninstall(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("qrexec",), {}),
("features.__setitem__", ("qrexec", False), {}),
@@ -126,6 +128,7 @@ def test_012_notify_tools_uninstall2(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.get", ("qrexec", False), {}),
],
@@ -147,6 +150,7 @@ def test_013_notify_tools_no_version(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("qrexec",), {}),
("features.__setitem__", ("qrexec", True), {}),
@@ -174,6 +178,7 @@ def test_015_notify_tools_invalid_value_qrexec(self):
self.assertEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("gui",), {}),
("features.__setitem__", ("gui", True), {}),
@@ -198,6 +203,7 @@ def test_016_notify_tools_invalid_value_gui(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("qrexec",), {}),
("features.__setitem__", ("qrexec", True), {}),
@@ -250,6 +256,7 @@ def test_018_notify_tools_already_installed(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
("features.__contains__", ("qrexec",), {}),
("features.__contains__", ("gui",), {}),
@@ -270,6 +277,7 @@ def test_20_version(self):
self.vm.mock_calls,
[
("features.__setitem__", ("qubes-agent-version", "4.1"), {}),
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -287,6 +295,7 @@ def test_21_version_invalid(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -301,6 +310,7 @@ def test_21_version_invalid(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -315,6 +325,7 @@ def test_21_version_invalid(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -329,6 +340,7 @@ def test_21_version_invalid(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -354,6 +366,7 @@ def test_30_distro_meta(self):
("features.__setitem__", ("os-distribution", "debian"), {}),
("features.__setitem__", ("os-version", "12"), {}),
("features.__setitem__", ("os-eol", "2026-06-10"), {}),
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -385,6 +398,7 @@ def test_031_distro_meta_ubuntu(self):
),
("features.__setitem__", ("os-version", "22.04"), {}),
("features.__setitem__", ("os-eol", "2027-06-01"), {}),
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -416,6 +430,7 @@ def test_032_distro_meta_invalid(self):
),
("log.warning", unittest.mock.ANY, {}),
("log.warning", unittest.mock.ANY, {}),
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -447,6 +462,7 @@ def test_033_distro_meta_invalid2(self):
),
("log.warning", unittest.mock.ANY, {}),
("log.warning", unittest.mock.ANY, {}),
+ ("features.items", (), {}),
("features.get", ("qrexec", False), {}),
],
)
@@ -470,6 +486,1103 @@ def test_034_distro_meta_empty(self):
self.assertListEqual(
self.vm.mock_calls,
[
+ ("features.items", (), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_040_bootmode_noname(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_041_bootmode_withname(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_042_bootmode_extraname(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ "boot-mode.name.bad": "Bad Entry",
+ "boot-mode.name.vmreq2": "VMReq2",
+ "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq2", "VMReq2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_043_bootmode_extrakernelopts(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ "boot-mode.kernelopts.extra": "extra-opts",
+ "boot-mode.name.vmreq2": "VMReq2",
+ "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.extra", "extra-opts"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq2", "VMReq2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_044_bootmode_discard_old(self):
+ del self.vm.template
+ self.features["boot-mode.name.old"] = "Old"
+ self.features["boot-mode.kernelopts.old"] = "old1 old2"
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ ("features.__delitem__", ("boot-mode.name.old",), {}),
+ ("features.__delitem__", ("boot-mode.kernelopts.old",), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_045_bootmode_update_old(self):
+ del self.vm.template
+ self.features["boot-mode.name.old1"] = "Old1"
+ self.features["boot-mode.kernelopts.old1"] = "old1-1 old1-2"
+ self.features["boot-mode.name.old2"] = "Old2"
+ self.features["boot-mode.kernelopts.old2"] = "old2-1 old2-2"
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ "boot-mode.kernelopts.old1": "old-newer1-1 old-newer1-2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ ("features.__delitem__", ("boot-mode.name.old1",), {}),
+ ("features.__delitem__", ("boot-mode.name.old2",), {}),
+ ("features.__delitem__", ("boot-mode.kernelopts.old2",), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.old1", "old-newer1-1 old-newer1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_046_bootmode_flood1(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.kernelopts.garbage1": "garbage1",
+ "boot-mode.kernelopts.garbage2": "garbage2",
+ "boot-mode.kernelopts.garbage3": "garbage3",
+ "boot-mode.kernelopts.garbage4": "garbage4",
+ "boot-mode.kernelopts.garbage5": "garbage5",
+ "boot-mode.kernelopts.garbage6": "garbage6",
+ "boot-mode.kernelopts.garbage7": "garbage7",
+ "boot-mode.kernelopts.garbage8": "garbage8",
+ "boot-mode.kernelopts.garbage9": "garbage9",
+ "boot-mode.kernelopts.garbage10": "garbage10",
+ "boot-mode.kernelopts.garbage11": "garbage11",
+ "boot-mode.kernelopts.garbage12": "garbage12",
+ "boot-mode.kernelopts.garbage13": "garbage13",
+ "boot-mode.kernelopts.garbage14": "garbage14",
+ "boot-mode.kernelopts.garbage15": "garbage15",
+ "boot-mode.kernelopts.garbage16": "garbage16",
+ "boot-mode.kernelopts.garbage17": "garbage17",
+ "boot-mode.kernelopts.garbage18": "garbage18",
+ "boot-mode.kernelopts.garbage19": "garbage19",
+ "boot-mode.kernelopts.garbage20": "garbage20",
+ "boot-mode.kernelopts.garbage21": "garbage21",
+ "boot-mode.kernelopts.garbage22": "garbage22",
+ "boot-mode.kernelopts.garbage23": "garbage23",
+ "boot-mode.kernelopts.garbage24": "garbage24",
+ "boot-mode.kernelopts.garbage25": "garbage25",
+ "boot-mode.kernelopts.garbage26": "garbage26",
+ "boot-mode.kernelopts.garbage27": "garbage27",
+ "boot-mode.kernelopts.garbage28": "garbage28",
+ "boot-mode.kernelopts.garbage29": "garbage29",
+ "boot-mode.kernelopts.garbage30": "garbage30",
+ "boot-mode.kernelopts.garbage31": "garbage31",
+ "boot-mode.kernelopts.garbage32": "garbage32",
+ "boot-mode.kernelopts.garbage33": "garbage33",
+ "boot-mode.kernelopts.garbage34": "garbage34",
+ "boot-mode.kernelopts.garbage35": "garbage35",
+ "boot-mode.kernelopts.garbage36": "garbage36",
+ "boot-mode.kernelopts.garbage37": "garbage37",
+ "boot-mode.kernelopts.garbage38": "garbage38",
+ "boot-mode.kernelopts.garbage39": "garbage39",
+ "boot-mode.kernelopts.garbage40": "garbage40",
+ "boot-mode.kernelopts.garbage41": "garbage41",
+ "boot-mode.kernelopts.garbage42": "garbage42",
+ "boot-mode.kernelopts.garbage43": "garbage43",
+ "boot-mode.kernelopts.garbage44": "garbage44",
+ "boot-mode.kernelopts.garbage45": "garbage45",
+ "boot-mode.kernelopts.garbage46": "garbage46",
+ "boot-mode.kernelopts.garbage47": "garbage47",
+ "boot-mode.kernelopts.garbage48": "garbage48",
+ "boot-mode.kernelopts.garbage49": "garbage49",
+ "boot-mode.kernelopts.garbage50": "garbage50",
+ "boot-mode.kernelopts.garbage51": "garbage51",
+ "boot-mode.kernelopts.garbage52": "garbage52",
+ "boot-mode.kernelopts.garbage53": "garbage53",
+ "boot-mode.kernelopts.garbage54": "garbage54",
+ "boot-mode.kernelopts.garbage55": "garbage55",
+ "boot-mode.kernelopts.garbage56": "garbage56",
+ "boot-mode.kernelopts.garbage57": "garbage57",
+ "boot-mode.kernelopts.garbage58": "garbage58",
+ "boot-mode.kernelopts.garbage59": "garbage59",
+ "boot-mode.kernelopts.garbage60": "garbage60",
+ "boot-mode.kernelopts.garbage61": "garbage61",
+ "boot-mode.kernelopts.garbage62": "garbage62",
+ "boot-mode.kernelopts.garbage63": "garbage63",
+ "boot-mode.kernelopts.garbage64": "garbage64",
+ "boot-mode.kernelopts.garbage65": "garbage65",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_047_bootmode_flood2(self):
+ del self.vm.template
+ self.features["boot-mode.kernelopts.garbage1"] = "garbage1"
+ self.features["boot-mode.kernelopts.garbage2"] = "garbage2"
+ self.features["boot-mode.kernelopts.garbage3"] = "garbage3"
+ self.features["boot-mode.kernelopts.garbage4"] = "garbage4"
+ self.features["boot-mode.kernelopts.garbage5"] = "garbage5"
+ self.features["boot-mode.kernelopts.garbage6"] = "garbage6"
+ self.features["boot-mode.kernelopts.garbage7"] = "garbage7"
+ self.features["boot-mode.kernelopts.garbage8"] = "garbage8"
+ self.features["boot-mode.kernelopts.garbage9"] = "garbage9"
+ self.features["boot-mode.kernelopts.garbage10"] = "garbage10"
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.kernelopts.garbage11": "garbage11",
+ "boot-mode.kernelopts.garbage12": "garbage12",
+ "boot-mode.kernelopts.garbage13": "garbage13",
+ "boot-mode.kernelopts.garbage14": "garbage14",
+ "boot-mode.kernelopts.garbage15": "garbage15",
+ "boot-mode.kernelopts.garbage16": "garbage16",
+ "boot-mode.kernelopts.garbage17": "garbage17",
+ "boot-mode.kernelopts.garbage18": "garbage18",
+ "boot-mode.kernelopts.garbage19": "garbage19",
+ "boot-mode.kernelopts.garbage20": "garbage20",
+ "boot-mode.kernelopts.garbage21": "garbage21",
+ "boot-mode.kernelopts.garbage22": "garbage22",
+ "boot-mode.kernelopts.garbage23": "garbage23",
+ "boot-mode.kernelopts.garbage24": "garbage24",
+ "boot-mode.kernelopts.garbage25": "garbage25",
+ "boot-mode.kernelopts.garbage26": "garbage26",
+ "boot-mode.kernelopts.garbage27": "garbage27",
+ "boot-mode.kernelopts.garbage28": "garbage28",
+ "boot-mode.kernelopts.garbage29": "garbage29",
+ "boot-mode.kernelopts.garbage30": "garbage30",
+ "boot-mode.kernelopts.garbage31": "garbage31",
+ "boot-mode.kernelopts.garbage32": "garbage32",
+ "boot-mode.kernelopts.garbage33": "garbage33",
+ "boot-mode.kernelopts.garbage34": "garbage34",
+ "boot-mode.kernelopts.garbage35": "garbage35",
+ "boot-mode.kernelopts.garbage36": "garbage36",
+ "boot-mode.kernelopts.garbage37": "garbage37",
+ "boot-mode.kernelopts.garbage38": "garbage38",
+ "boot-mode.kernelopts.garbage39": "garbage39",
+ "boot-mode.kernelopts.garbage40": "garbage40",
+ "boot-mode.kernelopts.garbage41": "garbage41",
+ "boot-mode.kernelopts.garbage42": "garbage42",
+ "boot-mode.kernelopts.garbage43": "garbage43",
+ "boot-mode.kernelopts.garbage44": "garbage44",
+ "boot-mode.kernelopts.garbage45": "garbage45",
+ "boot-mode.kernelopts.garbage46": "garbage46",
+ "boot-mode.kernelopts.garbage47": "garbage47",
+ "boot-mode.kernelopts.garbage48": "garbage48",
+ "boot-mode.kernelopts.garbage49": "garbage49",
+ "boot-mode.kernelopts.garbage50": "garbage50",
+ "boot-mode.kernelopts.garbage51": "garbage51",
+ "boot-mode.kernelopts.garbage52": "garbage52",
+ "boot-mode.kernelopts.garbage53": "garbage53",
+ "boot-mode.kernelopts.garbage54": "garbage54",
+ "boot-mode.kernelopts.garbage55": "garbage55",
+ "boot-mode.kernelopts.garbage56": "garbage56",
+ "boot-mode.kernelopts.garbage57": "garbage57",
+ "boot-mode.kernelopts.garbage58": "garbage58",
+ "boot-mode.kernelopts.garbage59": "garbage59",
+ "boot-mode.kernelopts.garbage60": "garbage60",
+ "boot-mode.kernelopts.garbage61": "garbage61",
+ "boot-mode.kernelopts.garbage62": "garbage62",
+ "boot-mode.kernelopts.garbage63": "garbage63",
+ "boot-mode.kernelopts.garbage64": "garbage64",
+ "boot-mode.kernelopts.garbage65": "garbage65",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage1",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage2",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage3",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage4",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage5",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage6",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage7",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage8",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage9",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.garbage10",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage11", "garbage11"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage12", "garbage12"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage13", "garbage13"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage14", "garbage14"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage15", "garbage15"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage16", "garbage16"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage17", "garbage17"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage18", "garbage18"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage19", "garbage19"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage20", "garbage20"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage21", "garbage21"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage22", "garbage22"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage23", "garbage23"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage24", "garbage24"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage25", "garbage25"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage26", "garbage26"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage27", "garbage27"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage28", "garbage28"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage29", "garbage29"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage30", "garbage30"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage31", "garbage31"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage32", "garbage32"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage33", "garbage33"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage34", "garbage34"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage35", "garbage35"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage36", "garbage36"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage37", "garbage37"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage38", "garbage38"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage39", "garbage39"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage40", "garbage40"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage41", "garbage41"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage42", "garbage42"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage43", "garbage43"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage44", "garbage44"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage45", "garbage45"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage46", "garbage46"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage47", "garbage47"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage48", "garbage48"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage49", "garbage49"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage50", "garbage50"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage51", "garbage51"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage52", "garbage52"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage53", "garbage53"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage54", "garbage54"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage55", "garbage55"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage56", "garbage56"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage57", "garbage57"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage58", "garbage58"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage59", "garbage59"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage60", "garbage60"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage61", "garbage61"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage62", "garbage62"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage63", "garbage63"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage64", "garbage64"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.garbage65", "garbage65"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_048_bootmode_defaultname(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ "boot-mode.name.default": "No Params",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.default", "No Params"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_049_bootmode_multidots(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ "boot-mode.name.vmreq.two": "VMReq Two",
+ "boot-mode.kernelopts.vmreq.two": "vmreqtwo1 vmreqtwo2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_050_bootmode_noid(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ "boot-mode.name.": "Ghost",
+ "boot-mode.kernelopts.": "ghost",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_051_bootmode_defaultkernelopts(self):
+ del self.vm.template
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ "boot-mode.kernelopts.default": "default_kernelopts",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_052_bootmode_active_appvm_default(self):
+ del self.vm.template
+ self.vm.bootmode = ""
+ self.vm.appvm_default_bootmode = ""
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ "boot-mode.name.vmreq2": "VMReq2",
+ "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2",
+ "boot-mode.active": "vmreq1",
+ "boot-mode.appvm-default": "vmreq2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq2", "VMReq2"),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.vmreq1",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.active", "vmreq1"),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.vmreq2",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.appvm-default", "vmreq2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_053_bootmode_active_appvm_default_mod(self):
+ del self.vm.template
+ self.vm.bootmode = ""
+ self.vm.appvm_default_bootmode = ""
+ self.features["boot-mode.kernelopts.old1"] = "oldopts1"
+ self.features["boot-mode.active"] = "old1"
+ self.features["boot-mode.kernelopts.old2"] = "oldopts2"
+ self.features["boot-mode.appvm-default"] = "old2"
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq1": "VMReq1",
+ "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2",
+ "boot-mode.name.vmreq2": "VMReq2",
+ "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2",
+ "boot-mode.active": "vmreq1",
+ "boot-mode.appvm-default": "vmreq2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.old1",),
+ {},
+ ),
+ (
+ "features.__delitem__",
+ ("boot-mode.kernelopts.old2",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq1", "VMReq1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq2", "VMReq2"),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.vmreq1",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.active", "vmreq1"),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.vmreq2",),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.appvm-default", "vmreq2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_054_bootmode_active_appvm_default_wrong(self):
+ del self.vm.template
+ self.vm.bootmode = ""
+ self.vm.appvm_default_bootmode = ""
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.vmreq": "VMReq",
+ "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
+ "boot-mode.active": "nonexistent1",
+ "boot-mode.appvm-default": "nonexistent2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.vmreq", "VMReq"),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.nonexistent1",),
+ {},
+ ),
+ (
+ "features.__contains__",
+ ("boot-mode.kernelopts.nonexistent2",),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
+ ("features.get", ("qrexec", False), {}),
+ ],
+ )
+
+ def test_055_bootmode_preserve_oldvals(self):
+ del self.vm.template
+ self.features["boot-mode.name.old1"] = "Old1"
+ self.features["boot-mode.kernelopts.old1"] = "oldopts1"
+ self.features["boot-mode.name.old2"] = "Old2"
+ self.features["boot-mode.kernelopts.old2"] = "oldopts2"
+ self.loop.run_until_complete(
+ self.ext.qubes_features_request(
+ self.vm,
+ "features-request",
+ untrusted_features={
+ "boot-mode.name.old1": "Old1",
+ "boot-mode.kernelopts.old1": "oldopts1",
+ "boot-mode.name.old2": "Old2",
+ "boot-mode.kernelopts.old2": "oldopts2",
+ },
+ )
+ )
+ self.assertListEqual(
+ self.vm.mock_calls,
+ [
+ ("features.items", (), {}),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.old1", "oldopts1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.kernelopts.old2", "oldopts2"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.old1", "Old1"),
+ {},
+ ),
+ (
+ "features.__setitem__",
+ ("boot-mode.name.old2", "Old2"),
+ {},
+ ),
+ ("features.get", ("qrexec", False), {}),
("features.get", ("qrexec", False), {}),
],
)
diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py
index 42b7fec12..09bc4f36f 100644
--- a/qubes/tests/vm/qubesvm.py
+++ b/qubes/tests/vm/qubesvm.py
@@ -3095,3 +3095,47 @@ def test_801_ordering(self):
assert qubes.vm.qubesvm.QubesVM(
self.app, None, qid=1, name="bogus"
) > qubes.vm.adminvm.AdminVM(self.app, None)
+
+ def test_810_bootmode_kernelopts(self):
+ vm = self.get_vm(cls=qubes.vm.appvm.AppVM)
+ vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM)
+ vm.bootmode = qubes.property.DEFAULT
+ self.assertEqual(vm.bootmode_kernelopts, "")
+ vm.features["boot-mode.kernelopts.testmode1"] = "abc def"
+ vm.bootmode = "testmode1"
+ self.assertEqual(vm.bootmode_kernelopts, " abc def")
+ del vm.features["boot-mode.kernelopts.testmode1"]
+ self.assertEqual(vm.bootmode_kernelopts, "")
+ vm.template.features["boot-mode.kernelopts.testmode2"] = "ghi jkl"
+ vm.template.appvm_default_bootmode = "testmode2"
+ vm.bootmode = "nonexistent"
+ self.assertEqual(vm.bootmode_kernelopts, " ghi jkl")
+ del vm.template.features["boot-mode.kernelopts.testmode2"]
+ self.assertEqual(vm.bootmode_kernelopts, "")
+
+ def test_811_default_bootmode(self):
+ vm = self.get_vm(cls=qubes.vm.appvm.AppVM)
+ vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM)
+ vm.bootmode = qubes.property.DEFAULT
+ self.assertEqual(vm.bootmode, "default")
+ vm.features["boot-mode.active"] = "default"
+ self.assertEqual(vm.bootmode, "default")
+ vm.features["boot-mode.active"] = "testmode1"
+ vm.template.features["boot-mode.kernelopts.testmode1"] = "abc def"
+ self.assertEqual(vm.bootmode, "testmode1")
+ vm.features["boot-mode.active"] = "testmode1"
+ self.assertEqual(vm.bootmode, "testmode1")
+ del vm.template.features["boot-mode.kernelopts.testmode1"]
+ self.assertEqual(vm.bootmode, "default")
+ vm.template.features["boot-mode.appvm-default"] = "testmode2"
+ vm.template.features["boot-mode.kernelopts.testmode2"] = "ghi jkl"
+ self.assertEqual(vm.bootmode, "testmode2")
+ vm.template.features["boot-mode.appvm-default"] = "testmode2"
+ self.assertEqual(vm.bootmode, "testmode2")
+ del vm.template.features["boot-mode.kernelopts.testmode2"]
+ self.assertEqual(vm.bootmode, "default")
+ vm.template.features["boot-mode.kernelopts.testmode3"] = "mno pqr"
+ vm.template.appvm_default_bootmode = "testmode3"
+ self.assertEqual(vm.bootmode, "testmode3")
+ del vm.template.features["boot-mode.kernelopts.testmode3"]
+ self.assertEqual(vm.bootmode, "default")
diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py
index c9c159c78..f8afe1681 100644
--- a/qubes/vm/qubesvm.py
+++ b/qubes/vm/qubesvm.py
@@ -216,6 +216,36 @@ def _setter_kbd_layout(self, prop, value):
return value
+def _default_bootmode(self):
+ """
+ Return the VM's default bootmode. If that doesn't exist, return the
+ template's default bootmode for AppVMs, and if that doesn't exist return
+ the special value "default".
+ """
+ if "boot-mode.active" in self.features:
+ bootmode_value = self.features["boot-mode.active"]
+ if bootmode_value == "default":
+ return "default"
+ kernelopts = self.features.check_with_template(
+ f"boot-mode.kernelopts.{bootmode_value}", None
+ )
+ if kernelopts is not None:
+ return bootmode_value
+ subject = self
+ while hasattr(subject, "template"):
+ if hasattr(subject.template, "appvm_default_bootmode"):
+ bootmode_value = subject.template.appvm_default_bootmode
+ if bootmode_value == "default":
+ return "default"
+ kernelopts = subject.features.check_with_template(
+ f"boot-mode.kernelopts.{bootmode_value}", None
+ )
+ if kernelopts is not None:
+ return bootmode_value
+ subject = subject.template
+ return "default"
+
+
def _default_virt_mode(self):
if list(self.devices["pci"].get_assigned_devices()):
return "hvm"
@@ -660,6 +690,14 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
#
# properties loaded from XML
#
+ bootmode = qubes.property(
+ "bootmode",
+ type=str,
+ load_stage=4,
+ default=_default_bootmode,
+ doc="Active boot mode for this domain",
+ )
+
guivm = qubes.VMProperty(
"guivm",
load_stage=4,
@@ -954,6 +992,24 @@ def attached_volumes(self):
return result + list(self.volumes.values())
+ @property
+ def bootmode_kernelopts(self):
+ if self.bootmode == "default":
+ return ""
+ kernelopts = self.features.check_with_template(
+ f"boot-mode.kernelopts.{self.bootmode}", None
+ )
+ if kernelopts is None:
+ default_bootmode = _default_bootmode(self)
+ if default_bootmode == "default":
+ return ""
+ kernelopts = self.features.check_with_template(
+ f"boot-mode.kernelopts.{default_bootmode}", None
+ )
+ if kernelopts is None:
+ return ""
+ return f" {kernelopts}"
+
@property
def libvirt_domain(self):
"""Libvirt domain object from libvirt.
@@ -1147,6 +1203,16 @@ def on_domain_init_loaded(self, event):
self._domain_stopped_event_received = False
self._domain_stopped_event_handled = False
+ @qubes.events.handler("domain-feature-set:boot-mode.active")
+ def on_feature_bootmode_active_set(
+ self, event, feature, value, oldvalue=None
+ ):
+ # pylint: disable=unused-argument
+ if value == oldvalue:
+ return
+ if self.property_is_default("bootmode"):
+ self.fire_event("property-reset:bootmode", name="bootmode")
+
@qubes.events.handler("property-set:label")
def on_property_set_label(self, event, name, newvalue, oldvalue=None):
# pylint: disable=unused-argument
diff --git a/qubes/vm/templatevm.py b/qubes/vm/templatevm.py
index 0561cf11b..3ff93637a 100644
--- a/qubes/vm/templatevm.py
+++ b/qubes/vm/templatevm.py
@@ -29,6 +29,12 @@
from qubes.vm.qubesvm import QubesVM
+def _default_appvm_default_bootmode(self):
+ if "boot-mode.appvm-default" in self.features:
+ return self.features["boot-mode.appvm-default"]
+ return "default"
+
+
class TemplateVM(QubesVM):
"""Template for AppVM"""
@@ -43,6 +49,14 @@ def appvms(self):
if hasattr(vm, "template") and vm.template is self:
yield vm
+ appvm_default_bootmode = qubes.property(
+ "appvm_default_bootmode",
+ type=str,
+ load_stage=4,
+ default=_default_appvm_default_bootmode,
+ doc="Default active bootmode for AppVMs based on this template",
+ )
+
netvm = qubes.VMProperty(
"netvm",
load_stage=4,
@@ -94,6 +108,22 @@ def __init__(self, *args, **kwargs):
}
super().__init__(*args, **kwargs)
+ @qubes.events.handler("domain-feature-set:boot-mode.appvm-default")
+ def on_feature_bootmode_appvm_set(
+ self, event, feature, value, oldvalue=None
+ ):
+ # pylint: disable=unused-argument
+ if value == oldvalue:
+ return
+ if self.property_is_default("appvm_default_bootmode"):
+ self.fire_event(
+ "property-reset:appvm_default_bootmode",
+ name="appvm_default_bootmode",
+ )
+ for appvm in getattr(self, "appvms", []):
+ if appvm.property_is_default("bootmode"):
+ appvm.fire_event("property-reset:bootmode", name="bootmode")
+
@qubes.events.handler(
"property-set:default_user",
"property-set:kernel",
diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml
index 3d18136e5..ec56214c3 100644
--- a/templates/libvirt/xen.xml
+++ b/templates/libvirt/xen.xml
@@ -62,13 +62,13 @@
{% endif %}
{% if vm.kernel %}
{% if vm.features.check_with_template('no-default-kernelopts', False) -%}
- {{ vm.kernelopts }}
+ {{ vm.kernelopts }}{{ vm.bootmode_kernelopts }}
{% elif vm.features.check_with_template('apparmor', '0') == '1' -%}
- {{ vm.kernelopts_common }}{{ vm.kernelopts }} apparmor=1 security=apparmor
+ {{ vm.kernelopts_common }}{{ vm.kernelopts }} apparmor=1 security=apparmor{{ vm.bootmode_kernelopts }}
{% elif vm.features.check_with_template('selinux', '0') == '1' -%}
- {{ vm.kernelopts_common }}{{ vm.kernelopts }} selinux=1 security=selinux
+ {{ vm.kernelopts_common }}{{ vm.kernelopts }} selinux=1 security=selinux{{ vm.bootmode_kernelopts }}
{% else -%}
- {{ vm.kernelopts_common }}{{ vm.kernelopts }}
+ {{ vm.kernelopts_common }}{{ vm.kernelopts }}{{ vm.bootmode_kernelopts }}
{% endif -%}
{% endif %}
{% endblock %}