Skip to content

Commit 0ccf400

Browse files
committed
Assure paused domains prior to suspending remain paused
fixes: QubesOS/qubes-issues#1611
1 parent b83d9ab commit 0ccf400

File tree

2 files changed

+70
-4
lines changed

2 files changed

+70
-4
lines changed

qubes/api/internal.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import asyncio
2424
import json
25+
import os
2526
import subprocess
2627

2728
import qubes.api
@@ -30,6 +31,9 @@
3031
import qubes.vm.dispvm
3132

3233

34+
PREVIOUSLY_PAUSED = "/run/qubes/previously-paused.list"
35+
36+
3337
class SystemInfoCache:
3438
cache = None
3539
cache_for_app = None
@@ -244,13 +248,24 @@ async def suspend_pre(self):
244248
:return:
245249
"""
246250

247-
# first notify all VMs
251+
# first keep track of VMs which were paused before suspending
252+
previously_paused = [
253+
vm.name
254+
for vm in self.app.domains
255+
if vm.get_power_state() in ["Paused", "Suspended"]
256+
]
257+
with open(PREVIOUSLY_PAUSED, "w", encoding="ascii") as file:
258+
file.write("\n".join(previously_paused))
259+
260+
# then notify all VMs (except paused ones)
248261
processes = []
249262
for vm in self.app.domains:
250263
if isinstance(vm, qubes.vm.adminvm.AdminVM):
251264
continue
252265
if not vm.is_running():
253266
continue
267+
if vm.name in previously_paused:
268+
continue
254269
if not vm.features.check_with_template("qrexec", False):
255270
continue
256271
try:
@@ -289,6 +304,8 @@ async def suspend_pre(self):
289304
for vm in self.app.domains:
290305
if isinstance(vm, qubes.vm.adminvm.AdminVM):
291306
continue
307+
if vm.name in previously_paused:
308+
continue
292309
if vm.is_running():
293310
coros.append(asyncio.create_task(vm.suspend()))
294311
if coros:
@@ -312,23 +329,37 @@ async def suspend_post(self):
312329
:return:
313330
"""
314331

332+
# Reload list of previously paused qubes before suspending
333+
previously_paused = []
334+
try:
335+
if os.path.isfile(PREVIOUSLY_PAUSED):
336+
with open(PREVIOUSLY_PAUSED, encoding="ascii") as file:
337+
previously_paused = file.read().split("\n")
338+
os.unlink(PREVIOUSLY_PAUSED)
339+
except OSError:
340+
previously_paused = []
341+
315342
coros = []
316343
# first resume/unpause VMs
317344
for vm in self.app.domains:
318345
if isinstance(vm, qubes.vm.adminvm.AdminVM):
319346
continue
347+
if vm.name in previously_paused:
348+
continue
320349
if vm.get_power_state() in ["Paused", "Suspended"]:
321350
coros.append(asyncio.create_task(vm.resume()))
322351
if coros:
323352
await asyncio.wait(coros)
324353

325-
# then notify all VMs
354+
# then notify all VMs (except previously paused ones)
326355
processes = []
327356
for vm in self.app.domains:
328357
if isinstance(vm, qubes.vm.adminvm.AdminVM):
329358
continue
330359
if not vm.is_running():
331360
continue
361+
if vm.name in previously_paused:
362+
continue
332363
if not vm.features.check_with_template("qrexec", False):
333364
continue
334365
try:

qubes/tests/api_internal.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,26 @@ def test_000_suspend_pre(self):
9191
no_qrexec_vm = self.create_mockvm()
9292
no_qrexec_vm.is_running.return_value = True
9393

94+
paused_vm = self.create_mockvm(features={"qrexec": True})
95+
paused_vm.is_running.return_value = True
96+
paused_vm.get_power_state.return_value = "Paused"
97+
paused_vm.name = "SleepingBeauty"
98+
9499
self.domains.update(
95100
{
96101
"running": running_vm,
97102
"not-running": not_running_vm,
98103
"no-qrexec": no_qrexec_vm,
104+
"paused": paused_vm,
99105
}
100106
)
101107

102-
ret = self.call_mgmt_func(b"internal.SuspendPre")
108+
with mock.patch.object(
109+
qubes.api.internal,
110+
'PREVIOUSLY_PAUSED',
111+
"/tmp/qubes-previously-paused.tmp"
112+
):
113+
ret = self.call_mgmt_func(b"internal.SuspendPre")
103114
self.assertIsNone(ret)
104115
self.assertFalse(self.dom0.called)
105116

@@ -119,6 +130,13 @@ def test_000_suspend_pre(self):
119130
("run_service", ("qubes.SuspendPreAll",), mock.ANY),
120131
no_qrexec_vm.mock_calls,
121132
)
133+
134+
self.assertNotIn(
135+
("run_service", ("qubes.SuspendPreAll",), mock.ANY),
136+
paused_vm.mock_calls,
137+
)
138+
self.assertIn(("suspend", (), {}), running_vm.mock_calls)
139+
122140
self.assertIn(("suspend", (), {}), no_qrexec_vm.mock_calls)
123141

124142
def test_001_suspend_post(self):
@@ -134,15 +152,26 @@ def test_001_suspend_post(self):
134152
no_qrexec_vm.is_running.return_value = True
135153
no_qrexec_vm.get_power_state.return_value = "Suspended"
136154

155+
paused_vm = self.create_mockvm(features={"qrexec": True})
156+
paused_vm.is_running.return_value = True
157+
paused_vm.get_power_state.return_value = "Paused"
158+
paused_vm.name = "SleepingBeauty"
159+
137160
self.domains.update(
138161
{
139162
"running": running_vm,
140163
"not-running": not_running_vm,
141164
"no-qrexec": no_qrexec_vm,
165+
"paused": paused_vm,
142166
}
143167
)
144168

145-
ret = self.call_mgmt_func(b"internal.SuspendPost")
169+
with mock.patch.object(
170+
qubes.api.internal,
171+
'PREVIOUSLY_PAUSED',
172+
"/tmp/qubes-previously-paused.tmp"
173+
):
174+
ret = self.call_mgmt_func(b"internal.SuspendPost")
146175
self.assertIsNone(ret)
147176
self.assertFalse(self.dom0.called)
148177

@@ -164,6 +193,12 @@ def test_001_suspend_post(self):
164193
)
165194
self.assertIn(("resume", (), {}), no_qrexec_vm.mock_calls)
166195

196+
self.assertNotIn(
197+
("run_service", ("qubes.SuspendPostAll",), mock.ANY),
198+
paused_vm.mock_calls,
199+
)
200+
self.assertNotIn(("resume", (), {}), paused_vm.mock_calls)
201+
167202
def test_010_get_system_info(self):
168203
self.dom0.name = "dom0"
169204
self.dom0.tags = ["tag1", "tag2"]

0 commit comments

Comments
 (0)