Skip to content

Commit a34e979

Browse files
committed
Allow VM boot modes to change a VM's default user
1 parent e7d49e3 commit a34e979

File tree

5 files changed

+192
-18
lines changed

5 files changed

+192
-18
lines changed

qubes/ext/core_features.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,11 @@ async def qubes_features_request(self, vm, event, untrusted_features):
108108
# handle boot mode advertisement
109109
old_bootmode_info = {}
110110
for feature_key, feature_val in vm.features.items():
111-
if feature_key.startswith(
112-
"boot-mode.kernelopts."
113-
) or feature_key.startswith("boot-mode.name."):
111+
if (
112+
feature_key.startswith("boot-mode.kernelopts.")
113+
or feature_key.startswith("boot-mode.name.")
114+
or feature_key.startswith("boot-mode.default-user.")
115+
):
114116
old_bootmode_info[feature_key] = feature_val
115117
new_bootmode_info = {}
116118
new_bootmode_names = []
@@ -136,18 +138,26 @@ async def qubes_features_request(self, vm, event, untrusted_features):
136138
untrusted_feature_key,
137139
untrusted_feature_value,
138140
) in untrusted_features.items():
139-
if untrusted_feature_key.startswith("boot-mode.name."):
140-
bootmode_key_parts = untrusted_feature_key.split(".")
141-
if len(bootmode_key_parts) != 3:
142-
# Boot mode key contains unexpected data, reject it
143-
continue
144-
bootmode_name = bootmode_key_parts[2]
145-
if bootmode_name == "":
146-
continue
141+
if not untrusted_feature_key.startswith("boot-mode."):
142+
continue
143+
bootmode_key_parts = untrusted_feature_key.split(".")
144+
if len(bootmode_key_parts) != 3:
145+
# Boot mode key contains unexpected data, reject it
146+
continue
147+
bootmode_name = bootmode_key_parts[2]
148+
if bootmode_name == "":
149+
continue
150+
if (
151+
f"boot-mode.kernelopts.{bootmode_name}" not in new_bootmode_info
152+
) and bootmode_name != "default":
153+
continue
154+
if untrusted_feature_key.startswith(
155+
"boot-mode.name."
156+
) or untrusted_feature_key.startswith("boot-mode.default-user."):
147157
if (
148-
f"boot-mode.kernelopts.{bootmode_name}"
149-
not in new_bootmode_info
150-
) and bootmode_name != "default":
158+
untrusted_feature_key.startswith("boot-mode.default-user.")
159+
and bootmode_name == "default"
160+
):
151161
continue
152162
bootmode_feature = untrusted_feature_key
153163
bootmode_value = untrusted_feature_value

qubes/tests/ext.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,95 @@ def test_055_bootmode_preserve_oldvals(self):
15871587
],
15881588
)
15891589

