qrexec.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2014-2020
  5. # Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  6. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  7. #
  8. # This library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. #
  21. import asyncio
  22. import contextlib
  23. import os
  24. import subprocess
  25. import sys
  26. import qubes.config
  27. import qubes.devices
  28. import qubes.tests
  29. import qubes.vm.appvm
  30. import qubes.vm.templatevm
  31. TEST_DATA = b"0123456789" * 1024
  32. class TC_00_QrexecMixin(object):
  33. def setUp(self):
  34. super().setUp()
  35. self.init_default_template(self.template)
  36. if self._testMethodName == 'test_210_time_sync':
  37. self.init_networking()
  38. self.testvm1 = self.app.add_new_vm(
  39. qubes.vm.appvm.AppVM,
  40. label='red',
  41. name=self.make_vm_name('vm1'),
  42. template=self.app.domains[self.template])
  43. self.loop.run_until_complete(self.testvm1.create_on_disk())
  44. self.testvm2 = self.app.add_new_vm(
  45. qubes.vm.appvm.AppVM,
  46. label='red',
  47. name=self.make_vm_name('vm2'),
  48. template=self.app.domains[self.template])
  49. self.loop.run_until_complete(self.testvm2.create_on_disk())
  50. self.app.save()
  51. def tearDown(self):
  52. # socket-based qrexec tests:
  53. if os.path.exists('/etc/qubes-rpc/test.Socket'):
  54. os.unlink('/etc/qubes-rpc/test.Socket')
  55. if hasattr(self, 'service_proc'):
  56. try:
  57. self.service_proc.terminate()
  58. self.loop.run_until_complete(self.service_proc.communicate())
  59. except ProcessLookupError:
  60. pass
  61. super().tearDown()
  62. def test_050_qrexec_simple_eof(self):
  63. """Test for data and EOF transmission dom0->VM"""
  64. # XXX is this still correct? this is no longer simple qrexec,
  65. # but qubes.VMShell
  66. self.loop.run_until_complete(self.testvm1.start())
  67. try:
  68. (stdout, stderr) = self.loop.run_until_complete(asyncio.wait_for(
  69. self.testvm1.run_for_stdio('cat', input=TEST_DATA),
  70. timeout=10))
  71. except asyncio.TimeoutError:
  72. self.fail(
  73. "Timeout, probably EOF wasn't transferred to the VM process")
  74. self.assertEqual(stdout, TEST_DATA,
  75. 'Received data differs from what was sent')
  76. self.assertFalse(stderr,
  77. 'Some data was printed to stderr')
  78. def test_051_qrexec_simple_eof_reverse(self):
  79. """Test for EOF transmission VM->dom0"""
  80. @asyncio.coroutine
  81. def run(self):
  82. p = yield from self.testvm1.run(
  83. 'echo test; exec >&-; cat > /dev/null',
  84. stdin=subprocess.PIPE,
  85. stdout=subprocess.PIPE,
  86. stderr=subprocess.PIPE)
  87. # this will hang on test failure
  88. stdout = yield from asyncio.wait_for(p.stdout.read(), timeout=10)
  89. p.stdin.write(TEST_DATA)
  90. yield from p.stdin.drain()
  91. p.stdin.close()
  92. self.assertEqual(stdout.strip(), b'test',
  93. 'Received data differs from what was expected')
  94. # this may hang in some buggy cases
  95. self.assertFalse((yield from p.stderr.read()),
  96. 'Some data was printed to stderr')
  97. try:
  98. yield from asyncio.wait_for(p.wait(), timeout=1)
  99. except asyncio.TimeoutError:
  100. self.fail("Timeout, "
  101. "probably EOF wasn't transferred from the VM process")
  102. self.loop.run_until_complete(self.testvm1.start())
  103. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  104. self.loop.run_until_complete(run(self))
  105. def test_052_qrexec_vm_service_eof(self):
  106. """Test for EOF transmission VM(src)->VM(dst)"""
  107. self.loop.run_until_complete(asyncio.gather(
  108. self.testvm1.start(),
  109. self.testvm2.start()))
  110. self.loop.run_until_complete(asyncio.gather(
  111. self.wait_for_session(self.testvm1),
  112. self.wait_for_session(self.testvm2)))
  113. self.create_remote_file(self.testvm2,
  114. '/etc/qubes-rpc/test.EOF',
  115. '#!/bin/sh\n/bin/cat\n')
  116. with self.qrexec_policy('test.EOF', self.testvm1, self.testvm2):
  117. try:
  118. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  119. self.testvm1.run_for_stdio('''\
  120. /usr/lib/qubes/qrexec-client-vm {} test.EOF \
  121. /bin/sh -c 'echo test; exec >&-; cat >&$SAVED_FD_1'
  122. '''.format(self.testvm2.name)),
  123. timeout=10))
  124. except subprocess.CalledProcessError as e:
  125. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  126. e.cmd, e.returncode, e.stderr))
  127. except asyncio.TimeoutError:
  128. self.fail("Timeout, probably EOF wasn't transferred")
  129. self.assertEqual(stdout, b'test\n',
  130. 'Received data differs from what was expected')
  131. def test_053_qrexec_vm_service_eof_reverse(self):
  132. """Test for EOF transmission VM(src)<-VM(dst)"""
  133. self.loop.run_until_complete(asyncio.gather(
  134. self.testvm1.start(),
  135. self.testvm2.start()))
  136. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.EOF',
  137. '#!/bin/sh\n'
  138. 'echo test; exec >&-; cat >/dev/null')
  139. with self.qrexec_policy('test.EOF', self.testvm1, self.testvm2):
  140. try:
  141. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  142. self.testvm1.run_for_stdio('''\
  143. /usr/lib/qubes/qrexec-client-vm {} test.EOF \
  144. /bin/sh -c 'cat >&$SAVED_FD_1'
  145. '''.format(self.testvm2.name)),
  146. timeout=10))
  147. except subprocess.CalledProcessError as e:
  148. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  149. e.cmd, e.returncode, e.stderr))
  150. except asyncio.TimeoutError:
  151. self.fail("Timeout, probably EOF wasn't transferred")
  152. self.assertEqual(stdout, b'test\n',
  153. 'Received data differs from what was expected')
  154. def test_055_qrexec_dom0_service_abort(self):
  155. """
  156. Test if service abort (by dom0) is properly handled by source VM.
  157. If "remote" part of the service terminates, the source part should
  158. properly be notified. This includes closing its stdin (which is
  159. already checked by test_053_qrexec_vm_service_eof_reverse), but also
  160. its stdout - otherwise such service might hang on write(2) call.
  161. """
  162. self.loop.run_until_complete(self.testvm1.start())
  163. self.create_local_file('/etc/qubes-rpc/test.Abort',
  164. 'sleep 1')
  165. with self.qrexec_policy('test.Abort', self.testvm1, 'dom0'):
  166. try:
  167. # two possible exit codes, depending on when exactly dom0
  168. # service terminates:
  169. # exit code 141: EPIPE (no buffered data)
  170. # exit code 1: ECONNRESET (some buffered data remains)
  171. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  172. self.testvm1.run_for_stdio('''\
  173. /usr/lib/qubes/qrexec-client-vm dom0 test.Abort \
  174. /bin/sh -c 'cat /dev/zero; echo $? >/tmp/exit-code';
  175. sleep 1;
  176. e=$(cat /tmp/exit-code);
  177. test $e -eq 141 -o $e -eq 1'''),
  178. timeout=10))
  179. except subprocess.CalledProcessError as e:
  180. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  181. e.cmd, e.returncode, e.stderr))
  182. except asyncio.TimeoutError:
  183. self.fail("Timeout, probably stdout wasn't closed")
  184. def test_060_qrexec_exit_code_dom0(self):
  185. self.loop.run_until_complete(self.testvm1.start())
  186. self.loop.run_until_complete(self.testvm1.run_for_stdio('exit 0'))
  187. with self.assertRaises(subprocess.CalledProcessError) as e:
  188. self.loop.run_until_complete(self.testvm1.run_for_stdio('exit 3'))
  189. self.assertEqual(e.exception.returncode, 3)
  190. def test_065_qrexec_exit_code_vm(self):
  191. self.loop.run_until_complete(asyncio.gather(
  192. self.testvm1.start(),
  193. self.testvm2.start()))
  194. with self.qrexec_policy('test.Retcode', self.testvm1, self.testvm2):
  195. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Retcode',
  196. 'exit 0')
  197. (stdout, stderr) = self.loop.run_until_complete(
  198. self.testvm1.run_for_stdio('''\
  199. /usr/lib/qubes/qrexec-client-vm {} test.Retcode;
  200. echo $?'''.format(self.testvm2.name),
  201. stderr=None))
  202. self.assertEqual(stdout, b'0\n')
  203. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Retcode',
  204. 'exit 3')
  205. (stdout, stderr) = self.loop.run_until_complete(
  206. self.testvm1.run_for_stdio('''\
  207. /usr/lib/qubes/qrexec-client-vm {} test.Retcode;
  208. echo $?'''.format(self.testvm2.name),
  209. stderr=None))
  210. self.assertEqual(stdout, b'3\n')
  211. def test_070_qrexec_vm_simultaneous_write(self):
  212. """Test for simultaneous write in VM(src)->VM(dst) connection
  213. This is regression test for #1347
  214. Check for deadlock when initially both sides writes a lot of data
  215. (and not read anything). When one side starts reading, it should
  216. get the data and the remote side should be possible to write then more.
  217. There was a bug where remote side was waiting on write(2) and not
  218. handling anything else.
  219. """
  220. self.loop.run_until_complete(asyncio.gather(
  221. self.testvm1.start(),
  222. self.testvm2.start()))
  223. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
  224. # first write a lot of data
  225. dd if=/dev/zero bs=993 count=10000 iflag=fullblock
  226. # and only then read something
  227. dd of=/dev/null bs=993 count=10000 iflag=fullblock
  228. ''')
  229. with self.qrexec_policy('test.write', self.testvm1, self.testvm2):
  230. try:
  231. self.loop.run_until_complete(asyncio.wait_for(
  232. # first write a lot of data to fill all the buffers
  233. # then after some time start reading
  234. self.testvm1.run_for_stdio('''\
  235. /usr/lib/qubes/qrexec-client-vm {} test.write \
  236. /bin/sh -c '
  237. dd if=/dev/zero bs=993 count=10000 iflag=fullblock &
  238. sleep 1;
  239. dd of=/dev/null bs=993 count=10000 iflag=fullblock;
  240. wait'
  241. '''.format(self.testvm2.name)), timeout=10))
  242. except subprocess.CalledProcessError as e:
  243. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  244. e.cmd, e.returncode, e.stderr))
  245. except asyncio.TimeoutError:
  246. self.fail('Timeout, probably deadlock')
  247. def test_071_qrexec_dom0_simultaneous_write(self):
  248. """Test for simultaneous write in dom0(src)->VM(dst) connection
  249. Similar to test_070_qrexec_vm_simultaneous_write, but with dom0
  250. as a source.
  251. """
  252. self.loop.run_until_complete(self.testvm2.start())
  253. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
  254. # first write a lot of data
  255. dd if=/dev/zero bs=993 count=10000 iflag=fullblock
  256. # and only then read something
  257. dd of=/dev/null bs=993 count=10000 iflag=fullblock
  258. ''')
  259. # can't use subprocess.PIPE, because asyncio will claim those FDs
  260. pipe1_r, pipe1_w = os.pipe()
  261. pipe2_r, pipe2_w = os.pipe()
  262. try:
  263. local_proc = self.loop.run_until_complete(
  264. asyncio.create_subprocess_shell(
  265. # first write a lot of data to fill all the buffers
  266. "dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
  267. # then after some time start reading
  268. "sleep 1; "
  269. "dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
  270. "wait", stdin=pipe1_r, stdout=pipe2_w))
  271. self.service_proc = self.loop.run_until_complete(self.testvm2.run_service(
  272. "test.write", stdin=pipe2_r, stdout=pipe1_w))
  273. finally:
  274. os.close(pipe1_r)
  275. os.close(pipe1_w)
  276. os.close(pipe2_r)
  277. os.close(pipe2_w)
  278. try:
  279. self.loop.run_until_complete(
  280. asyncio.wait_for(self.service_proc.wait(), timeout=10))
  281. except asyncio.TimeoutError:
  282. self.fail("Timeout, probably deadlock")
  283. else:
  284. self.assertEqual(self.service_proc.returncode, 0,
  285. "Service call failed")
  286. def test_072_qrexec_to_dom0_simultaneous_write(self):
  287. """Test for simultaneous write in dom0(src)<-VM(dst) connection
  288. Similar to test_071_qrexec_dom0_simultaneous_write, but with dom0
  289. as a "hanging" side.
  290. """
  291. self.loop.run_until_complete(self.testvm2.start())
  292. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
  293. # first write a lot of data
  294. dd if=/dev/zero bs=993 count=10000 iflag=fullblock &
  295. # and only then read something
  296. dd of=/dev/null bs=993 count=10000 iflag=fullblock
  297. sleep 1;
  298. wait
  299. ''')
  300. # can't use subprocess.PIPE, because asyncio will claim those FDs
  301. pipe1_r, pipe1_w = os.pipe()
  302. pipe2_r, pipe2_w = os.pipe()
  303. try:
  304. local_proc = self.loop.run_until_complete(
  305. asyncio.create_subprocess_shell(
  306. # first write a lot of data to fill all the buffers
  307. "dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
  308. # then, only when all written, read something
  309. "dd of=/dev/null bs=993 count=10000 iflag=fullblock; ",
  310. stdin=pipe1_r, stdout=pipe2_w))
  311. self.service_proc = self.loop.run_until_complete(
  312. self.testvm2.run_service(
  313. "test.write", stdin=pipe2_r, stdout=pipe1_w))
  314. finally:
  315. os.close(pipe1_r)
  316. os.close(pipe1_w)
  317. os.close(pipe2_r)
  318. os.close(pipe2_w)
  319. try:
  320. self.loop.run_until_complete(
  321. asyncio.wait_for(self.service_proc.wait(), timeout=10))
  322. except asyncio.TimeoutError:
  323. self.fail("Timeout, probably deadlock")
  324. else:
  325. self.assertEqual(self.service_proc.returncode, 0,
  326. "Service call failed")
  327. def test_080_qrexec_service_argument_allow_default(self):
  328. """Qrexec service call with argument"""
  329. self.loop.run_until_complete(asyncio.gather(
  330. self.testvm1.start(),
  331. self.testvm2.start()))
  332. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  333. '/usr/bin/printf %s "$1"')
  334. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  335. stdout, stderr = self.loop.run_until_complete(
  336. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  337. '{} test.Argument+argument'.format(self.testvm2.name),
  338. stderr=None))
  339. self.assertEqual(stdout, b'argument')
  340. def test_081_qrexec_service_argument_allow_specific(self):
  341. """Qrexec service call with argument - allow only specific value"""
  342. self.loop.run_until_complete(asyncio.gather(
  343. self.testvm1.start(),
  344. self.testvm2.start()))
  345. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  346. '/usr/bin/printf %s "$1"')
  347. with self.qrexec_policy('test.Argument', '$anyvm', '$anyvm', False):
  348. with self.qrexec_policy('test.Argument+argument',
  349. self.testvm1.name, self.testvm2.name):
  350. stdout, stderr = self.loop.run_until_complete(
  351. self.testvm1.run_for_stdio(
  352. '/usr/lib/qubes/qrexec-client-vm '
  353. '{} test.Argument+argument'.format(self.testvm2.name),
  354. stderr=None))
  355. self.assertEqual(stdout, b'argument')
  356. def test_082_qrexec_service_argument_deny_specific(self):
  357. """Qrexec service call with argument - deny specific value"""
  358. self.loop.run_until_complete(asyncio.gather(
  359. self.testvm1.start(),
  360. self.testvm2.start()))
  361. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  362. '/usr/bin/printf %s "$1"')
  363. with self.qrexec_policy('test.Argument', '$anyvm', '$anyvm'):
  364. with self.qrexec_policy('test.Argument+argument',
  365. self.testvm1, self.testvm2, allow=False):
  366. with self.assertRaises(subprocess.CalledProcessError,
  367. msg='Service request should be denied'):
  368. self.loop.run_until_complete(
  369. self.testvm1.run_for_stdio(
  370. '/usr/lib/qubes/qrexec-client-vm {} '
  371. 'test.Argument+argument'.format(self.testvm2.name),
  372. stderr=None))
  373. def test_083_qrexec_service_argument_specific_implementation(self):
  374. """Qrexec service call with argument - argument specific
  375. implementatation"""
  376. self.loop.run_until_complete(asyncio.gather(
  377. self.testvm1.start(),
  378. self.testvm2.start()))
  379. self.create_remote_file(self.testvm2,
  380. '/etc/qubes-rpc/test.Argument',
  381. '/usr/bin/printf %s "$1"')
  382. self.create_remote_file(self.testvm2,
  383. '/etc/qubes-rpc/test.Argument+argument',
  384. '/usr/bin/printf "specific: %s" "$1"')
  385. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  386. stdout, stderr = self.loop.run_until_complete(
  387. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  388. '{} test.Argument+argument'.format(self.testvm2.name),
  389. stderr=None))
  390. self.assertEqual(stdout, b'specific: argument')
  391. def test_084_qrexec_service_argument_extra_env(self):
  392. """Qrexec service call with argument - extra env variables"""
  393. self.loop.run_until_complete(asyncio.gather(
  394. self.testvm1.start(),
  395. self.testvm2.start()))
  396. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  397. '/usr/bin/printf "%s %s" '
  398. '"$QREXEC_SERVICE_FULL_NAME" "$QREXEC_SERVICE_ARGUMENT"')
  399. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  400. stdout, stderr = self.loop.run_until_complete(
  401. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  402. '{} test.Argument+argument'.format(self.testvm2.name),
  403. stderr=None))
  404. self.assertEqual(stdout, b'test.Argument+argument argument')
  405. def test_090_qrexec_service_socket_dom0(self):
  406. """Basic test socket services (dom0) - data receive"""
  407. self.loop.run_until_complete(self.testvm1.start())
  408. self.service_proc = self.loop.run_until_complete(
  409. asyncio.create_subprocess_shell(
  410. 'socat -u UNIX-LISTEN:/etc/qubes-rpc/test.Socket,mode=666 -',
  411. stdout=subprocess.PIPE, stdin=subprocess.PIPE))
  412. try:
  413. with self.qrexec_policy('test.Socket', self.testvm1, '@adminvm'):
  414. (stdout, stderr) = self.loop.run_until_complete(asyncio.wait_for(
  415. self.testvm1.run_for_stdio(
  416. 'qrexec-client-vm @adminvm test.Socket', input=TEST_DATA),
  417. timeout=10))
  418. except subprocess.CalledProcessError as e:
  419. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  420. e.cmd, e.returncode, e.stderr))
  421. except asyncio.TimeoutError:
  422. self.fail(
  423. "service timeout, probably EOF wasn't transferred to the VM process")
  424. try:
  425. (service_stdout, service_stderr) = self.loop.run_until_complete(
  426. asyncio.wait_for(
  427. self.service_proc.communicate(),
  428. timeout=10))
  429. except asyncio.TimeoutError:
  430. self.fail(
  431. "socat timeout, probably EOF wasn't transferred to the VM process")
  432. service_descriptor = b'test.Socket+ test-inst-vm1 keyword adminvm\0'
  433. self.assertEqual(service_stdout, service_descriptor + TEST_DATA,
  434. 'Received data differs from what was sent')
  435. self.assertFalse(stderr,
  436. 'Some data was printed to stderr')
  437. self.assertFalse(service_stderr,
  438. 'Some data was printed to stderr')
  439. def test_091_qrexec_service_socket_dom0_send(self):
  440. """Basic test socket services (dom0) - data send"""
  441. self.loop.run_until_complete(self.testvm1.start())
  442. self.create_local_file('/tmp/service-input', TEST_DATA.decode())
  443. self.service_proc = self.loop.run_until_complete(
  444. asyncio.create_subprocess_shell(
  445. 'socat -u OPEN:/tmp/service-input UNIX-LISTEN:/etc/qubes-rpc/test.Socket,mode=666'))
  446. try:
  447. with self.qrexec_policy('test.Socket', self.testvm1, '@adminvm'):
  448. stdout, stderr = self.loop.run_until_complete(asyncio.wait_for(
  449. self.testvm1.run_for_stdio(
  450. 'qrexec-client-vm @adminvm test.Socket'),
  451. timeout=10))
  452. except subprocess.CalledProcessError as e:
  453. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  454. e.cmd, e.returncode, e.stderr))
  455. except asyncio.TimeoutError:
  456. self.fail(
  457. "service timeout, probably EOF wasn't transferred to the VM process")
  458. try:
  459. (service_stdout, service_stderr) = self.loop.run_until_complete(
  460. asyncio.wait_for(
  461. self.service_proc.communicate(),
  462. timeout=10))
  463. except asyncio.TimeoutError:
  464. self.fail(
  465. "socat timeout, probably EOF wasn't transferred to the VM process")
  466. self.assertEqual(stdout, TEST_DATA,
  467. 'Received data differs from what was sent')
  468. self.assertFalse(stderr,
  469. 'Some data was printed to stderr')
  470. self.assertFalse(service_stderr,
  471. 'Some data was printed to stderr')
  472. def test_092_qrexec_service_socket_dom0_eof_reverse(self):
  473. """Test for EOF transmission dom0(socket)->VM"""
  474. self.loop.run_until_complete(self.testvm1.start())
  475. self.create_local_file(
  476. '/tmp/service_script',
  477. '#!/usr/bin/python3\n'
  478. 'import socket, os, sys, time\n'
  479. 's = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n'
  480. 'os.umask(0)\n'
  481. 's.bind("/etc/qubes-rpc/test.Socket")\n'
  482. 's.listen(1)\n'
  483. 'conn, addr = s.accept()\n'
  484. 'conn.send(b"test\\n")\n'
  485. 'conn.shutdown(socket.SHUT_WR)\n'
  486. # wait longer than the timeout below
  487. 'time.sleep(15)\n'
  488. )
  489. self.service_proc = self.loop.run_until_complete(
  490. asyncio.create_subprocess_shell('python3 /tmp/service_script',
  491. stdout=subprocess.PIPE, stdin=subprocess.PIPE))
  492. try:
  493. with self.qrexec_policy('test.Socket', self.testvm1, '@adminvm'):
  494. p = self.loop.run_until_complete(self.testvm1.run(
  495. 'qrexec-client-vm @adminvm test.Socket',
  496. stdout=subprocess.PIPE, stdin=subprocess.PIPE))
  497. stdout = self.loop.run_until_complete(asyncio.wait_for(
  498. p.stdout.read(),
  499. timeout=10))
  500. except asyncio.TimeoutError:
  501. self.fail(
  502. "service timeout, probably EOF wasn't transferred from the VM process")
  503. finally:
  504. with contextlib.suppress(ProcessLookupError):
  505. p.terminate()
  506. self.loop.run_until_complete(p.wait())
  507. self.assertEqual(stdout, b'test\n',
  508. 'Received data differs from what was expected')
  509. def test_093_qrexec_service_socket_dom0_eof(self):
  510. """Test for EOF transmission VM->dom0(socket)"""
  511. self.loop.run_until_complete(self.testvm1.start())
  512. self.create_local_file(
  513. '/tmp/service_script',
  514. '#!/usr/bin/python3\n'
  515. 'import socket, os, sys, time\n'
  516. 's = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n'
  517. 'os.umask(0)\n'
  518. 's.bind("/etc/qubes-rpc/test.Socket")\n'
  519. 's.listen(1)\n'
  520. 'conn, addr = s.accept()\n'
  521. 'buf = conn.recv(100)\n'
  522. 'sys.stdout.buffer.write(buf)\n'
  523. 'buf = conn.recv(10)\n'
  524. 'sys.stdout.buffer.write(buf)\n'
  525. 'sys.stdout.buffer.flush()\n'
  526. 'os.close(1)\n'
  527. # wait longer than the timeout below
  528. 'time.sleep(15)\n'
  529. )
  530. self.service_proc = self.loop.run_until_complete(
  531. asyncio.create_subprocess_shell('python3 /tmp/service_script',
  532. stdout=subprocess.PIPE, stdin=subprocess.PIPE))
  533. try:
  534. with self.qrexec_policy('test.Socket', self.testvm1, '@adminvm'):
  535. p = self.loop.run_until_complete(self.testvm1.run(
  536. 'qrexec-client-vm @adminvm test.Socket',
  537. stdin=subprocess.PIPE))
  538. p.stdin.write(b'test1test2')
  539. p.stdin.write_eof()
  540. service_stdout = self.loop.run_until_complete(asyncio.wait_for(
  541. self.service_proc.stdout.read(),
  542. timeout=10))
  543. except asyncio.TimeoutError:
  544. self.fail(
  545. "service timeout, probably EOF wasn't transferred from the VM process")
  546. finally:
  547. with contextlib.suppress(ProcessLookupError):
  548. p.terminate()
  549. self.loop.run_until_complete(p.wait())
  550. service_descriptor = b'test.Socket+ test-inst-vm1 keyword adminvm\0'
  551. self.assertEqual(service_stdout, service_descriptor + b'test1test2',
  552. 'Received data differs from what was expected')
  553. def _wait_for_socket_setup(self):
  554. try:
  555. self.loop.run_until_complete(asyncio.wait_for(
  556. self.testvm1.run_for_stdio(
  557. 'while ! test -e /etc/qubes-rpc/test.Socket; do sleep 0.1; done'),
  558. timeout=10))
  559. except asyncio.TimeoutError:
  560. self.fail(
  561. "waiting for /etc/qubes-rpc/test.Socket in VM timed out")
  562. def test_095_qrexec_service_socket_vm(self):
  563. """Basic test socket services (VM) - receive"""
  564. self.loop.run_until_complete(self.testvm1.start())
  565. self.service_proc = self.loop.run_until_complete(self.testvm1.run(
  566. 'socat -u UNIX-LISTEN:/etc/qubes-rpc/test.Socket,mode=666 -',
  567. stdout=subprocess.PIPE, stdin=subprocess.PIPE,
  568. user='root'))
  569. self._wait_for_socket_setup()
  570. try:
  571. (stdout, stderr) = self.loop.run_until_complete(asyncio.wait_for(
  572. self.testvm1.run_service_for_stdio('test.Socket+', input=TEST_DATA),
  573. timeout=10))
  574. except subprocess.CalledProcessError as e:
  575. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  576. e.cmd, e.returncode, e.stderr))
  577. except asyncio.TimeoutError:
  578. self.fail(
  579. "service timeout, probably EOF wasn't transferred to the VM process")
  580. try:
  581. (service_stdout, service_stderr) = self.loop.run_until_complete(
  582. asyncio.wait_for(
  583. self.service_proc.communicate(),
  584. timeout=10))
  585. except asyncio.TimeoutError:
  586. self.fail(
  587. "socat timeout, probably EOF wasn't transferred to the VM process")
  588. service_descriptor = b'test.Socket+ dom0\0'
  589. self.assertEqual(service_stdout, service_descriptor + TEST_DATA,
  590. 'Received data differs from what was sent')
  591. self.assertFalse(stderr,
  592. 'Some data was printed to stderr')
  593. self.assertFalse(service_stderr,
  594. 'Some data was printed to stderr')
  595. def test_096_qrexec_service_socket_vm_send(self):
  596. """Basic test socket services (VM) - send"""
  597. self.loop.run_until_complete(self.testvm1.start())
  598. self.create_remote_file(self.testvm1,
  599. '/tmp/service-input',
  600. TEST_DATA.decode())
  601. self.service_proc = self.loop.run_until_complete(self.testvm1.run(
  602. 'socat -u OPEN:/tmp/service-input UNIX-LISTEN:/etc/qubes-rpc/test.Socket,mode=666',
  603. user='root'))
  604. self._wait_for_socket_setup()
  605. try:
  606. (stdout, stderr) = self.loop.run_until_complete(asyncio.wait_for(
  607. self.testvm1.run_service_for_stdio('test.Socket+'),
  608. timeout=10))
  609. except subprocess.CalledProcessError as e:
  610. self.fail('{} exited with non-zero code {}; stderr: {}'.format(
  611. e.cmd, e.returncode, e.stderr))
  612. except asyncio.TimeoutError:
  613. self.fail(
  614. "service timeout, probably EOF wasn't transferred to the VM process")
  615. try:
  616. (service_stdout, service_stderr) = self.loop.run_until_complete(
  617. asyncio.wait_for(
  618. self.service_proc.communicate(),
  619. timeout=10))
  620. except asyncio.TimeoutError:
  621. self.fail(
  622. "socat timeout, probably EOF wasn't transferred to the VM process")
  623. self.assertEqual(stdout, TEST_DATA,
  624. 'Received data differs from what was sent')
  625. self.assertFalse(stderr,
  626. 'Some data was printed to stderr')
  627. self.assertFalse(service_stderr,
  628. 'Some data was printed to stderr')
  629. def test_097_qrexec_service_socket_vm_eof_reverse(self):
  630. """Test for EOF transmission VM(socket)->dom0"""
  631. self.loop.run_until_complete(self.testvm1.start())
  632. self.create_remote_file(self.testvm1,
  633. '/tmp/service_script',
  634. '#!/usr/bin/python3\n'
  635. 'import socket, os, sys, time\n'
  636. 's = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n'
  637. 'os.umask(0)\n'
  638. 's.bind("/etc/qubes-rpc/test.Socket")\n'
  639. 's.listen(1)\n'
  640. 'conn, addr = s.accept()\n'
  641. 'conn.send(b"test\\n")\n'
  642. 'conn.shutdown(socket.SHUT_WR)\n'
  643. # wait longer than the timeout below
  644. 'time.sleep(15)\n'
  645. )
  646. self.service_proc = self.loop.run_until_complete(self.testvm1.run(
  647. 'python3 /tmp/service_script',
  648. stdout=subprocess.PIPE, stdin=subprocess.PIPE,
  649. user='root'))
  650. self._wait_for_socket_setup()
  651. try:
  652. p = self.loop.run_until_complete(
  653. self.testvm1.run_service('test.Socket+',
  654. stdin=subprocess.PIPE, stdout=subprocess.PIPE))
  655. stdout = self.loop.run_until_complete(asyncio.wait_for(p.stdout.read(),
  656. timeout=10))
  657. except asyncio.TimeoutError:
  658. p.terminate()
  659. self.fail(
  660. "service timeout, probably EOF wasn't transferred from the VM process")
  661. finally:
  662. self.loop.run_until_complete(p.wait())
  663. self.assertEqual(stdout,
  664. b'test\n',
  665. 'Received data differs from what was expected')
  666. def test_098_qrexec_service_socket_vm_eof(self):
  667. """Test for EOF transmission dom0->VM(socket)"""
  668. self.loop.run_until_complete(self.testvm1.start())
  669. self.create_remote_file(
  670. self.testvm1,
  671. '/tmp/service_script',
  672. '#!/usr/bin/python3\n'
  673. 'import socket, os, sys, time\n'
  674. 's = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n'
  675. 'os.umask(0)\n'
  676. 's.bind("/etc/qubes-rpc/test.Socket")\n'
  677. 's.listen(1)\n'
  678. 'conn, addr = s.accept()\n'
  679. 'buf = conn.recv(100)\n'
  680. 'sys.stdout.buffer.write(buf)\n'
  681. 'buf = conn.recv(10)\n'
  682. 'sys.stdout.buffer.write(buf)\n'
  683. 'sys.stdout.buffer.flush()\n'
  684. 'os.close(1)\n'
  685. # wait longer than the timeout below
  686. 'time.sleep(15)\n'
  687. )
  688. self.service_proc = self.loop.run_until_complete(self.testvm1.run(
  689. 'python3 /tmp/service_script',
  690. stdout=subprocess.PIPE, stdin=subprocess.PIPE,
  691. user='root'))
  692. self._wait_for_socket_setup()
  693. try:
  694. p = self.loop.run_until_complete(
  695. self.testvm1.run_service('test.Socket+',
  696. stdin=subprocess.PIPE, stdout=subprocess.PIPE))
  697. p.stdin.write(b'test1test2')
  698. self.loop.run_until_complete(
  699. asyncio.wait_for(p.stdin.drain(), timeout=10))
  700. p.stdin.close()
  701. service_stdout = self.loop.run_until_complete(asyncio.wait_for(
  702. self.service_proc.stdout.read(),
  703. timeout=10))
  704. except asyncio.TimeoutError:
  705. p.terminate()
  706. self.fail(
  707. "service timeout, probably EOF wasn't transferred to the VM process")
  708. finally:
  709. self.loop.run_until_complete(p.wait())
  710. service_descriptor = b'test.Socket+ dom0\0'
  711. self.assertEqual(service_stdout, service_descriptor + b'test1test2',
  712. 'Received data differs from what was expected')
  713. def create_testcases_for_templates():
  714. return qubes.tests.create_testcases_for_templates('TC_00_Qrexec',
  715. TC_00_QrexecMixin, qubes.tests.SystemTestCase,
  716. module=sys.modules[__name__])
  717. def load_tests(loader, tests, pattern):
  718. tests.addTests(loader.loadTestsFromNames(
  719. create_testcases_for_templates()))
  720. return tests
  721. qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)