Skip to content

Commit f03fe7b

Browse files
committed
Discard preloaded disposables with outdated props
If a qube property is changed on the disposable template, it is not replicated to the preloaded disposable, no refresh occurs, which means, if a disposable template has the netvm changed, the preloaded disposable would still remain with the old setting. While changing netvm on the fly is possible after unpause, several other settings requires a restart. Fixes: QubesOS/qubes-issues#10525 For: QubesOS/qubes-issues#1512
1 parent 7562aaf commit f03fe7b

5 files changed

Lines changed: 199 additions & 24 deletions

File tree

qubes/tests/integ/dispvm.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,8 +832,29 @@ async def _test_019_preload_refresh(self):
832832
self.log_preload()
833833
logger.info("end")
834834

835+
def test_020_preload_discard_outdated(self):
836+
"""Discard preload if properties differ from the disposable template."""
837+
self.loop.run_until_complete(self._test_020_preload_discard_outdated())
838+
839+
async def _test_020_preload_discard_outdated(self):
840+
logger.info("start")
841+
self.log_preload()
842+
preload_max = 1
843+
self.disp_base.features["preload-dispvm-max"] = str(preload_max)
844+
await self.wait_preload(preload_max)
845+
preload_dispvm = self.disp_base.get_feat_preload()
846+
self.disp_base.netvm = None
847+
try:
848+
dispvm = await asyncio.wait_for(
849+
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base), 30
850+
)
851+
self.assertNotIn(dispvm.name, preload_dispvm)
852+
finally:
853+
await dispvm.cleanup()
854+
logger.info("end")
855+
835856
@unittest.skipUnless(which("xdotool"), "xdotool not installed")
836-
def test_020_gui_app(self):
857+
def test_080_gui_app(self):
837858
dispvm = self.loop.run_until_complete(
838859
qubes.vm.dispvm.DispVM.from_appvm(self.disp_base)
839860
)
@@ -1057,7 +1078,7 @@ def _whonix_ws_dispvm_confirm(self, action_str):
10571078
return (True, "")
10581079

