vm_qrexec_gui.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2014-2015
  5. # Marek Marczykowski-Górecki <marmarek@invisiblethingslab.com>
  6. # Copyright (C) 2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program 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
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. #
  22. from distutils import spawn
  23. import asyncio
  24. import multiprocessing
  25. import os
  26. import shlex
  27. import subprocess
  28. import time
  29. import unittest
  30. import qubes.config
  31. import qubes.tests
  32. import qubes.vm.appvm
  33. import qubes.vm.templatevm
  34. import re
  35. TEST_DATA = b"0123456789" * 1024
  36. class TC_00_AppVMMixin(qubes.tests.SystemTestsMixin):
  37. def setUp(self):
  38. super(TC_00_AppVMMixin, self).setUp()
  39. self.init_default_template(self.template)
  40. if self._testMethodName == 'test_210_time_sync':
  41. self.init_networking()
  42. self.testvm1 = self.app.add_new_vm(
  43. qubes.vm.appvm.AppVM,
  44. label='red',
  45. name=self.make_vm_name('vm1'),
  46. template=self.app.domains[self.template])
  47. self.loop.run_until_complete(self.testvm1.create_on_disk())
  48. self.testvm2 = self.app.add_new_vm(
  49. qubes.vm.appvm.AppVM,
  50. label='red',
  51. name=self.make_vm_name('vm2'),
  52. template=self.app.domains[self.template])
  53. self.loop.run_until_complete(self.testvm2.create_on_disk())
  54. self.app.save()
  55. def create_local_file(self, filename, content, mode='w'):
  56. with open(filename, mode) as file:
  57. file.write(content)
  58. self.addCleanup(os.unlink, filename)
  59. def create_remote_file(self, vm, filename, content):
  60. self.loop.run_until_complete(vm.run_for_stdio(
  61. 'cat > {}'.format(shlex.quote(filename)),
  62. user='root', input=content.encode('utf-8')))
  63. def test_000_start_shutdown(self):
  64. self.loop.run_until_complete(self.testvm1.start())
  65. self.assertEqual(self.testvm1.get_power_state(), "Running")
  66. self.loop.run_until_complete(self.testvm1.shutdown())
  67. shutdown_counter = 0
  68. while self.testvm1.is_running():
  69. if shutdown_counter > qubes.config.defaults["shutdown_counter_max"]:
  70. self.fail("VM hanged during shutdown")
  71. shutdown_counter += 1
  72. time.sleep(1)
  73. time.sleep(1)
  74. self.assertEqual(self.testvm1.get_power_state(), "Halted")
  75. @unittest.skipUnless(spawn.find_executable('xdotool'),
  76. "xdotool not installed")
  77. def test_010_run_xterm(self):
  78. self.loop.run_until_complete(self.testvm1.start())
  79. self.assertEqual(self.testvm1.get_power_state(), "Running")
  80. p = self.loop.run_until_complete(self.testvm1.run('xterm'))
  81. try:
  82. wait_count = 0
  83. title = 'user@{}'.format(self.testvm1.name)
  84. if self.template.count("whonix"):
  85. title = 'user@host'
  86. while subprocess.call(
  87. ['xdotool', 'search', '--name', title],
  88. stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) > 0:
  89. wait_count += 1
  90. if wait_count > 100:
  91. self.fail("Timeout while waiting for xterm window")
  92. time.sleep(0.1)
  93. time.sleep(0.5)
  94. subprocess.check_call(
  95. ['xdotool', 'search', '--name', title,
  96. 'windowactivate', 'type', 'exit\n'])
  97. wait_count = 0
  98. while subprocess.call(['xdotool', 'search', '--name', title],
  99. stdout=open(os.path.devnull, 'w'),
  100. stderr=subprocess.STDOUT) == 0:
  101. wait_count += 1
  102. if wait_count > 100:
  103. self.fail("Timeout while waiting for xterm "
  104. "termination")
  105. time.sleep(0.1)
  106. finally:
  107. p.terminate()
  108. self.loop.run_until_complete(p.wait())
  109. @unittest.skipUnless(spawn.find_executable('xdotool'),
  110. "xdotool not installed")
  111. def test_011_run_gnome_terminal(self):
  112. if "minimal" in self.template:
  113. self.skipTest("Minimal template doesn't have 'gnome-terminal'")
  114. self.loop.run_until_complete(self.testvm1.start())
  115. self.assertEqual(self.testvm1.get_power_state(), "Running")
  116. p = self.loop.run_until_complete(self.testvm1.run('gnome-terminal'))
  117. try:
  118. title = 'user@{}'.format(self.testvm1.name)
  119. if self.template.count("whonix"):
  120. title = 'user@host'
  121. wait_count = 0
  122. while subprocess.call(
  123. ['xdotool', 'search', '--name', title],
  124. stdout=open(os.path.devnull, 'w'),
  125. stderr=subprocess.STDOUT) > 0:
  126. wait_count += 1
  127. if wait_count > 100:
  128. self.fail("Timeout while waiting for gnome-terminal window")
  129. time.sleep(0.1)
  130. time.sleep(0.5)
  131. subprocess.check_call(
  132. ['xdotool', 'search', '--name', title,
  133. 'windowactivate', '--sync', 'type', 'exit\n'])
  134. wait_count = 0
  135. while subprocess.call(['xdotool', 'search', '--name', title],
  136. stdout=open(os.path.devnull, 'w'),
  137. stderr=subprocess.STDOUT) == 0:
  138. wait_count += 1
  139. if wait_count > 100:
  140. self.fail("Timeout while waiting for gnome-terminal "
  141. "termination")
  142. time.sleep(0.1)
  143. finally:
  144. p.terminate()
  145. self.loop.run_until_complete(p.wait())
  146. @unittest.skipUnless(spawn.find_executable('xdotool'),
  147. "xdotool not installed")
  148. @unittest.expectedFailure
  149. def test_012_qubes_desktop_run(self):
  150. self.loop.run_until_complete(self.testvm1.start())
  151. self.assertEqual(self.testvm1.get_power_state(), "Running")
  152. xterm_desktop_path = "/usr/share/applications/xterm.desktop"
  153. # Debian has it different...
  154. xterm_desktop_path_debian = \
  155. "/usr/share/applications/debian-xterm.desktop"
  156. try:
  157. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  158. 'test -r {}'.format(xterm_desktop_path_debian)))
  159. except subprocess.CalledProcessError:
  160. pass
  161. else:
  162. xterm_desktop_path = xterm_desktop_path_debian
  163. self.loop.run_until_complete(
  164. self.testvm1.run('qubes-desktop-run {}'.format(xterm_desktop_path)))
  165. title = 'user@{}'.format(self.testvm1.name)
  166. if self.template.count("whonix"):
  167. title = 'user@host'
  168. wait_count = 0
  169. while subprocess.call(
  170. ['xdotool', 'search', '--name', title],
  171. stdout=open(os.path.devnull, 'w'),
  172. stderr=subprocess.STDOUT) > 0:
  173. wait_count += 1
  174. if wait_count > 100:
  175. self.fail("Timeout while waiting for xterm window")
  176. time.sleep(0.1)
  177. time.sleep(0.5)
  178. subprocess.check_call(
  179. ['xdotool', 'search', '--name', title,
  180. 'windowactivate', '--sync', 'type', 'exit\n'])
  181. wait_count = 0
  182. while subprocess.call(['xdotool', 'search', '--name', title],
  183. stdout=open(os.path.devnull, 'w'),
  184. stderr=subprocess.STDOUT) == 0:
  185. wait_count += 1
  186. if wait_count > 100:
  187. self.fail("Timeout while waiting for xterm "
  188. "termination")
  189. time.sleep(0.1)
  190. def test_050_qrexec_simple_eof(self):
  191. """Test for data and EOF transmission dom0->VM"""
  192. # XXX is this still correct? this is no longer simple qrexec,
  193. # but qubes.VMShell
  194. self.loop.run_until_complete(self.testvm1.start())
  195. try:
  196. (stdout, stderr) = self.loop.run_until_complete(asyncio.wait_for(
  197. self.testvm1.run_for_stdio('cat', input=TEST_DATA),
  198. timeout=10))
  199. except asyncio.TimeoutError:
  200. self.fail(
  201. "Timeout, probably EOF wasn't transferred to the VM process")
  202. self.assertEqual(stdout, TEST_DATA,
  203. 'Received data differs from what was sent')
  204. self.assertFalse(stderr,
  205. 'Some data was printed to stderr')
  206. def test_051_qrexec_simple_eof_reverse(self):
  207. """Test for EOF transmission VM->dom0"""
  208. @asyncio.coroutine
  209. def run(self):
  210. p = yield from self.testvm1.run(
  211. 'echo test; exec >&-; cat > /dev/null',
  212. stdin=subprocess.PIPE,
  213. stdout=subprocess.PIPE,
  214. stderr=subprocess.PIPE)
  215. # this will hang on test failure
  216. stdout = yield from asyncio.wait_for(p.stdout.read(), timeout=10)
  217. p.stdin.write(TEST_DATA)
  218. yield from p.stdin.drain()
  219. p.stdin.close()
  220. self.assertEqual(stdout.strip(), 'test',
  221. 'Received data differs from what was expected')
  222. # this may hang in some buggy cases
  223. self.assertFalse((yield from p.stderr.read()),
  224. 'Some data was printed to stderr')
  225. try:
  226. yield from asyncio.wait_for(p.wait(), timeout=1)
  227. except asyncio.TimeoutError:
  228. self.fail("Timeout, "
  229. "probably EOF wasn't transferred from the VM process")
  230. self.loop.run_until_complete(self.testvm1.start())
  231. self.loop.run_until_complete(run(self))
  232. def test_052_qrexec_vm_service_eof(self):
  233. """Test for EOF transmission VM(src)->VM(dst)"""
  234. self.loop.run_until_complete(asyncio.wait([
  235. self.testvm1.start(),
  236. self.testvm2.start()]))
  237. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  238. 'cat > /etc/qubes-rpc/test.EOF',
  239. user='root',
  240. input=b'/bin/cat'))
  241. with self.qrexec_policy('test.EOF', self.testvm1, self.testvm2):
  242. try:
  243. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  244. self.testvm1.run_for_stdio('''\
  245. /usr/lib/qubes/qrexec-client-vm {} test.EOF \
  246. /bin/sh -c 'echo test; exec >&-; cat >&$SAVED_FD_1'
  247. '''.format(self.testvm2.name)),
  248. timeout=10))
  249. except asyncio.TimeoutError:
  250. self.fail("Timeout, probably EOF wasn't transferred")
  251. self.assertEqual(stdout, b'test',
  252. 'Received data differs from what was expected')
  253. @unittest.expectedFailure
  254. def test_053_qrexec_vm_service_eof_reverse(self):
  255. """Test for EOF transmission VM(src)<-VM(dst)"""
  256. self.loop.run_until_complete(asyncio.wait([
  257. self.testvm1.start(),
  258. self.testvm2.start()]))
  259. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.EOF',
  260. 'echo test; exec >&-; cat >/dev/null')
  261. with self.qrexec_policy('test.EOF', self.testvm1, self.testvm2):
  262. try:
  263. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  264. self.testvm1.run_for_stdio('''\
  265. /usr/lib/qubes/qrexec-client-vm {} test.EOF \
  266. /bin/sh -c 'cat >&$SAVED_FD_1'
  267. '''.format(self.testvm2.name)),
  268. timeout=10))
  269. except asyncio.TimeoutError:
  270. self.fail("Timeout, probably EOF wasn't transferred")
  271. self.assertEqual(stdout, b'test',
  272. 'Received data differs from what was expected')
  273. def test_055_qrexec_dom0_service_abort(self):
  274. """
  275. Test if service abort (by dom0) is properly handled by source VM.
  276. If "remote" part of the service terminates, the source part should
  277. properly be notified. This includes closing its stdin (which is
  278. already checked by test_053_qrexec_vm_service_eof_reverse), but also
  279. its stdout - otherwise such service might hang on write(2) call.
  280. """
  281. self.loop.run_until_complete(self.testvm1.start())
  282. self.create_local_file('/etc/qubes-rpc/test.Abort',
  283. 'sleep 1')
  284. with self.qrexec_policy('test.Abort', self.testvm1, 'dom0'):
  285. try:
  286. stdout, _ = self.loop.run_until_complete(asyncio.wait_for(
  287. self.testvm1.run_for_stdio('''\
  288. /usr/lib/qubes/qrexec-client-vm dom0 test.Abort \
  289. /bin/cat /dev/zero'''),
  290. timeout=10))
  291. except asyncio.TimeoutError:
  292. self.fail("Timeout, probably stdout wasn't closed")
  293. def test_060_qrexec_exit_code_dom0(self):
  294. self.loop.run_until_complete(self.testvm1.start())
  295. self.loop.run_until_complete(self.testvm1.run_for_stdio('exit 0'))
  296. with self.assertRaises(subprocess.CalledProcessError):
  297. self.loop.run_until_complete(self.testvm1.run_for_stdio('exit 3'))
  298. @unittest.expectedFailure
  299. def test_065_qrexec_exit_code_vm(self):
  300. self.loop.run_until_complete(asyncio.wait([
  301. self.testvm1.start(),
  302. self.testvm2.start()]))
  303. with self.qrexec_policy('test.Retcode', self.testvm1, self.testvm2):
  304. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Retcode',
  305. 'exit 0')
  306. (stdout, stderr) = self.loop.run_until_complete(
  307. self.testvm1.run_for_stdio('''\
  308. /usr/lib/qubes/qrexec-client-vm {} test.Retcode \
  309. /bin/sh -c 'cat >/dev/null';
  310. echo $?'''.format(self.testvm1.name)))
  311. self.assertEqual(stdout, b'0\n')
  312. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Retcode',
  313. 'exit 3')
  314. (stdout, stderr) = self.loop.run_until_complete(
  315. self.testvm1.run_for_stdio('''\
  316. /usr/lib/qubes/qrexec-client-vm {} test.Retcode \
  317. /bin/sh -c 'cat >/dev/null';
  318. echo $?'''.format(self.testvm1.name)))
  319. self.assertEqual(stdout, b'3\n')
  320. def test_070_qrexec_vm_simultaneous_write(self):
  321. """Test for simultaneous write in VM(src)->VM(dst) connection
  322. This is regression test for #1347
  323. Check for deadlock when initially both sides writes a lot of data
  324. (and not read anything). When one side starts reading, it should
  325. get the data and the remote side should be possible to write then more.
  326. There was a bug where remote side was waiting on write(2) and not
  327. handling anything else.
  328. """
  329. self.loop.run_until_complete(asyncio.wait([
  330. self.testvm1.start(),
  331. self.testvm2.start()]))
  332. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
  333. # first write a lot of data
  334. dd if=/dev/zero bs=993 count=10000 iflag=fullblock
  335. # and only then read something
  336. dd of=/dev/null bs=993 count=10000 iflag=fullblock
  337. ''')
  338. with self.qrexec_policy('test.write', self.testvm1, self.testvm2):
  339. try:
  340. self.loop.run_until_complete(asyncio.wait_for(
  341. # first write a lot of data to fill all the buffers
  342. # then after some time start reading
  343. self.testvm1.run_for_stdio('''\
  344. /usr/lib/qubes/qrexec-client-vm {} test.write \
  345. /bin/sh -c '
  346. dd if=/dev/zero bs=993 count=10000 iflag=fullblock &
  347. sleep 1;
  348. dd of=/dev/null bs=993 count=10000 iflag=fullblock;
  349. wait'
  350. '''.format(self.testvm2.name)), timeout=10))
  351. except subprocess.CalledProcessError:
  352. self.fail('Service call failed')
  353. except asyncio.TimeoutError:
  354. self.fail('Timeout, probably deadlock')
  355. @unittest.skip('localcmd= argument went away')
  356. def test_071_qrexec_dom0_simultaneous_write(self):
  357. """Test for simultaneous write in dom0(src)->VM(dst) connection
  358. Similar to test_070_qrexec_vm_simultaneous_write, but with dom0
  359. as a source.
  360. """
  361. def run(self):
  362. result.value = self.testvm2.run_service(
  363. "test.write", localcmd="/bin/sh -c '"
  364. # first write a lot of data to fill all the buffers
  365. "dd if=/dev/zero bs=993 count=10000 iflag=fullblock & "
  366. # then after some time start reading
  367. "sleep 1; "
  368. "dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
  369. "wait"
  370. "'")
  371. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.write', '''\
  372. # first write a lot of data
  373. dd if=/dev/zero bs=993 count=10000 iflag=fullblock
  374. # and only then read something
  375. dd of=/dev/null bs=993 count=10000 iflag=fullblock
  376. ''')
  377. self.create_local_file('/etc/qubes-rpc/policy/test.write',
  378. '{} {} allow'.format(self.testvm1.name, self.testvm2.name))
  379. t = multiprocessing.Process(target=run, args=(self,))
  380. t.start()
  381. t.join(timeout=10)
  382. if t.is_alive():
  383. t.terminate()
  384. self.fail("Timeout, probably deadlock")
  385. self.assertEqual(result.value, 0, "Service call failed")
  386. @unittest.skip('localcmd= argument went away')
  387. def test_072_qrexec_to_dom0_simultaneous_write(self):
  388. """Test for simultaneous write in dom0(src)<-VM(dst) connection
  389. Similar to test_071_qrexec_dom0_simultaneous_write, but with dom0
  390. as a "hanging" side.
  391. """
  392. result = multiprocessing.Value('i', -1)
  393. def run(self):
  394. result.value = self.testvm2.run_service(
  395. "test.write", localcmd="/bin/sh -c '"
  396. # first write a lot of data to fill all the buffers
  397. "dd if=/dev/zero bs=993 count=10000 iflag=fullblock "
  398. # then, only when all written, read something
  399. "dd of=/dev/null bs=993 count=10000 iflag=fullblock; "
  400. "'")
  401. self.loop.run_until_complete(self.testvm2.start())
  402. p = self.testvm2.run("cat > /etc/qubes-rpc/test.write", user="root",
  403. passio_popen=True)
  404. # first write a lot of data
  405. p.stdin.write(b"dd if=/dev/zero bs=993 count=10000 iflag=fullblock &\n")
  406. # and only then read something
  407. p.stdin.write(b"dd of=/dev/null bs=993 count=10000 iflag=fullblock\n")
  408. p.stdin.write(b"sleep 1; \n")
  409. p.stdin.write(b"wait\n")
  410. p.stdin.close()
  411. p.wait()
  412. policy = open("/etc/qubes-rpc/policy/test.write", "w")
  413. policy.write("%s %s allow" % (self.testvm1.name, self.testvm2.name))
  414. policy.close()
  415. self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.write")
  416. t = multiprocessing.Process(target=run, args=(self,))
  417. t.start()
  418. t.join(timeout=10)
  419. if t.is_alive():
  420. t.terminate()
  421. self.fail("Timeout, probably deadlock")
  422. self.assertEqual(result.value, 0, "Service call failed")
  423. def test_080_qrexec_service_argument_allow_default(self):
  424. """Qrexec service call with argument"""
  425. self.loop.run_until_complete(asyncio.wait([
  426. self.testvm1.start(),
  427. self.testvm2.start()]))
  428. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  429. '/usr/bin/printf %s "$1"')
  430. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  431. stdout, stderr = self.loop.run_until_complete(
  432. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  433. '{} test.Argument+argument'.format(self.testvm2.name)))
  434. self.assertEqual(stdout, b'argument')
  435. def test_081_qrexec_service_argument_allow_specific(self):
  436. """Qrexec service call with argument - allow only specific value"""
  437. self.loop.run_until_complete(asyncio.wait([
  438. self.testvm1.start(),
  439. self.testvm2.start()]))
  440. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  441. '/usr/bin/printf %s "$1"')
  442. with self.qrexec_policy('test.Argument', '$anyvm', '$anyvm', False):
  443. with self.qrexec_policy('test.Argument+argument',
  444. self.testvm1.name, self.testvm2.name):
  445. stdout, stderr = self.loop.run_until_complete(self.testvm1.run(
  446. '/usr/lib/qubes/qrexec-client-vm '
  447. '{} test.Argument+argument'.format(self.testvm2.name)))
  448. self.assertEqual(stdout, b'argument')
  449. def test_082_qrexec_service_argument_deny_specific(self):
  450. """Qrexec service call with argument - deny specific value"""
  451. self.loop.run_until_complete(asyncio.wait([
  452. self.testvm1.start(),
  453. self.testvm2.start()]))
  454. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  455. '/usr/bin/printf %s "$1"')
  456. with self.qrexec_policy('test.Argument', '$anyvm', '$anyvm'):
  457. with self.qrexec_policy('test.Argument+argument',
  458. self.testvm1, self.testvm2, allow=False):
  459. with self.assertRaises(subprocess.CalledProcessError,
  460. msg='Service request should be denied'):
  461. self.loop.run_until_complete(
  462. self.testvm1.run('/usr/lib/qubes/qrexec-client-vm {} '
  463. 'test.Argument+argument'.format(self.testvm2.name)))
  464. def test_083_qrexec_service_argument_specific_implementation(self):
  465. """Qrexec service call with argument - argument specific
  466. implementatation"""
  467. self.loop.run_until_complete(asyncio.wait([
  468. self.testvm1.start(),
  469. self.testvm2.start()]))
  470. self.create_remote_file(self.testvm2,
  471. '/etc/qubes-rpc/test.Argument',
  472. '/usr/bin/printf %s "$1"')
  473. self.create_remote_file(self.testvm2,
  474. '/etc/qubes-rpc/test.Argument+argument',
  475. '/usr/bin/printf "specific: %s" "$1"')
  476. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  477. stdout, stderr = self.loop.run_until_complete(
  478. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  479. '{} test.Argument+argument'.format(self.testvm2.name)))
  480. self.assertEqual(stdout, b'specific: argument')
  481. def test_084_qrexec_service_argument_extra_env(self):
  482. """Qrexec service call with argument - extra env variables"""
  483. self.loop.run_until_complete(asyncio.wait([
  484. self.testvm1.start(),
  485. self.testvm2.start()]))
  486. self.create_remote_file(self.testvm2, '/etc/qubes-rpc/test.Argument',
  487. '/usr/bin/printf "%s %s" '
  488. '"$QREXEC_SERVICE_FULL_NAME" "$QREXEC_SERVICE_ARGUMENT"')
  489. with self.qrexec_policy('test.Argument', self.testvm1, self.testvm2):
  490. stdout, stderr = self.loop.run_until_complete(
  491. self.testvm1.run_for_stdio('/usr/lib/qubes/qrexec-client-vm '
  492. '{} test.Argument+argument'.format(self.testvm2.name)))
  493. self.assertEqual(stdout, b'test.Argument+argument argument')
  494. def test_100_qrexec_filecopy(self):
  495. self.loop.run_until_complete(asyncio.wait([
  496. self.testvm1.start(),
  497. self.testvm2.start()]))
  498. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  499. try:
  500. stdout, stderr = self.loop.run_until_complete(
  501. self.testvm1.run_for_stdio(
  502. 'qvm-copy-to-vm {} /etc/passwd'.format(
  503. self.testvm2.name)))
  504. except subprocess.CalledProcessError:
  505. self.fail('qvm-copy-to-vm failed: {}'.format(stderr))
  506. try:
  507. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  508. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  509. self.testvm1.name)))
  510. except subprocess.CalledProcessError:
  511. self.fail('file differs')
  512. try:
  513. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  514. 'test -f /etc/passwd'))
  515. except subprocess.CalledProcessError:
  516. self.fail('source file got removed')
  517. def test_105_qrexec_filemove(self):
  518. self.loop.run_until_complete(asyncio.wait([
  519. self.testvm1.start(),
  520. self.testvm2.start()]))
  521. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  522. 'cp /etc/passwd passwd'))
  523. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  524. try:
  525. stdout, stderr = self.loop.run_until_complete(
  526. self.testvm1.run_for_stdio(
  527. 'qvm-move-to-vm {} passwd'.format(self.testvm2.name)))
  528. except subprocess.CalledProcessError:
  529. self.fail('qvm-move-to-vm failed: {}'.format(stderr))
  530. try:
  531. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  532. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  533. self.testvm1.name)))
  534. except subprocess.CalledProcessError:
  535. self.fail('file differs')
  536. with self.assertRaises(subprocess.CalledProcessError):
  537. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  538. 'test -f passwd'))
  539. def test_101_qrexec_filecopy_with_autostart(self):
  540. self.loop.run_until_complete(self.testvm1.start())
  541. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  542. try:
  543. stdout, stderr = self.loop.run_until_complete(
  544. self.testvm1.run_for_stdio(
  545. 'qvm-copy-to-vm {} /etc/passwd'.format(
  546. self.testvm2.name)))
  547. except subprocess.CalledProcessError:
  548. self.fail('qvm-copy-to-vm failed: {}'.format(stderr))
  549. # workaround for libvirt bug (domain ID isn't updated when is started
  550. # from other application) - details in
  551. # QubesOS/qubes-core-libvirt@63ede4dfb4485c4161dd6a2cc809e8fb45ca664f
  552. # XXX is it still true with qubesd? --woju 20170523
  553. self.testvm2._libvirt_domain = None
  554. self.assertTrue(self.testvm2.is_running())
  555. try:
  556. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  557. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  558. self.testvm1.name)))
  559. except subprocess.CalledProcessError:
  560. self.fail('file differs')
  561. try:
  562. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  563. 'test -f /etc/passwd'))
  564. except subprocess.CalledProcessError:
  565. self.fail('source file got removed')
  566. def test_110_qrexec_filecopy_deny(self):
  567. self.loop.run_until_complete(asyncio.wait([
  568. self.testvm1.start(),
  569. self.testvm2.start()]))
  570. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2,
  571. allow=False):
  572. with self.assertRaises(subprocess.CalledProcessError):
  573. stdout, stderr = self.loop.run_until_complete(
  574. self.testvm1.run_for_stdio(
  575. 'qvm-copy-to-vm {} /etc/passwd'.format(
  576. self.testvm2.name)))
  577. with self.assertRaises(subprocess.CalledProcessError):
  578. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  579. 'test -d /home/user/QubesIncoming/{}'.format(
  580. self.testvm1.name)))
  581. @unittest.skip("Xen gntalloc driver crashes when page is mapped in the "
  582. "same domain")
  583. def test_120_qrexec_filecopy_self(self):
  584. self.testvm1.start()
  585. self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
  586. self.testvm1.name)
  587. p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
  588. self.testvm1.name, passio_popen=True,
  589. passio_stderr=True)
  590. p.wait()
  591. self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
  592. p.stderr.read())
  593. retcode = self.testvm1.run(
  594. "diff /etc/passwd /home/user/QubesIncoming/{}/passwd".format(
  595. self.testvm1.name),
  596. wait=True)
  597. self.assertEqual(retcode, 0, "file differs")
  598. @unittest.skipUnless(spawn.find_executable('xdotool'),
  599. "xdotool not installed")
  600. def test_130_qrexec_filemove_disk_full(self):
  601. self.loop.run_until_complete(asyncio.wait([
  602. self.testvm1.start(),
  603. self.testvm2.start()]))
  604. # Prepare test file
  605. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  606. 'yes teststring | dd of=testfile bs=1M count=50 iflag=fullblock'))
  607. # Prepare target directory with limited size
  608. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  609. 'mkdir -p /home/user/QubesIncoming && '
  610. 'chown user /home/user/QubesIncoming && '
  611. 'mount -t tmpfs none /home/user/QubesIncoming -o size=48M',
  612. user='root'))
  613. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  614. with self.assertRaises(subprocess.CalledProcessError):
  615. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  616. 'qvm-move-to-vm {} testfile'.format(self.testvm2.name)))
  617. # Close GUI error message
  618. self.enter_keys_in_window('Error', ['Return'])
  619. # the file shouldn't be removed in source vm
  620. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  621. 'test -f testfile'))
  622. def test_200_timezone(self):
  623. """Test whether timezone setting is properly propagated to the VM"""
  624. if "whonix" in self.template:
  625. self.skipTest("Timezone propagation disabled on Whonix templates")
  626. self.loop.run_until_complete(self.testvm1.start())
  627. vm_tz, _ = self.loop.run_until_complete(self.testvm1.run_for_stdio(
  628. 'date +%Z'))
  629. dom0_tz = subprocess.check_output(['date', '+%Z'])
  630. self.assertEqual(vm_tz.strip(), dom0_tz.strip())
  631. # Check if reverting back to UTC works
  632. vm_tz, _ = self.loop.run_until_complete(self.testvm1.run_for_stdio(
  633. 'TZ=UTC date +%Z'))
  634. self.assertEqual(vm_tz.strip(), b'UTC')
  635. def test_210_time_sync(self):
  636. """Test time synchronization mechanism"""
  637. if self.template.startswith('whonix-'):
  638. self.skipTest('qvm-sync-clock disabled for Whonix VMs')
  639. self.loop.run_until_complete(asyncio.wait([
  640. self.testvm1.start(),
  641. self.testvm2.start()]))
  642. start_time = subprocess.check_output(['date', '-u', '+%s'])
  643. try:
  644. self.app.clockvm = self.testvm1
  645. self.app.save()
  646. # break vm and dom0 time, to check if qvm-sync-clock would fix it
  647. subprocess.check_call(['sudo', 'date', '-s', '2001-01-01T12:34:56'],
  648. stdout=subprocess.DEVNULL)
  649. self.loop.run_until_complete(asyncio.wait([
  650. self.testvm1.run_for_stdio('date -s 2001-01-01T12:34:56',
  651. user='root'),
  652. self.testvm2.run_for_stdio('date -s 2001-01-01T12:34:56',
  653. user='root'),
  654. ]))
  655. subprocess.check_call(['qvm-sync-clock'], stdout=subprocess.DEVNULL)
  656. # qvm-sync-clock is asynchronous - it spawns qubes.SetDateTime
  657. # service, send it timestamp value and exists without waiting for
  658. # actual time set
  659. time.sleep(1)
  660. vm_time, _ = self.loop.run_until_complete(
  661. self.testvm1.run_for_stdio('date -u +%s'))
  662. self.assertAlmostEquals(int(vm_time), int(start_time), delta=30)
  663. vm_time, _ = self.loop.run_until_complete(
  664. self.testvm2.run_for_stdio('date -u +%s'))
  665. self.assertAlmostEquals(int(vm_time), int(start_time), delta=30)
  666. dom0_time, _ = subprocess.check_output(['date', '-u', '+%s'])
  667. self.assertAlmostEquals(int(dom0_time), int(start_time), delta=30)
  668. except:
  669. # reset time to some approximation of the real time
  670. subprocess.Popen(
  671. ["sudo", "date", "-u", "-s", "@" + start_time.decode()])
  672. raise
  673. @unittest.expectedFailure
  674. def test_250_resize_private_img(self):
  675. """
  676. Test private.img resize, both offline and online
  677. :return:
  678. """
  679. # First offline test
  680. self.testvm1.storage.resize('private', 4*1024**3)
  681. self.loop.run_until_complete(self.testvm1.start())
  682. df_cmd = '( df --output=size /rw || df /rw | awk \'{print $2}\' )|' \
  683. 'tail -n 1'
  684. # new_size in 1k-blocks
  685. new_size, _ = self.loop.run_until_complete(
  686. self.testvm1.run_for_stdio(df_cmd))
  687. # some safety margin for FS metadata
  688. self.assertGreater(int(new_size.strip()), 3.8*1024**2)
  689. # Then online test
  690. self.loop.run_until_complete(
  691. self.testvm1.storage.resize('private', 6*1024**3))
  692. # new_size in 1k-blocks
  693. new_size, _ = self.loop.run_until_complete(
  694. self.testvm1.run_for_stdio(df_cmd))
  695. # some safety margin for FS metadata
  696. self.assertGreater(int(new_size.strip()), 5.8*1024**2)
  697. @unittest.skipUnless(spawn.find_executable('xdotool'),
  698. "xdotool not installed")
  699. def test_300_bug_1028_gui_memory_pinning(self):
  700. """
  701. If VM window composition buffers are relocated in memory, GUI will
  702. still use old pointers and will display old pages
  703. :return:
  704. """
  705. # this test does too much asynchronous operations,
  706. # so let's rewrite it as a coroutine and call it as such
  707. return self.loop.run_until_complete(
  708. self._test_300_bug_1028_gui_memory_pinning())
  709. @asyncio.coroutine
  710. def _test_300_bug_1028_gui_memory_pinning(self):
  711. self.testvm1.memory = 800
  712. self.testvm1.maxmem = 800
  713. # exclude from memory balancing
  714. self.testvm1.features['services/meminfo-writer'] = False
  715. yield from self.testvm1.start()
  716. # and allow large map count
  717. yield from self.testvm1.run('echo 256000 > /proc/sys/vm/max_map_count',
  718. user="root")
  719. allocator_c = '''
  720. #include <sys/mman.h>
  721. #include <stdlib.h>
  722. #include <stdio.h>
  723. int main(int argc, char **argv) {
  724. int total_pages;
  725. char *addr, *iter;
  726. total_pages = atoi(argv[1]);
  727. addr = mmap(NULL, total_pages * 0x1000, PROT_READ | PROT_WRITE,
  728. MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, -1, 0);
  729. if (addr == MAP_FAILED) {
  730. perror("mmap");
  731. exit(1);
  732. }
  733. printf("Stage1\\n");
  734. fflush(stdout);
  735. getchar();
  736. for (iter = addr; iter < addr + total_pages*0x1000; iter += 0x2000) {
  737. if (mlock(iter, 0x1000) == -1) {
  738. perror("mlock");
  739. fprintf(stderr, "%d of %d\\n", (iter-addr)/0x1000, total_pages);
  740. exit(1);
  741. }
  742. }
  743. printf("Stage2\\n");
  744. fflush(stdout);
  745. for (iter = addr+0x1000; iter < addr + total_pages*0x1000; iter += 0x2000) {
  746. if (munmap(iter, 0x1000) == -1) {
  747. perror(\"munmap\");
  748. exit(1);
  749. }
  750. }
  751. printf("Stage3\\n");
  752. fflush(stdout);
  753. fclose(stdout);
  754. getchar();
  755. return 0;
  756. }
  757. '''
  758. yield from self.testvm1.run_for_stdio('cat > allocator.c',
  759. input=allocator_c.encode())
  760. try:
  761. stdout, stderr = yield from self.testvm1.run_for_stdio(
  762. 'gcc allocator.c -o allocator')
  763. except subprocess.CalledProcessError:
  764. self.skipTest('allocator compile failed: {}'.format(stderr))
  765. # drop caches to have even more memory pressure
  766. yield from self.testvm1.run_for_stdio(
  767. 'echo 3 > /proc/sys/vm/drop_caches', user='root')
  768. # now fragment all free memory
  769. stdout, _ = yield from self.testvm1.run_for_stdio(
  770. "grep ^MemFree: /proc/meminfo|awk '{print $2}'")
  771. memory_pages = int(stdout) // 4 # 4k pages
  772. alloc1 = yield from self.testvm1.run_for_stdio(
  773. 'ulimit -l unlimited; exec /home/user/allocator {}'.format(
  774. memory_pages),
  775. user="root")
  776. # wait for memory being allocated; can't use just .read(), because EOF
  777. # passing is unreliable while the process is still running
  778. yield from alloc1.stdin.write(b'\n')
  779. yield from alloc1.stdin.flush()
  780. alloc_out = yield from alloc1.stdout.read(
  781. len('Stage1\nStage2\nStage3\n'))
  782. if b'Stage3' not in alloc_out:
  783. # read stderr only in case of failed assert (), but still have nice
  784. # failure message (don't use self.fail() directly)
  785. #
  786. # stderr isn't always read, because on not-failed run, the process
  787. # is still running, so stderr.read() will wait (indefinitely).
  788. self.assertIn(b'Stage3', alloc_out,
  789. (yield from alloc1.stderr.read()))
  790. # now, launch some window - it should get fragmented composition buffer
  791. # it is important to have some changing content there, to generate
  792. # content update events (aka damage notify)
  793. proc = yield from self.testvm1.run(
  794. 'gnome-terminal --full-screen -e top')
  795. # help xdotool a little...
  796. yield from asyncio.sleep(2)
  797. # get window ID
  798. winid = yield from asyncio.get_event_loop().run_in_executor(
  799. subprocess.check_output,
  800. ['xdotool', 'search', '--sync', '--onlyvisible', '--class',
  801. self.testvm1.name + ':.*erminal']).decode()
  802. xprop = yield from asyncio.get_event_loop().run_in_executor(
  803. subprocess.check_output,
  804. ['xprop', '-notype', '-id', winid, '_QUBES_VMWINDOWID'])
  805. vm_winid = xprop.decode().strip().split(' ')[4]
  806. # now free the fragmented memory and trigger compaction
  807. yield from alloc1.stdin.write(b'\n')
  808. yield from alloc1.stdin.flush()
  809. yield from alloc1.wait()
  810. yield from self.testvm1.run_for_stdio(
  811. 'echo 1 > /proc/sys/vm/compact_memory', user='root')
  812. # now window may be already "broken"; to be sure, allocate (=zero)
  813. # some memory
  814. alloc2 = yield from self.testvm1.run(
  815. 'ulimit -l unlimited; /home/user/allocator {}'.format(memory_pages),
  816. user='root')
  817. yield from alloc2.stdout.read(len('Stage1\n'))
  818. # wait for damage notify - top updates every 3 sec by default
  819. yield from asyncio.sleep(6)
  820. # now take screenshot of the window, from dom0 and VM
  821. # choose pnm format, as it doesn't have any useless metadata - easy
  822. # to compare
  823. vm_image, _ = yield from self.testvm1.run_for_stdio(
  824. 'import -window {} pnm:-'.format(vm_winid))
  825. dom0_image = yield from asyncio.get_event_loop().run_in_executor(
  826. subprocess.check_output, ['import', '-window', winid, 'pnm:-'])
  827. if vm_image != dom0_image:
  828. self.fail("Dom0 window doesn't match VM window content")
  829. class TC_10_Generic(qubes.tests.SystemTestsMixin, qubes.tests.QubesTestCase):
  830. def setUp(self):
  831. super(TC_10_Generic, self).setUp()
  832. self.init_default_template()
  833. self.vm = self.app.add_new_vm(
  834. qubes.vm.appvm.AppVM,
  835. name=self.make_vm_name('vm'),
  836. label='red',
  837. template=self.app.default_template)
  838. self.loop.run_until_complete(self.vm.create_on_disk())
  839. self.save_and_reload_db()
  840. self.vm = self.app.domains[self.vm.qid]
  841. def test_000_anyvm_deny_dom0(self):
  842. '''$anyvm in policy should not match dom0'''
  843. policy = open("/etc/qubes-rpc/policy/test.AnyvmDeny", "w")
  844. policy.write("%s $anyvm allow" % (self.vm.name,))
  845. policy.close()
  846. self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.AnyvmDeny")
  847. flagfile = '/tmp/test-anyvmdeny-flag'
  848. if os.path.exists(flagfile):
  849. os.remove(flagfile)
  850. self.create_local_file('/etc/qubes-rpc/test.AnyvmDeny',
  851. 'touch {}\necho service output\n'.format(flagfile))
  852. self.loop.run_until_complete(self.vm.start())
  853. with self.qrexec_policy('test.AnyvmDeny', self.vm, '$anyvm'):
  854. with self.assertRaises(subprocess.CalledProcessError,
  855. msg='$anyvm matched dom0'):
  856. stdout, stderr = self.loop.run_until_complete(
  857. self.vm.run_for_stdio(
  858. '/usr/lib/qubes/qrexec-client-vm dom0 test.AnyvmDeny'))
  859. self.assertFalse(os.path.exists(flagfile),
  860. 'Flag file created (service was run) even though should be denied,'
  861. ' qrexec-client-vm output: {} {}'.format(stdout, stderr))
  862. def load_tests(loader, tests, pattern):
  863. try:
  864. app = qubes.Qubes()
  865. templates = [vm.name for vm in app.domains if
  866. isinstance(vm, qubes.vm.templatevm.TemplateVM)]
  867. except OSError:
  868. templates = []
  869. for template in templates:
  870. tests.addTests(loader.loadTestsFromTestCase(
  871. type(
  872. 'TC_00_AppVM_' + template,
  873. (TC_00_AppVMMixin, qubes.tests.QubesTestCase),
  874. {'template': template})))
  875. return tests