1590+
def test_056_bootmode_default_user(self):
1591+
del self.vm.template
1592+
self.loop.run_until_complete(
1593+
self.ext.qubes_features_request(
1594+
self.vm,
1595+
"features-request",
1596+
untrusted_features={
1597+
"boot-mode.name.vmreq": "VMReq",
1598+
"boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
1599+
"boot-mode.default-user.vmreq": "altuser",
1600+
},
1601+
)
1602+
)
1603+
self.assertListEqual(
1604+
self.vm.mock_calls,
1605+
[
1606+
("features.items", (), {}),
1607+
(
1608+
"features.__setitem__",
1609+
("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
1610+
{},
1611+
),
1612+
(
1613+
"features.__setitem__",
1614+
("boot-mode.name.vmreq", "VMReq"),
1615+
{},
1616+
),
1617+
(
1618+
"features.__setitem__",
1619+
("boot-mode.default-user.vmreq", "altuser"),
1620+
{},
1621+
),
1622+
("features.get", ("qrexec", False), {}),
1623+
("features.get", ("qrexec", False), {}),
1624+
],
1625+
)
1626+
1627+
def test_056_bootmode_default_user_mismatch(self):
1628+
del self.vm.template
1629+
self.loop.run_until_complete(
1630+
self.ext.qubes_features_request(
1631+
self.vm,
1632+
"features-request",
1633+
untrusted_features={
1634+
"boot-mode.name.vmreq": "VMReq",
1635+
"boot-mode.kernelopts.vmreq": "vmreq1 vmreq2",
1636+
"boot-mode.default-user.nope": "altuser",
1637+
},
1638+
)
1639+
)
1640+
self.assertListEqual(
1641+
self.vm.mock_calls,
1642+
[
1643+
("features.items", (), {}),
1644+
(
1645+
"features.__setitem__",
1646+
("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"),
1647+
{},
1648+
),
1649+
(
1650+
"features.__setitem__",
1651+
("boot-mode.name.vmreq", "VMReq"),
1652+
{},
1653+
),
1654+
("features.get", ("qrexec", False), {}),
1655+
("features.get", ("qrexec", False), {}),
1656+
],
1657+
)
1658+
1659+
def test_057_bootmode_default_user_default_bootmode(self):
1660+
del self.vm.template
1661+
self.loop.run_until_complete(
1662+
self.ext.qubes_features_request(
1663+
self.vm,
1664+
"features-request",
1665+
untrusted_features={
1666+
"boot-mode.default-user.default": "altuser",
1667+
},
1668+
)
1669+
)
1670+
self.assertListEqual(
1671+
self.vm.mock_calls,
1672+
[
1673+
("features.items", (), {}),
1674+
("features.get", ("qrexec", False), {}),
1675+
("features.get", ("qrexec", False), {}),
1676+
],
1677+
)
1678+
15901679
def test_100_servicevm_feature(self):
15911680
self.vm.provides_network = True
15921681
self.ext.set_servicevm_feature(self.vm)

qubes/tests/integ/basic.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,30 @@ async def _test_bootmode(self, tpl, vm):
513513
else:
514514
self.assertIn("opt1=val1", cmdline)
515515

516+
async def _test_bootmode_default_user(self, vm):
517+
await vm.start()
518+
await vm.run_for_stdio(
519+
"cat > /etc/qubes/post-install.d/50-test.sh",
520+
input=b"""#!/bin/sh
521+
522+
qvm-features-request boot-mode.kernelopts.defuser="opt2"
523+
qvm-features-request boot-mode.name.defuser="Mode with default user"
524+
qvm-features-request boot-mode.default-user.defuser="altuser"
525+
qvm-features-request boot-mode.active="defuser"
526+
""",
527+
user="root",
528+
)
529+
await vm.run_for_stdio("useradd -m altuser", user="root")
530+
await vm.run_for_stdio(
531+
"chmod +x " "/etc/qubes/post-install.d/50-test.sh", user="root"
532+
)
533+
await vm.run_service_for_stdio("qubes.PostInstall", user="root")
534+
await vm.shutdown(wait=True)
535+
536+
await vm.start()
537+
user_id = (await vm.run_for_stdio("id -un"))[0].decode()
538+
self.assertEqual(user_id.strip(), "altuser")
539+
516540
def test_210_bootmode_template(self):
517541
self.test_template = self.app.add_new_vm(
518542
qubes.vm.templatevm.TemplateVM,
@@ -551,6 +575,20 @@ def test_211_bootmode_standalone(self):
551575
self.app.save()
552576
self.loop.run_until_complete(self._test_bootmode(self.vm, self.vm))
553577

578+
def test_212_bootmode_default_user(self):
579+
self.vm = self.app.add_new_vm(
580+
qubes.vm.standalonevm.StandaloneVM,
581+
name=self.make_vm_name("vm"),
582+
label="red",
583+
)
584+
self.vm.clone_properties(self.app.default_template)
585+
self.vm.features.update(self.app.default_template.features)
586+
self.loop.run_until_complete(
587+
self.vm.clone_disk_files(self.app.default_template)
588+
)
589+
self.app.save()
590+
self.loop.run_until_complete(self._test_bootmode_default_user(self.vm))
591+
554592

555593
class TC_01_Properties(qubes.tests.SystemTestCase):
556594
# pylint: disable=attribute-defined-outside-init

qubes/tests/vm/qubesvm.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,3 +3232,25 @@ def test_811_default_bootmode(self):
32323232
self.assertEqual(vm.bootmode, "testmode3")
32333233
del vm.template.features["boot-mode.kernelopts.testmode3"]
32343234
self.assertEqual(vm.bootmode, "default")
3235+
3236+
def test_812_bootmode_default_user(self):
3237+
vm = self.get_vm(cls=qubes.vm.appvm.AppVM)
3238+
vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM)
3239+
vm.bootmode = qubes.property.DEFAULT
3240+
self.assertEqual(vm.get_default_user(), "user")
3241+
vm.features["boot-mode.kernelopts.testmode1"] = "abc def"
3242+
vm.features["boot-mode.default-user.testmode1"] = "altuser"
3243+
vm.features["boot-mode.active"] = "testmode1"
3244+
self.assertEqual(vm.get_default_user(), "altuser")
3245+
del vm.features["boot-mode.default-user.testmode1"]
3246+
self.assertEqual(vm.get_default_user(), "user")
3247+
vm.features["boot-mode.default-user.testmode1"] = "altuser"
3248+
del vm.features["boot-mode.kernelopts.testmode1"]
3249+
self.assertEqual(vm.get_default_user(), "user")
3250+
del vm.features["boot-mode.default-user.testmode1"]
3251+
vm.template.features["boot-mode.kernelopts.testmode2"] = "ghi jkl"
3252+
vm.template.features["boot-mode.default-user.testmode2"] = "altuser2"
3253+
vm.features["boot-mode.active"] = "testmode2"
3254+
self.assertEqual(vm.get_default_user(), "altuser2")
3255+
del vm.features["boot-mode.active"]
3256+
self.assertEqual(vm.get_default_user(), "user")

qubes/vm/qubesvm.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,7 +1807,7 @@ async def run_service(
18071807
name = self.name + "-dm" if stubdom else self.name
18081808

18091809
if user is None:
1810-
user = self.default_user
1810+
user = self.get_default_user()
18111811

18121812
if self.is_paused():
18131813
# XXX what about autostart?
@@ -1876,7 +1876,7 @@ async def run(self, command, user=None, **kwargs):
18761876
""" # pylint: disable=redefined-builtin
18771877

18781878
if user is None:
1879-
user = self.default_user
1879+
user = self.get_default_user()
18801880

18811881
return await asyncio.create_subprocess_exec(
18821882
qubes.config.system_path["qrexec_client_path"],
@@ -2106,7 +2106,7 @@ async def start_qrexec_daemon(self, stubdom=False):
21062106
"--",
21072107
str(self.xid),
21082108
self.name,
2109-
self.default_user,
2109+
self.get_default_user(),
21102110
]
21112111

21122112
if not self.debug:
@@ -2274,6 +2274,21 @@ def libvirt_undefine(self):
22742274

22752275
# state of the machine
22762276

2277+
def get_default_user(self):
2278+
"""Return a user account name suitable for use as a default user.
2279+
2280+
Usually returns the value of the default_user property, but also
2281+
allows boot modes to override the default user.
2282+
"""
2283+
if self.bootmode == "default":
2284+
return self.default_user
2285+
bootmode_default_user = self.features.check_with_template(
2286+
f"boot-mode.default-user.{self.bootmode}", None
2287+
)
2288+
if bootmode_default_user is None:
2289+
return self.default_user
2290+
return bootmode_default_user
2291+
22772292
def get_power_state(self):
22782293
"""Return power state description string.
22792294
@@ -2726,7 +2741,7 @@ def create_qdb_entries(self):
27262741

27272742
self.untrusted_qdb.write("/name", self.name)
27282743
self.untrusted_qdb.write("/type", self.__class__.__name__)
2729-
self.untrusted_qdb.write("/default-user", self.default_user)
2744+
self.untrusted_qdb.write("/default-user", self.get_default_user())
27302745
self.untrusted_qdb.write("/qubes-vm-updateable", str(self.updateable))
27312746
self.untrusted_qdb.write(
27322747
"/qubes-vm-persistence", "full" if self.updateable else "rw-only"

0 commit comments

Comments
 (0)