10591080
@unittest.skipUnless(which("xdotool"), "xdotool not installed")
1060-
def test_030_edit_file(self):
1081+
def test_090_edit_file(self):
10611082
self.testvm1 = self.app.add_new_vm(
10621083
qubes.vm.appvm.AppVM,
10631084
name=self.make_vm_name("vm1"),

qubes/tests/vm/dispvm.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ def test_000_from_appvm_preload_use(
181181
mock_symlink,
182182
mock_start,
183183
):
184+
# pylint: disable=unused-argument
184185
mock_storage.return_value.create.side_effect = self.mock_coro
185186
mock_start.side_effect = self.mock_coro
186187
self.appvm.template_for_dispvms = True
@@ -222,8 +223,10 @@ def test_000_from_appvm_preload_use(
222223
mock_qube.features = dispvm.features
223224
mock_qube.unpause = self.mock_coro
224225
mock_qube.request_preload.return_value = dispvm
226+
mock_qube.is_preload_outdated = dispvm.is_preload_outdated
225227
mock_qube.get_preload = mock.AsyncMock()
226228
mock_qube.volumes = {}
229+
dispvm.volume_config = self.appvm.volume_config
227230
fresh_dispvm = self.loop.run_until_complete(
228231
qubes.vm.dispvm.DispVM.from_appvm(self.appvm)
229232
)
@@ -656,3 +659,65 @@ def test_023_inherit_ephemeral(self, _mock_makedirs, _mock_symlink):
656659
self.loop.run_until_complete(dispvm.create_on_disk())
657660
self.assertIs(dispvm.template, self.appvm)
658661
self.assertTrue(dispvm.volumes["volatile"].ephemeral)
662+
663+
@mock.patch("qubes.vm.qubesvm.QubesVM.start")
664+
@mock.patch("os.symlink")
665+
@mock.patch("os.makedirs")
666+
@mock.patch("qubes.storage.Storage")
667+
def test_024_is_preload_outdated(
668+
self,
669+
mock_storage,
670+
mock_makedirs,
671+
mock_symlink,
672+
mock_start,
673+
):
674+
mock_storage.return_value.create.side_effect = self.mock_coro
675+
mock_makedirs.return_value = self.mock_coro
676+
mock_symlink.return_value = self.mock_coro
677+
mock_start.side_effect = self.mock_coro
678+
self.appvm.template_for_dispvms = True
679+
680+
self.appvm.features["supported-rpc.qubes.WaitForRunningSystem"] = True
681+
self.appvm.features["preload-dispvm-max"] = "1"
682+
orig_getitem = self.app.domains.__getitem__
683+
with mock.patch.object(
684+
self.app, "domains", wraps=self.app.domains
685+
) as mock_domains:
686+
mock_qube = mock.Mock()
687+
mock_qube.template = self.appvm
688+
mock_qube.qrexec_timeout = self.appvm.qrexec_timeout
689+
mock_qube.preload_complete = mock.Mock(spec=asyncio.Event)
690+
mock_qube.preload_complete.is_set.return_value = True
691+
mock_qube.preload_complete.set = self.mock_coro
692+
mock_qube.preload_complete.clear = self.mock_coro
693+
mock_qube.preload_complete.wait = self.mock_coro
694+
mock_domains.configure_mock(
695+
**{
696+
"get_new_unused_dispid": mock.Mock(return_value=42),
697+
"__contains__.return_value": True,
698+
"__getitem__.side_effect": lambda key: (
699+
mock_qube if key == "disp42" else orig_getitem(key)
700+
),
701+
}
702+
)
703+
dispvm = self.loop.run_until_complete(
704+
qubes.vm.dispvm.DispVM.from_appvm(self.appvm, preload=True)
705+
)
706+
dispvm.volume_config = self.appvm.volume_config
707+
708+
self.assertFalse(dispvm.is_preload_outdated())
709+
self.appvm.debug = not self.appvm.debug
710+
self.assertEqual(
711+
dispvm.is_preload_outdated(), {"properties": ["debug"]}
712+
)
713+
self.appvm.debug = not self.appvm.debug
714+
715+
self.assertFalse(dispvm.is_preload_outdated())
716+
self.appvm_alt.provides_network = True
717+
self.assertFalse(dispvm.is_preload_outdated())
718+
self.appvm.netvm = self.appvm_alt
719+
self.assertIn("properties", dispvm.is_preload_outdated().keys())
720+
self.assertEqual(
721+
sorted(dispvm.is_preload_outdated()["properties"]),
722+
sorted(["netvm", "dns", "visible_netmask"]),
723+
)

qubes/vm/dispvm.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@
3131
import qubes.vm.appvm
3232
import qubes.vm.qubesvm
3333

34+
PRELOAD_OUTDATED_IGNORED_PROPERTIES = [
35+
"autostart",
36+
"backup_timestamp",
37+
"default_dispvm",
38+
"dispid",
39+
"gateway",
40+
"gateway6",
41+
"icon",
42+
"include_in_backups",
43+
"installed_by_rpm",
44+
"ip",
45+
"ip6",
46+
"klass",
47+
"name",
48+
"qid",
49+
"start_time",
50+
"stubdom_uuid",
51+
"stubdom_xid",
52+
"template",
53+
"template_for_dispvms",
54+
"updateable",
55+
"uuid",
56+
"visible_gateway",
57+
"visible_gateway6",
58+
"visible_ip",
59+
"visible_ip6",
60+
"xid",
61+
]
62+
3463

3564
def _setter_template(self, prop, value):
3665
if not getattr(value, "template_for_dispvms", False):
@@ -417,6 +446,43 @@ def is_preload(self) -> bool:
417446
return True
418447
return False
419448

449+
def is_preload_outdated(self) -> dict:
450+
"""
451+
Show properties that differ on disposable compared to its template.
452+
453+
:rtype: dict
454+
"""
455+
differed: dict[str, list] = {}
456+
if not self.is_preload:
457+
return differed
458+
459+
appvm = self.template
460+
if (
461+
self.volume_config["private"]["size"]
462+
!= appvm.volume_config["private"]["size"]
463+
):
464+
differed["volumes"] = ["private"]
465+
return differed
466+
467+
if any(vol for vol in self.volumes.values() if vol.is_outdated()):
468+
# Volume name is irrelevant. We use any() to return fast.
469+
differed["volumes"] = ["root"]
470+
return differed
471+
472+
appvm_props = appvm.property_dict()
473+
props = self.property_dict()
474+
differed_props = [
475+
k
476+
for k in props.keys() & appvm_props.keys()
477+
if k not in PRELOAD_OUTDATED_IGNORED_PROPERTIES
478+
and getattr(self, k, None) != getattr(appvm, k, None)
479+
]
480+
if not differed_props:
481+
return differed
482+
# Not using any() cause it is nice to know the property for debugging.
483+
differed["properties"] = differed_props
484+
return differed
485+
420486
@qubes.events.handler("domain-load")
421487
def on_domain_loaded(self, event) -> None:
422488
"""

qubes/vm/mix/dvmtemplate.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,15 @@ async def on_dvmtemplate_domain_shutdown(self, _event, **_kwargs) -> None:
148148
"""
149149
Refresh preloaded disposables on shutdown.
150150
"""
151-
await self.refresh_preload()
151+
self.refresh_outdated_preload()
152+
153+
@qubes.events.handler("property-reset:*", "property-set:*")
154+
def on_dvmtemplate_property_changed(self, _event, name, **_kwargs) -> None:
155+
"""
156+
Refresh preloaded disposables if property affects the disposable.
157+
"""
158+
if name not in qubes.vm.dispvm.PRELOAD_OUTDATED_IGNORED_PROPERTIES:
159+
self.refresh_outdated_preload(delay=30)
152160

153161
@qubes.events.handler("domain-feature-pre-set:preload-dispvm-delay")
154162
def on_feature_pre_set_preload_dispvm_delay(
@@ -641,21 +649,32 @@ def can_preload(self) -> bool:
641649
return True
642650
return False
643651

644-
async def refresh_preload(self) -> None:
652+
def refresh_outdated_preload(
653+
self, skip_check: bool = False, delay: Union[int, float] = 4
654+
) -> None:
645655
"""
646-
Refresh disposables which have outdated volumes.
656+
Refresh disposables which have outdated volumes or properties.
657+
658+
:param bool skip_check: Skip check of outdated preloads, refresh all.
647659
"""
648660
assert isinstance(self, qubes.vm.BaseVM)
649-
outdated = []
650-
for qube in self.dispvms:
651-
if not qube.is_preload or not any(
652-
vol.is_outdated() for vol in qube.volumes.values()
653-
):
661+
outdated: list | Iterator = []
662+
dispvms: list | Iterator = []
663+
if skip_check:
664+
outdated = self.dispvms
665+
else:
666+
dispvms = self.dispvms
667+
for qube in dispvms:
668+
if not qube.is_preload:
654669
continue
655-
outdated.append(qube)
670+
if qube.is_preload_outdated():
671+
assert isinstance(outdated, list)
672+
outdated.append(qube)
673+
656674
if outdated:
657675
self.remove_preload_from_list(
658-
[qube.name for qube in outdated], reason="of outdated volume(s)"
676+
[qube.name for qube in outdated],
677+
reason="of outdated volume(s) or property(ies)",
659678
)
660679
tasks = [self.app.domains[qube].cleanup() for qube in outdated]
661680
asyncio.ensure_future(asyncio.gather(*tasks))
@@ -664,7 +683,7 @@ async def refresh_preload(self) -> None:
664683
self.fire_event_async(
665684
"domain-preload-dispvm-start",
666685
reason="of outdated volume(s)",
667-
delay=4,
686+
delay=delay,
668687
)
669688
)
670689

@@ -682,16 +701,20 @@ def request_preload(self) -> Optional["qubes.vm.dispvm.DispVM"]:
682701
dispvm = None
683702
for item in preload_dispvm:
684703
qube = self.app.domains[item]
685-
if any(vol.is_outdated() for vol in qube.volumes.values()):
704+
if outdated_reason := qube.is_preload_outdated():
705+
if "properties" in outdated_reason:
706+
discard_reason = "property(ies): " + ", ".join(
707+
map(str, outdated_reason["properties"])
708+
)
709+
else:
710+
discard_reason = "volume(s)"
686711
qube.log.warning(
687-
"Requested preloaded qube but it is outdated, trying "
688-
"another one if available"
712+
"Discarding preloaded disposable as it has has outdated %s",
713+
discard_reason,
689714
)
690-
# The gap is filled after the delay set by the
691-
# 'domain-shutdown' of its ancestors. Not refilling now to
692-
# deliver a disposable faster.
715+
# Not refilling now to deliver a disposable faster.
693716
self.remove_preload_from_list(
694-
[qube.name], reason="of outdated volume(s)"
717+
[qube.name], reason="of outdated " + discard_reason
695718
)
696719
# Delay to not affect this run.
697720
asyncio.ensure_future(
@@ -706,6 +729,7 @@ def request_preload(self) -> Optional["qubes.vm.dispvm.DispVM"]:
706729
"Found only outdated preloaded qube(s), falling back to "
707730
"normal disposable"
708731
)
732+
self.fill_preload_gap()
709733
return None
710734
dispvm.mark_preload_requested()
711735
return dispvm

qubes/vm/templatevm.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,11 @@ async def on_template_domain_shutdown(self, _event, **_kwargs):
116116
"""
117117
appvms = [
118118
qube
119-
for qube in self.app.domains
120-
if getattr(qube, "template", None) == self
121-
and getattr(qube, "template_for_dispvms", False)
119+
for qube in self.appvms
120+
if getattr(qube, "template_for_dispvms", False)
122121
]
123122
for qube in appvms:
124-
await qube.refresh_preload()
123+
qube.refresh_outdated_preload()
125124

126125
@qubes.events.handler("domain-feature-set:boot-mode.appvm-default")
127126
def on_feature_bootmode_appvm_set(

0 commit comments

Comments
 (0)