Skip to content

Commit bc4c000

Browse files
committed
Add boot mode support
Boot modes provide a way for VMs to advertise that they can have their behavior changed by booting them with a predefined combination of additional kernel parameters set. This allows qubes to offer features like a secure system maintenance mode in a way that works seemlessly with Qubes OS. This commit adds boot mode support to qubes-core-admin, providing API support for boot modes and allowing VMs to be booted with special kernel parameters specified by boot modes.
1 parent 5de215d commit bc4c000

9 files changed

Lines changed: 368 additions & 19 deletions

File tree

qubes/ext/core_features.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,95 @@ async def qubes_features_request(self, vm, event, untrusted_features):
104104
untrusted_value = untrusted_features["qubes-agent-version"]
105105
if _version_re.fullmatch(untrusted_value):
106106
vm.features["qubes-agent-version"] = untrusted_value
107+
108+
# handle boot mode advertisement
109+
old_bootmode_info = {}
110+
for feature_key, feature_val in vm.features.items():
111+
if (
112+
feature_key.startswith("boot-mode.kernelopts.")
113+
or feature_key.startswith("boot-mode.name.")
114+
):
115+
old_bootmode_info[feature_key] = feature_val
116+
new_bootmode_info = {}
117+
untrusted_sanitized_features = {}
118+
for (
119+
untrusted_feature_key,
120+
untrusted_feature_value,
121+
) in untrusted_features.items():
122+
if not all(c in string.printable for c in untrusted_feature_value):
123+
continue
124+
untrusted_sanitized_features[untrusted_feature_key] = (
125+
untrusted_feature_value
126+
)
127+
for (
128+
untrusted_feature_key,
129+
untrusted_feature_value,
130+
) in untrusted_sanitized_features.items():
131+
if untrusted_feature_key.startswith("boot-mode.kernelopts."):
132+
bootmode_name = untrusted_feature_key.split(".")[2]
133+
if bootmode_name == "":
134+
continue
135+
bootmode_feature = untrusted_feature_key
136+
bootmode_value = untrusted_feature_value
137+
new_bootmode_info[bootmode_feature] = bootmode_value
138+
for (
139+
untrusted_feature_key,
140+
untrusted_feature_value,
141+
) in untrusted_sanitized_features.items():
142+
if untrusted_feature_key.startswith("boot-mode.name."):
143+
bootmode_name = untrusted_feature_key.split(".")[2]
144+
if bootmode_name == "":
145+
continue
146+
if not (
147+
f"boot-mode.kernelopts.{bootmode_name}"
148+
in new_bootmode_info
149+
):
150+
continue
151+
bootmode_feature = untrusted_feature_key
152+
bootmode_value = untrusted_feature_value
153+
new_bootmode_info[bootmode_feature] = bootmode_value
154+
bootmode_count = len(old_bootmode_info)
155+
for feature_key in new_bootmode_info:
156+
if feature_key not in old_bootmode_info:
157+
bootmode_count += 1
158+
# Only allow a maximum of 64 boot modes, fail to add or remove any
159+
# boot modes if more than 64 would be defined in the end
160+
if bootmode_count <= 64:
161+
for feature_key, feature_val in new_bootmode_info.items():
162+
vm.features[feature_key] = feature_val
163+
# Prevent wiping all boot modes
164+
if new_bootmode_info:
165+
for feature_key in old_bootmode_info:
166+
if feature_key not in new_bootmode_info:
167+
del vm.features[feature_key]
168+
for (
169+
untrusted_feature_key,
170+
untrusted_feature_value,
171+
) in untrusted_sanitized_features.items():
172+
if untrusted_feature_key == "boot-mode.active":
173+
if not (
174+
f"boot-mode.kernelopts.{untrusted_feature_value}"
175+
in vm.features
176+
):
177+
continue
178+
if vm.bootmode != "":
179+
continue
180+
bootmode_value = untrusted_feature_value
181+
vm.bootmode = bootmode_value
182+
elif untrusted_feature_key == "boot-mode.appvm-default":
183+
if not (
184+
f"boot-mode.kernelopts.{untrusted_feature_value}"
185+
in vm.features
186+
):
187+
continue
188+
if not hasattr(vm, "appvm_default_bootmode"):
189+
continue
190+
if vm.appvm_default_bootmode != "":
191+
continue
192+
bootmode_value = untrusted_feature_value
193+
vm.appvm_default_bootmode = bootmode_value
107194
del untrusted_features
195+
del untrusted_sanitized_features
108196

109197
# default user for qvm-run etc
110198
# starting with Qubes 4.x ignored

qubes/ext/pci.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,7 @@ def pcidev_class(dev_xmldesc):
100100

101101

102102
def pcidev_interface(dev_xmldesc):
103-
sysfs_path = dev_xmldesc.findtext("path")
104-
assert sysfs_path
105-
try:
106-
with open(sysfs_path + "/class", encoding="ascii") as f_class:
107-
class_id = f_class.read().strip()
108-
except OSError:
109-
return "000000"
110-
103+
class_id = dev_xmldesc.xpath("capability[@type='pci']/class/text()")[0]
111104
if class_id.startswith("0x"):
112105
class_id = class_id[2:]
113106
return class_id

