Skip to content

Commit 4a453d0

Browse files
committed
tests: add qrexec performance tests
Add simple connection latency, and throughput tests. Run them with different type of services (scripts, socket, via fork-server or not). They print a test run time for comparison - the lower the better. QubesOS/qubes-issues#5740
1 parent 6cd9523 commit 4a453d0

3 files changed

Lines changed: 257 additions & 0 deletions

File tree

qubes/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,7 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
18251825
"qubes.tests.integ.devices_block",
18261826
"qubes.tests.integ.devices_pci",
18271827
"qubes.tests.integ.qrexec",
1828+
"qubes.tests.integ.qrexec_perf",
18281829
"qubes.tests.integ.dom0_update",
18291830
"qubes.tests.integ.vm_update",
18301831
"qubes.tests.integ.network",

qubes/tests/integ/qrexec_perf.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#
2+
# The Qubes OS Project, https://www.qubes-os.org/
3+
#
4+
# Copyright (C) 2025 Marek Marczykowski-Górecki
5+
# <marmarek@invisiblethingslab.com>
6+
#
7+
# This library is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public
9+
# License as published by the Free Software Foundation; either
10+
# version 2 of the License, or (at your option) any later version.
11+
#
12+
# This library is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+
# General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public
18+
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
19+
import asyncio
20+
import os
21+
import subprocess
22+
import sys
23+
import time
24+
25+
import qubes.tests
26+
from qubes.tests import substitute_entry_points
27+
28+
29+
class TC_00_QrexecPerfMixin:
30+
def setUp(self: qubes.tests.SystemTestCase):
31+
super().setUp()
32+
self.vm1 = self.app.add_new_vm(
33+
"AppVM",
34+
name=self.make_vm_name("vm1"),
35+
label="red",
36+
)
37+
self.vm2 = self.app.add_new_vm(
38+
"AppVM",
39+
name=self.make_vm_name("vm2"),
40+
label="red",
41+
)
42+
self.loop.run_until_complete(
43+
asyncio.gather(
44+
self.vm1.create_on_disk(),
45+
self.vm2.create_on_disk(),
46+
)
47+
)
48+
self.loop.run_until_complete(
49+
asyncio.gather(
50+
self.vm1.start(),
51+
self.vm2.start(),
52+
)
53+
)
54+
self.iterations = int(os.environ.get("QUBES_TEST_ITERATIONS", "500"))
55+
56+
def run_latency_calls_from_vm(self):
57+
start_time = time.clock_gettime(time.CLOCK_MONOTONIC)
58+
p = self.vm1.run_for_stdio(
59+
f"set -e;"
60+
f"for i in $(seq {self.iterations}); do "
61+
f" out=$(qrexec-client-vm {self.vm2.name} test.Echo);"
62+
f" test \"$out\" = 'test';"
63+
f"done"
64+
)
65+
try:
66+
self.loop.run_until_complete(p)
67+
except subprocess.CalledProcessError as e:
68+
self.fail(
69+
f"test.Echo service failed ({e.returncode}):"
70+
f" {e.stdout},"
71+
f" {e.stderr}"
72+
)
73+
end_time = time.clock_gettime(time.CLOCK_MONOTONIC)
74+
print(f"Run time: {end_time-start_time}s")
75+
76+
def test_000_simple(self):
77+
"""Measure simple exec-based vm-vm calls latency"""
78+
self.create_remote_file(
79+
self.vm2, "/etc/qubes-rpc/test.Echo", "#!/bin/sh\necho test"
80+
)
81+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
82+
with self.qrexec_policy("test.Echo", self.vm1, self.vm2):
83+
self.run_latency_calls_from_vm()
84+
85+
def test_010_simple_root(self):
86+
"""Measure simple exec-based vm-vm calls latency, use root to
87+
bypass qrexec-fork-server"""
88+
self.create_remote_file(
89+
self.vm2, "/etc/qubes-rpc/test.Echo", "#!/bin/sh\necho test"
90+
)
91+
with self.qrexec_policy(
92+
"test.Echo", self.vm1, self.vm2, action="allow user=root"
93+
):
94+
self.run_latency_calls_from_vm()
95+
96+
def test_020_socket(self):
97+
"""Measure simple socket-based vm-vm calls latency"""
98+
self.create_remote_file(
99+
self.vm2,
100+
"/etc/qubes/rpc-config/test.Echo",
101+
"skip-service-descriptor=true\n",
102+
)
103+
server_p = self.loop.run_until_complete(
104+
self.vm2.run(
105+
"socat UNIX-LISTEN:/etc/qubes-rpc/test.Echo,mode=0666,fork "
106+
"EXEC:'/bin/echo test'",
107+
user="root",
108+
)
109+
)
110+
self.loop.run_until_complete(
111+
asyncio.wait_for(
112+
self.vm2.run_for_stdio(
113+
"while ! test -e /etc/qubes-rpc/test.Echo; do sleep 0.1; done"
114+
),
115+
timeout=10,
116+
)
117+
)
118+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
119+
try:
120+
with self.qrexec_policy("test.Echo", self.vm1, self.vm2):
121+
self.run_latency_calls_from_vm()
122+
finally:
123+
server_p.terminate()
124+
self.loop.run_until_complete(server_p.wait())
125+
126+
def test_030_socket_root(self):
127+
"""Measure simple socket-based vm-vm calls latency, use root to
128+
bypass qrexec-fork-server"""
129+
self.create_remote_file(
130+
self.vm2,
131+
"/etc/qubes/rpc-config/test.Echo",
132+
"skip-service-descriptor=true\n",
133+
)
134+
server_p = self.loop.run_until_complete(
135+
self.vm2.run(
136+
"socat UNIX-LISTEN:/etc/qubes-rpc/test.Echo,mode=0666,fork "
137+
"EXEC:'/bin/echo test'",
138+
user="root",
139+
)
140+
)
141+
self.loop.run_until_complete(
142+
asyncio.wait_for(
143+
self.vm2.run_for_stdio(
144+
"while ! test -e /etc/qubes-rpc/test.Echo; do sleep 0.1; done"
145+
),
146+
timeout=10,
147+
)
148+
)
149+
try:
150+
with self.qrexec_policy(
151+
"test.Echo", self.vm1, self.vm2, action="allow user=root"
152+
):
153+
self.run_latency_calls_from_vm()
154+
finally:
155+
server_p.terminate()
156+
self.loop.run_until_complete(server_p.wait())
157+
158+
def run_throughput_calls_from_vm(self, duplex=False):
159+
prefix = ""
160+
if duplex:
161+
prefix = "head -c 100000000 /dev/zero | "
162+
start_time = time.clock_gettime(time.CLOCK_MONOTONIC)
163+
p = self.vm1.run_for_stdio(
164+
f"set -e;"
165+
f"for i in $(seq {self.iterations//2}); do "
166+
f" out=$({prefix}qrexec-client-vm {self.vm2.name} test.Echo "
167+
f"| wc -c);"
168+
f' test "$out" = \'100000000\' || {{ echo "failed iteration $i:'
169+
f" '$out'\"; exit 1; }};"
170+
f"done"
171+
)
172+
try:
173+
self.loop.run_until_complete(p)
174+
except subprocess.CalledProcessError as e:
175+
self.fail(
176+
f"test.Echo service failed ({e.returncode}):"
177+
f" {e.stdout},"
178+
f" {e.stderr}"
179+
)
180+
end_time = time.clock_gettime(time.CLOCK_MONOTONIC)
181+
print(f"Run time: {end_time-start_time}s")
182+
183+
def test_100_simple_data_simplex(self):
184+
"""Measure simple exec-based vm-vm calls throughput"""
185+
self.create_remote_file(
186+
self.vm2,
187+
"/etc/qubes-rpc/test.Echo",
188+
"#!/bin/sh\nhead -c 100000000 /dev/zero",
189+
)
190+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
191+
with self.qrexec_policy("test.Echo", self.vm1, self.vm2):
192+
self.run_throughput_calls_from_vm()
193+
194+
def test_110_simple_data_duplex(self):
195+
"""Measure simple exec-based vm-vm calls throughput"""
196+
self.create_remote_file(self.vm2, "/etc/qubes-rpc/test.Echo", "#!/bin/sh\ncat")
197+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
198+
with self.qrexec_policy("test.Echo", self.vm1, self.vm2):
199+
self.run_throughput_calls_from_vm(duplex=True)
200+
201+
def test_120_simple_data_duplex_root(self):
202+
"""Measure simple exec-based vm-vm calls throughput"""
203+
self.create_remote_file(self.vm2, "/etc/qubes-rpc/test.Echo", "#!/bin/sh\ncat")
204+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
205+
with self.qrexec_policy(
206+
"test.Echo", self.vm1, self.vm2, action="allow user=root"
207+
):
208+
self.run_throughput_calls_from_vm(duplex=True)
209+
210+
def test_130_socket_data_duplex(self):
211+
"""Measure simple socket-based vm-vm calls throughput"""
212+
self.create_remote_file(
213+
self.vm2,
214+
"/etc/qubes/rpc-config/test.Echo",
215+
"skip-service-descriptor=true\n",
216+
)
217+
server_p = self.loop.run_until_complete(
218+
self.vm2.run(
219+
"socat UNIX-LISTEN:/etc/qubes-rpc/test.Echo,mode=0666,fork "
220+
"EXEC:'/bin/cat'",
221+
user="root",
222+
)
223+
)
224+
try:
225+
self.loop.run_until_complete(
226+
asyncio.wait_for(
227+
self.vm2.run_for_stdio(
228+
"while ! test -e /etc/qubes-rpc/test.Echo; do sleep 0.1; done"
229+
),
230+
timeout=10,
231+
)
232+
)
233+
self.loop.run_until_complete(self.wait_for_session(self.vm2))
234+
with self.qrexec_policy("test.Echo", self.vm1, self.vm2):
235+
self.run_throughput_calls_from_vm(duplex=True)
236+
finally:
237+
server_p.terminate()
238+
self.loop.run_until_complete(server_p.wait())
239+
240+
241+
def create_testcases_for_templates():
242+
return qubes.tests.create_testcases_for_templates(
243+
"TC_00_QrexecPerf",
244+
TC_00_QrexecPerfMixin,
245+
qubes.tests.SystemTestCase,
246+
module=sys.modules[__name__],
247+
)
248+
249+
250+
def load_tests(loader, tests, pattern):
251+
tests.addTests(loader.loadTestsFromNames(create_testcases_for_templates()))
252+
return tests
253+
254+
255+
qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)

rpm_spec/core-dom0.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ done
526526
%{python3_sitelib}/qubes/tests/integ/grub.py
527527
%{python3_sitelib}/qubes/tests/integ/salt.py
528528
%{python3_sitelib}/qubes/tests/integ/qrexec.py
529+
%{python3_sitelib}/qubes/tests/integ/qrexec_perf.py
529530
%{python3_sitelib}/qubes/tests/integ/storage.py
530531
%{python3_sitelib}/qubes/tests/integ/vm_qrexec_gui.py
531532

0 commit comments

Comments
 (0)