qubes/tests/ext.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def test_010_notify_tools(self):
7171
self.assertListEqual(
7272
self.vm.mock_calls,
7373
[
74+
("features.items", (), {}),
7475
("features.get", ("qrexec", False), {}),
7576
("features.__contains__", ("qrexec",), {}),
7677
("features.__setitem__", ("qrexec", True), {}),
@@ -100,6 +101,7 @@ def test_011_notify_tools_uninstall(self):
100101
self.assertListEqual(
101102
self.vm.mock_calls,
102103
[
104+
("features.items", (), {}),
103105
("features.get", ("qrexec", False), {}),
104106
("features.__contains__", ("qrexec",), {}),
105107
("features.__setitem__", ("qrexec", False), {}),
@@ -125,6 +127,7 @@ def test_012_notify_tools_uninstall2(self):
125127
self.assertListEqual(
126128
self.vm.mock_calls,
127129
[
130+
("features.items", (), {}),
128131
("features.get", ("qrexec", False), {}),
129132
("features.get", ("qrexec", False), {}),
130133
],
@@ -146,6 +149,7 @@ def test_013_notify_tools_no_version(self):
146149
self.assertListEqual(
147150
self.vm.mock_calls,
148151
[
152+
("features.items", (), {}),
149153
("features.get", ("qrexec", False), {}),
150154
("features.__contains__", ("qrexec",), {}),
151155
("features.__setitem__", ("qrexec", True), {}),
@@ -173,6 +177,7 @@ def test_015_notify_tools_invalid_value_qrexec(self):
173177
self.assertEqual(
174178
self.vm.mock_calls,
175179
[
180+
("features.items", (), {}),
176181
("features.get", ("qrexec", False), {}),
177182
("features.__contains__", ("gui",), {}),
178183
("features.__setitem__", ("gui", True), {}),
@@ -197,6 +202,7 @@ def test_016_notify_tools_invalid_value_gui(self):
197202
self.assertListEqual(
198203
self.vm.mock_calls,
199204
[
205+
("features.items", (), {}),
200206
("features.get", ("qrexec", False), {}),
201207
("features.__contains__", ("qrexec",), {}),
202208
("features.__setitem__", ("qrexec", True), {}),
@@ -249,6 +255,7 @@ def test_018_notify_tools_already_installed(self):
249255
self.assertListEqual(
250256
self.vm.mock_calls,
251257
[
258+
("features.items", (), {}),
252259
("features.get", ("qrexec", False), {}),
253260
("features.__contains__", ("qrexec",), {}),
254261
("features.__contains__", ("gui",), {}),
@@ -269,6 +276,7 @@ def test_20_version(self):
269276
self.vm.mock_calls,
270277
[
271278
("features.__setitem__", ("qubes-agent-version", "4.1"), {}),
279+
("features.items", (), {}),
272280
("features.get", ("qrexec", False), {}),
273281
],
274282
)
@@ -286,6 +294,7 @@ def test_21_version_invalid(self):
286294
self.assertListEqual(
287295
self.vm.mock_calls,
288296
[
297+
("features.items", (), {}),
289298
("features.get", ("qrexec", False), {}),
290299
],
291300
)
@@ -300,6 +309,7 @@ def test_21_version_invalid(self):
300309
self.assertListEqual(
301310
self.vm.mock_calls,
302311
[
312+
("features.items", (), {}),
303313
("features.get", ("qrexec", False), {}),
304314
],
305315
)
@@ -314,6 +324,7 @@ def test_21_version_invalid(self):
314324
self.assertListEqual(
315325
self.vm.mock_calls,
316326
[
327+
("features.items", (), {}),
317328
("features.get", ("qrexec", False), {}),
318329
],
319330
)
@@ -328,6 +339,7 @@ def test_21_version_invalid(self):
328339
self.assertListEqual(
329340
self.vm.mock_calls,
330341
[
342+
("features.items", (), {}),
331343
("features.get", ("qrexec", False), {}),
332344
],
333345
)
@@ -353,6 +365,7 @@ def test_30_distro_meta(self):
353365
("features.__setitem__", ("os-distribution", "debian"), {}),
354366
("features.__setitem__", ("os-version", "12"), {}),
355367
("features.__setitem__", ("os-eol", "2026-06-10"), {}),
368+
("features.items", (), {}),
356369
("features.get", ("qrexec", False), {}),
357370
],
358371
)
@@ -384,6 +397,7 @@ def test_031_distro_meta_ubuntu(self):
384397
),
385398
("features.__setitem__", ("os-version", "22.04"), {}),
386399
("features.__setitem__", ("os-eol", "2027-06-01"), {}),
400+
("features.items", (), {}),
387401
("features.get", ("qrexec", False), {}),
388402
],
389403
)
@@ -415,6 +429,7 @@ def test_032_distro_meta_invalid(self):
415429
),
416430
("log.warning", unittest.mock.ANY, {}),
417431
("log.warning", unittest.mock.ANY, {}),
432+
("features.items", (), {}),
418433
("features.get", ("qrexec", False), {}),
419434
],
420435
)
@@ -446,6 +461,7 @@ def test_033_distro_meta_invalid2(self):
446461
),
447462
("log.warning", unittest.mock.ANY, {}),
448463
("log.warning", unittest.mock.ANY, {}),
464+
("features.items", (), {}),
449465
("features.get", ("qrexec", False), {}),
450466
],
451467
)
@@ -469,6 +485,7 @@ def test_034_distro_meta_empty(self):
469485
self.assertListEqual(
470486
self.vm.mock_calls,
471487
[
488+
("features.items", (), {}),
472489
("features.get", ("qrexec", False), {}),
473490
],
474491
)

0 commit comments

Comments
 (0)