vm_qrexec_gui.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  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 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 os
  23. import subprocess
  24. import sys
  25. import tempfile
  26. import unittest
  27. from distutils import spawn
  28. import grp
  29. import qubes.config
  30. import qubes.devices
  31. import qubes.tests
  32. import qubes.vm.appvm
  33. import qubes.vm.templatevm
  34. class TC_00_AppVMMixin(object):
  35. def setUp(self):
  36. super(TC_00_AppVMMixin, self).setUp()
  37. self.init_default_template(self.template)
  38. if self._testMethodName == 'test_210_time_sync':
  39. self.init_networking()
  40. self.testvm1 = self.app.add_new_vm(
  41. qubes.vm.appvm.AppVM,
  42. label='red',
  43. name=self.make_vm_name('vm1'),
  44. template=self.app.domains[self.template])
  45. self.loop.run_until_complete(self.testvm1.create_on_disk())
  46. self.testvm2 = self.app.add_new_vm(
  47. qubes.vm.appvm.AppVM,
  48. label='red',
  49. name=self.make_vm_name('vm2'),
  50. template=self.app.domains[self.template])
  51. self.loop.run_until_complete(self.testvm2.create_on_disk())
  52. self.app.save()
  53. def test_000_start_shutdown(self):
  54. # TODO: wait_for, timeout
  55. self.loop.run_until_complete(self.testvm1.start())
  56. self.assertEqual(self.testvm1.get_power_state(), "Running")
  57. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  58. self.loop.run_until_complete(self.testvm1.shutdown(wait=True))
  59. self.assertEqual(self.testvm1.get_power_state(), "Halted")
  60. @unittest.skipUnless(spawn.find_executable('xdotool'),
  61. "xdotool not installed")
  62. def test_010_run_xterm(self):
  63. self.loop.run_until_complete(self.testvm1.start())
  64. self.assertEqual(self.testvm1.get_power_state(), "Running")
  65. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  66. p = self.loop.run_until_complete(self.testvm1.run('xterm'))
  67. try:
  68. title = 'user@{}'.format(self.testvm1.name)
  69. if self.template.count("whonix"):
  70. title = 'user@host'
  71. self.wait_for_window(title)
  72. self.loop.run_until_complete(asyncio.sleep(0.5))
  73. subprocess.check_call(
  74. ['xdotool', 'search', '--name', title,
  75. 'windowactivate', 'type', 'exit\n'])
  76. self.wait_for_window(title, show=False)
  77. finally:
  78. try:
  79. p.terminate()
  80. self.loop.run_until_complete(p.wait())
  81. except ProcessLookupError: # already dead
  82. pass
  83. @unittest.skipUnless(spawn.find_executable('xdotool'),
  84. "xdotool not installed")
  85. def test_011_run_gnome_terminal(self):
  86. if "minimal" in self.template:
  87. self.skipTest("Minimal template doesn't have 'gnome-terminal'")
  88. if 'whonix' in self.template:
  89. self.skipTest("Whonix template doesn't have 'gnome-terminal'")
  90. self.loop.run_until_complete(self.testvm1.start())
  91. self.assertEqual(self.testvm1.get_power_state(), "Running")
  92. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  93. p = self.loop.run_until_complete(self.testvm1.run('gnome-terminal'))
  94. try:
  95. title = 'user@{}'.format(self.testvm1.name)
  96. if self.template.count("whonix"):
  97. title = 'user@host'
  98. self.wait_for_window(title)
  99. self.loop.run_until_complete(asyncio.sleep(0.5))
  100. subprocess.check_call(
  101. ['xdotool', 'search', '--name', title,
  102. 'windowactivate', '--sync', 'type', 'exit\n'])
  103. wait_count = 0
  104. while subprocess.call(['xdotool', 'search', '--name', title],
  105. stdout=open(os.path.devnull, 'w'),
  106. stderr=subprocess.STDOUT) == 0:
  107. wait_count += 1
  108. if wait_count > 100:
  109. self.fail("Timeout while waiting for gnome-terminal "
  110. "termination")
  111. self.loop.run_until_complete(asyncio.sleep(0.1))
  112. finally:
  113. try:
  114. p.terminate()
  115. self.loop.run_until_complete(p.wait())
  116. except ProcessLookupError: # already dead
  117. pass
  118. @unittest.skipUnless(spawn.find_executable('xdotool'),
  119. "xdotool not installed")
  120. def test_012_qubes_desktop_run(self):
  121. self.loop.run_until_complete(self.testvm1.start())
  122. self.assertEqual(self.testvm1.get_power_state(), "Running")
  123. xterm_desktop_path = "/usr/share/applications/xterm.desktop"
  124. # Debian has it different...
  125. xterm_desktop_path_debian = \
  126. "/usr/share/applications/debian-xterm.desktop"
  127. try:
  128. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  129. 'test -r {}'.format(xterm_desktop_path_debian)))
  130. except subprocess.CalledProcessError:
  131. pass
  132. else:
  133. xterm_desktop_path = xterm_desktop_path_debian
  134. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  135. self.loop.run_until_complete(
  136. self.testvm1.run('qubes-desktop-run {}'.format(xterm_desktop_path)))
  137. title = 'user@{}'.format(self.testvm1.name)
  138. if self.template.count("whonix"):
  139. title = 'user@host'
  140. self.wait_for_window(title)
  141. self.loop.run_until_complete(asyncio.sleep(0.5))
  142. subprocess.check_call(
  143. ['xdotool', 'search', '--name', title,
  144. 'windowactivate', '--sync', 'type', 'exit\n'])
  145. self.wait_for_window(title, show=False)
  146. def test_100_qrexec_filecopy(self):
  147. self.loop.run_until_complete(asyncio.wait([
  148. self.testvm1.start(),
  149. self.testvm2.start()]))
  150. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  151. 'cp /etc/passwd /tmp/passwd'))
  152. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  153. try:
  154. self.loop.run_until_complete(
  155. self.testvm1.run_for_stdio(
  156. 'qvm-copy-to-vm {} /tmp/passwd'.format(
  157. self.testvm2.name)))
  158. except subprocess.CalledProcessError as e:
  159. self.fail('qvm-copy-to-vm failed: {}'.format(e.stderr))
  160. try:
  161. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  162. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  163. self.testvm1.name)))
  164. except subprocess.CalledProcessError:
  165. self.fail('file differs')
  166. try:
  167. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  168. 'test -f /tmp/passwd'))
  169. except subprocess.CalledProcessError:
  170. self.fail('source file got removed')
  171. def test_105_qrexec_filemove(self):
  172. self.loop.run_until_complete(asyncio.wait([
  173. self.testvm1.start(),
  174. self.testvm2.start()]))
  175. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  176. 'cp /etc/passwd /tmp/passwd'))
  177. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  178. try:
  179. self.loop.run_until_complete(
  180. self.testvm1.run_for_stdio(
  181. 'qvm-move-to-vm {} /tmp/passwd'.format(
  182. self.testvm2.name)))
  183. except subprocess.CalledProcessError as e:
  184. self.fail('qvm-move-to-vm failed: {}'.format(e.stderr))
  185. try:
  186. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  187. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  188. self.testvm1.name)))
  189. except subprocess.CalledProcessError:
  190. self.fail('file differs')
  191. with self.assertRaises(subprocess.CalledProcessError):
  192. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  193. 'test -f /tmp/passwd'))
  194. def test_101_qrexec_filecopy_with_autostart(self):
  195. self.loop.run_until_complete(self.testvm1.start())
  196. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  197. try:
  198. self.loop.run_until_complete(
  199. self.testvm1.run_for_stdio(
  200. 'qvm-copy-to-vm {} /etc/passwd'.format(
  201. self.testvm2.name)))
  202. except subprocess.CalledProcessError as e:
  203. self.fail('qvm-copy-to-vm failed: {}'.format(e.stderr))
  204. # workaround for libvirt bug (domain ID isn't updated when is started
  205. # from other application) - details in
  206. # QubesOS/qubes-core-libvirt@63ede4dfb4485c4161dd6a2cc809e8fb45ca664f
  207. # XXX is it still true with qubesd? --woju 20170523
  208. self.testvm2._libvirt_domain = None
  209. self.assertTrue(self.testvm2.is_running())
  210. try:
  211. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  212. 'diff /etc/passwd /home/user/QubesIncoming/{}/passwd'.format(
  213. self.testvm1.name)))
  214. except subprocess.CalledProcessError:
  215. self.fail('file differs')
  216. try:
  217. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  218. 'test -f /etc/passwd'))
  219. except subprocess.CalledProcessError:
  220. self.fail('source file got removed')
  221. def test_110_qrexec_filecopy_deny(self):
  222. self.loop.run_until_complete(asyncio.wait([
  223. self.testvm1.start(),
  224. self.testvm2.start()]))
  225. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2,
  226. allow=False):
  227. with self.assertRaises(subprocess.CalledProcessError):
  228. self.loop.run_until_complete(
  229. self.testvm1.run_for_stdio(
  230. 'qvm-copy-to-vm {} /etc/passwd'.format(
  231. self.testvm2.name)))
  232. with self.assertRaises(subprocess.CalledProcessError):
  233. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  234. 'test -d /home/user/QubesIncoming/{}'.format(
  235. self.testvm1.name)))
  236. def test_115_qrexec_filecopy_no_agent(self):
  237. # The operation should not hang when qrexec-agent is down on target
  238. # machine, see QubesOS/qubes-issues#5347.
  239. self.loop.run_until_complete(asyncio.wait([
  240. self.testvm1.start(),
  241. self.testvm2.start()]))
  242. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  243. try:
  244. self.loop.run_until_complete(
  245. self.testvm2.run_for_stdio(
  246. 'systemctl stop qubes-qrexec-agent.service', user='root'))
  247. except subprocess.CalledProcessError:
  248. # A failure is normal here, because we're killing the qrexec
  249. # process that is handling the command.
  250. pass
  251. with self.assertRaises(subprocess.CalledProcessError):
  252. self.loop.run_until_complete(
  253. asyncio.wait_for(
  254. self.testvm1.run_for_stdio(
  255. 'qvm-copy-to-vm {} /etc/passwd'.format(
  256. self.testvm2.name)),
  257. timeout=30))
  258. @unittest.skip("Xen gntalloc driver crashes when page is mapped in the "
  259. "same domain")
  260. def test_120_qrexec_filecopy_self(self):
  261. self.testvm1.start()
  262. self.qrexec_policy('qubes.Filecopy', self.testvm1.name,
  263. self.testvm1.name)
  264. p = self.testvm1.run("qvm-copy-to-vm %s /etc/passwd" %
  265. self.testvm1.name, passio_popen=True,
  266. passio_stderr=True)
  267. p.wait()
  268. self.assertEqual(p.returncode, 0, "qvm-copy-to-vm failed: %s" %
  269. p.stderr.read())
  270. retcode = self.testvm1.run(
  271. "diff /etc/passwd /home/user/QubesIncoming/{}/passwd".format(
  272. self.testvm1.name),
  273. wait=True)
  274. self.assertEqual(retcode, 0, "file differs")
  275. @unittest.skipUnless(spawn.find_executable('xdotool'),
  276. "xdotool not installed")
  277. def test_130_qrexec_filemove_disk_full(self):
  278. self.loop.run_until_complete(asyncio.wait([
  279. self.testvm1.start(),
  280. self.testvm2.start()]))
  281. self.loop.run_until_complete(self.wait_for_session(self.testvm1))
  282. # Prepare test file
  283. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  284. 'yes teststring | dd of=/tmp/testfile bs=1M count=50 '
  285. 'iflag=fullblock'))
  286. # Prepare target directory with limited size
  287. self.loop.run_until_complete(self.testvm2.run_for_stdio(
  288. 'mkdir -p /home/user/QubesIncoming && '
  289. 'chown user /home/user/QubesIncoming && '
  290. 'mount -t tmpfs none /home/user/QubesIncoming -o size=48M',
  291. user='root'))
  292. with self.qrexec_policy('qubes.Filecopy', self.testvm1, self.testvm2):
  293. p = self.loop.run_until_complete(self.testvm1.run(
  294. 'qvm-move-to-vm {} /tmp/testfile'.format(
  295. self.testvm2.name)))
  296. # Close GUI error message
  297. try:
  298. self.enter_keys_in_window('Error', ['Return'])
  299. except subprocess.CalledProcessError:
  300. pass
  301. self.loop.run_until_complete(p.wait())
  302. self.assertNotEqual(p.returncode, 0)
  303. # the file shouldn't be removed in source vm
  304. self.loop.run_until_complete(self.testvm1.run_for_stdio(
  305. 'test -f /tmp/testfile'))
  306. def test_200_timezone(self):
  307. """Test whether timezone setting is properly propagated to the VM"""
  308. if "whonix" in self.template:
  309. self.skipTest("Timezone propagation disabled on Whonix templates")
  310. self.loop.run_until_complete(self.testvm1.start())
  311. vm_tz, _ = self.loop.run_until_complete(self.testvm1.run_for_stdio(
  312. 'date +%Z'))
  313. dom0_tz = subprocess.check_output(['date', '+%Z'])
  314. self.assertEqual(vm_tz.strip(), dom0_tz.strip())
  315. # Check if reverting back to UTC works
  316. vm_tz, _ = self.loop.run_until_complete(self.testvm1.run_for_stdio(
  317. 'TZ=UTC date +%Z'))
  318. self.assertEqual(vm_tz.strip(), b'UTC')
  319. def test_210_time_sync(self):
  320. """Test time synchronization mechanism"""
  321. if self.template.startswith('whonix-'):
  322. self.skipTest('qvm-sync-clock disabled for Whonix VMs')
  323. self.loop.run_until_complete(asyncio.wait([
  324. self.testvm1.start(),
  325. self.testvm2.start(),]))
  326. start_time = subprocess.check_output(['date', '-u', '+%s'])
  327. try:
  328. self.app.clockvm = self.testvm1
  329. self.app.save()
  330. # break vm and dom0 time, to check if qvm-sync-clock would fix it
  331. subprocess.check_call(['sudo', 'date', '-s', '2001-01-01T12:34:56'],
  332. stdout=subprocess.DEVNULL)
  333. self.loop.run_until_complete(
  334. self.testvm2.run_for_stdio('date -s 2001-01-01T12:34:56',
  335. user='root'))
  336. self.loop.run_until_complete(
  337. self.testvm2.run_for_stdio('qvm-sync-clock',
  338. user='root'))
  339. p = self.loop.run_until_complete(
  340. asyncio.create_subprocess_exec('sudo', 'qvm-sync-clock',
  341. stdout=asyncio.subprocess.DEVNULL))
  342. self.loop.run_until_complete(p.wait())
  343. self.assertEqual(p.returncode, 0)
  344. vm_time, _ = self.loop.run_until_complete(
  345. self.testvm2.run_for_stdio('date -u +%s'))
  346. self.assertAlmostEquals(int(vm_time), int(start_time), delta=30)
  347. dom0_time = subprocess.check_output(['date', '-u', '+%s'])
  348. self.assertAlmostEquals(int(dom0_time), int(start_time), delta=30)
  349. except:
  350. # reset time to some approximation of the real time
  351. subprocess.Popen(
  352. ["sudo", "date", "-u", "-s", "@" + start_time.decode()])
  353. raise
  354. finally:
  355. self.app.clockvm = None
  356. def wait_for_pulseaudio_startup(self, vm):
  357. self.loop.run_until_complete(
  358. self.wait_for_session(self.testvm1))
  359. try:
  360. self.loop.run_until_complete(vm.run_for_stdio(
  361. "timeout 30s sh -c 'while ! pactl info; do sleep 1; done'"
  362. ))
  363. except subprocess.CalledProcessError as e:
  364. self.fail('Timeout waiting for pulseaudio start in {}: {}{}'.format(
  365. vm.name, e.stdout, e.stderr))
  366. # then wait for the stream to appear in dom0
  367. local_user = grp.getgrnam('qubes').gr_mem[0]
  368. p = self.loop.run_until_complete(asyncio.create_subprocess_shell(
  369. "sudo -E -u {} timeout 30s sh -c '"
  370. "while ! pactl list sink-inputs | grep -q :{}; do sleep 1; done'".format(
  371. local_user, vm.name)))
  372. self.loop.run_until_complete(p.wait())
  373. # and some more...
  374. self.loop.run_until_complete(asyncio.sleep(1))
  375. @unittest.skipUnless(spawn.find_executable('parecord'),
  376. "pulseaudio-utils not installed in dom0")
  377. def test_220_audio_playback(self):
  378. if 'whonix-gw' in self.template:
  379. self.skipTest('whonix-gw have no audio')
  380. self.loop.run_until_complete(self.testvm1.start())
  381. try:
  382. self.loop.run_until_complete(
  383. self.testvm1.run_for_stdio('which parecord'))
  384. except subprocess.CalledProcessError:
  385. self.skipTest('pulseaudio-utils not installed in VM')
  386. self.wait_for_pulseaudio_startup(self.testvm1)
  387. # generate some "audio" data
  388. audio_in = b'\x20' * 44100
  389. self.loop.run_until_complete(
  390. self.testvm1.run_for_stdio('cat > audio_in.raw', input=audio_in))
  391. local_user = grp.getgrnam('qubes').gr_mem[0]
  392. with tempfile.NamedTemporaryFile() as recorded_audio:
  393. os.chmod(recorded_audio.name, 0o666)
  394. # FIXME: -d 0 assumes only one audio device
  395. p = subprocess.Popen(['sudo', '-E', '-u', local_user,
  396. 'parecord', '-d', '0', '--raw', recorded_audio.name],
  397. stdout=subprocess.PIPE)
  398. try:
  399. self.loop.run_until_complete(
  400. self.testvm1.run_for_stdio('paplay --raw audio_in.raw'))
  401. except subprocess.CalledProcessError as err:
  402. self.fail('{} stderr: {}'.format(str(err), err.stderr))
  403. # wait for possible parecord buffering
  404. self.loop.run_until_complete(asyncio.sleep(1))
  405. p.terminate()
  406. # for some reason sudo do not relay SIGTERM sent above
  407. subprocess.check_call(['pkill', 'parecord'])
  408. p.wait()
  409. # allow up to 20ms missing, don't use assertIn, to avoid printing
  410. # the whole data in error message
  411. recorded_audio = recorded_audio.file.read()
  412. if audio_in[:-3528] not in recorded_audio:
  413. found_bytes = recorded_audio.count(audio_in[0])
  414. all_bytes = len(audio_in)
  415. self.fail('played sound not found in dom0, '
  416. 'missing {} bytes out of {}'.format(
  417. all_bytes-found_bytes, all_bytes))
  418. def _configure_audio_recording(self, vm):
  419. '''Connect VM's output-source to sink monitor instead of mic'''
  420. local_user = grp.getgrnam('qubes').gr_mem[0]
  421. sudo = ['sudo', '-E', '-u', local_user]
  422. source_outputs = subprocess.check_output(
  423. sudo + ['pacmd', 'list-source-outputs']).decode()
  424. last_index = None
  425. found = False
  426. for line in source_outputs.splitlines():
  427. if line.startswith(' index: '):
  428. last_index = line.split(':')[1].strip()
  429. elif line.startswith('\t\tapplication.name = '):
  430. app_name = line.split('=')[1].strip('" ')
  431. if vm.name == app_name:
  432. found = True
  433. break
  434. if not found:
  435. self.fail('source-output for VM {} not found'.format(vm.name))
  436. subprocess.check_call(sudo +
  437. ['pacmd', 'move-source-output', last_index, '0'])
  438. @unittest.skipUnless(spawn.find_executable('parecord'),
  439. "pulseaudio-utils not installed in dom0")
  440. def test_221_audio_record_muted(self):
  441. if 'whonix-gw' in self.template:
  442. self.skipTest('whonix-gw have no audio')
  443. self.loop.run_until_complete(self.testvm1.start())
  444. try:
  445. self.loop.run_until_complete(
  446. self.testvm1.run_for_stdio('which parecord'))
  447. except subprocess.CalledProcessError:
  448. self.skipTest('pulseaudio-utils not installed in VM')
  449. self.wait_for_pulseaudio_startup(self.testvm1)
  450. # connect VM's recording source output monitor (instead of mic)
  451. self._configure_audio_recording(self.testvm1)
  452. # generate some "audio" data
  453. audio_in = b'\x20' * 44100
  454. local_user = grp.getgrnam('qubes').gr_mem[0]
  455. record = self.loop.run_until_complete(
  456. self.testvm1.run('parecord --raw audio_rec.raw'))
  457. # give it time to start recording
  458. self.loop.run_until_complete(asyncio.sleep(0.5))
  459. p = subprocess.Popen(['sudo', '-E', '-u', local_user,
  460. 'paplay', '--raw'],
  461. stdin=subprocess.PIPE)
  462. p.communicate(audio_in)
  463. # wait for possible parecord buffering
  464. self.loop.run_until_complete(asyncio.sleep(1))
  465. self.loop.run_until_complete(
  466. self.testvm1.run_for_stdio('pkill parecord'))
  467. self.loop.run_until_complete(record.wait())
  468. recorded_audio, _ = self.loop.run_until_complete(
  469. self.testvm1.run_for_stdio('cat audio_rec.raw'))
  470. # should be empty or silence, so check just a little fragment
  471. if audio_in[:32] in recorded_audio:
  472. self.fail('VM recorded something, even though mic disabled')
  473. @unittest.skipUnless(spawn.find_executable('parecord'),
  474. "pulseaudio-utils not installed in dom0")
  475. def test_222_audio_record_unmuted(self):
  476. if 'whonix-gw' in self.template:
  477. self.skipTest('whonix-gw have no audio')
  478. self.loop.run_until_complete(self.testvm1.start())
  479. try:
  480. self.loop.run_until_complete(
  481. self.testvm1.run_for_stdio('which parecord'))
  482. except subprocess.CalledProcessError:
  483. self.skipTest('pulseaudio-utils not installed in VM')
  484. self.wait_for_pulseaudio_startup(self.testvm1)
  485. da = qubes.devices.DeviceAssignment(self.app.domains[0], 'mic')
  486. self.loop.run_until_complete(
  487. self.testvm1.devices['mic'].attach(da))
  488. # connect VM's recording source output monitor (instead of mic)
  489. self._configure_audio_recording(self.testvm1)
  490. # generate some "audio" data
  491. audio_in = b'\x20' * 44100
  492. local_user = grp.getgrnam('qubes').gr_mem[0]
  493. record = self.loop.run_until_complete(
  494. self.testvm1.run('parecord --raw audio_rec.raw'))
  495. # give it time to start recording
  496. self.loop.run_until_complete(asyncio.sleep(0.5))
  497. p = subprocess.Popen(['sudo', '-E', '-u', local_user,
  498. 'paplay', '--raw'],
  499. stdin=subprocess.PIPE)
  500. p.communicate(audio_in)
  501. # wait for possible parecord buffering
  502. self.loop.run_until_complete(asyncio.sleep(1))
  503. self.loop.run_until_complete(
  504. self.testvm1.run_for_stdio('pkill parecord || :'))
  505. _, record_stderr = self.loop.run_until_complete(record.communicate())
  506. if record_stderr:
  507. self.fail('parecord printed something on stderr: {}'.format(
  508. record_stderr))
  509. recorded_audio, _ = self.loop.run_until_complete(
  510. self.testvm1.run_for_stdio('cat audio_rec.raw'))
  511. # allow up to 20ms to be missing
  512. if audio_in[:-3528] not in recorded_audio:
  513. found_bytes = recorded_audio.count(audio_in[0])
  514. all_bytes = len(audio_in)
  515. self.fail('VM not recorded expected data, '
  516. 'missing {} bytes out of {}'.format(
  517. all_bytes-found_bytes, all_bytes))
  518. def test_250_resize_private_img(self):
  519. """
  520. Test private.img resize, both offline and online
  521. :return:
  522. """
  523. # First offline test
  524. self.loop.run_until_complete(
  525. self.testvm1.storage.resize('private', 4*1024**3))
  526. self.loop.run_until_complete(self.testvm1.start())
  527. df_cmd = '( df --output=size /rw || df /rw | awk \'{print $2}\' )|' \
  528. 'tail -n 1'
  529. # new_size in 1k-blocks
  530. new_size, _ = self.loop.run_until_complete(
  531. self.testvm1.run_for_stdio(df_cmd))
  532. # some safety margin for FS metadata
  533. self.assertGreater(int(new_size.strip()), 3.8*1024**2)
  534. # Then online test
  535. self.loop.run_until_complete(
  536. self.testvm1.storage.resize('private', 6*1024**3))
  537. # new_size in 1k-blocks
  538. new_size, _ = self.loop.run_until_complete(
  539. self.testvm1.run_for_stdio(df_cmd))
  540. # some safety margin for FS metadata
  541. self.assertGreater(int(new_size.strip()), 5.7*1024**2)
  542. @unittest.skipUnless(spawn.find_executable('xdotool'),
  543. "xdotool not installed")
  544. def test_300_bug_1028_gui_memory_pinning(self):
  545. """
  546. If VM window composition buffers are relocated in memory, GUI will
  547. still use old pointers and will display old pages
  548. :return:
  549. """
  550. # this test does too much asynchronous operations,
  551. # so let's rewrite it as a coroutine and call it as such
  552. return self.loop.run_until_complete(
  553. self._test_300_bug_1028_gui_memory_pinning())
  554. @asyncio.coroutine
  555. def _test_300_bug_1028_gui_memory_pinning(self):
  556. self.testvm1.memory = 800
  557. self.testvm1.maxmem = 800
  558. # exclude from memory balancing
  559. self.testvm1.features['service.meminfo-writer'] = False
  560. yield from self.testvm1.start()
  561. yield from self.wait_for_session(self.testvm1)
  562. # and allow large map count
  563. yield from self.testvm1.run('echo 256000 > /proc/sys/vm/max_map_count',
  564. user="root")
  565. allocator_c = '''
  566. #include <sys/mman.h>
  567. #include <stdlib.h>
  568. #include <stdio.h>
  569. int main(int argc, char **argv) {
  570. int total_pages;
  571. char *addr, *iter;
  572. total_pages = atoi(argv[1]);
  573. addr = mmap(NULL, total_pages * 0x1000, PROT_READ | PROT_WRITE,
  574. MAP_ANONYMOUS | MAP_PRIVATE | MAP_POPULATE, -1, 0);
  575. if (addr == MAP_FAILED) {
  576. perror("mmap");
  577. exit(1);
  578. }
  579. printf("Stage1\\n");
  580. fflush(stdout);
  581. getchar();
  582. for (iter = addr; iter < addr + total_pages*0x1000; iter += 0x2000) {
  583. if (mlock(iter, 0x1000) == -1) {
  584. perror("mlock");
  585. fprintf(stderr, "%d of %d\\n", (iter-addr)/0x1000, total_pages);
  586. exit(1);
  587. }
  588. }
  589. printf("Stage2\\n");
  590. fflush(stdout);
  591. for (iter = addr+0x1000; iter < addr + total_pages*0x1000; iter += 0x2000) {
  592. if (munmap(iter, 0x1000) == -1) {
  593. perror(\"munmap\");
  594. exit(1);
  595. }
  596. }
  597. printf("Stage3\\n");
  598. fflush(stdout);
  599. fclose(stdout);
  600. getchar();
  601. return 0;
  602. }
  603. '''
  604. yield from self.testvm1.run_for_stdio('cat > allocator.c',
  605. input=allocator_c.encode())
  606. try:
  607. yield from self.testvm1.run_for_stdio(
  608. 'gcc allocator.c -o allocator')
  609. except subprocess.CalledProcessError as e:
  610. self.skipTest('allocator compile failed: {}'.format(e.stderr))
  611. # drop caches to have even more memory pressure
  612. yield from self.testvm1.run_for_stdio(
  613. 'echo 3 > /proc/sys/vm/drop_caches', user='root')
  614. # now fragment all free memory
  615. stdout, _ = yield from self.testvm1.run_for_stdio(
  616. "grep ^MemFree: /proc/meminfo|awk '{print $2}'")
  617. memory_pages = int(stdout) // 4 # 4k pages
  618. alloc1 = yield from self.testvm1.run(
  619. 'ulimit -l unlimited; exec /home/user/allocator {}'.format(
  620. memory_pages),
  621. user="root",
  622. stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  623. stderr=subprocess.PIPE)
  624. # wait for memory being allocated; can't use just .read(), because EOF
  625. # passing is unreliable while the process is still running
  626. alloc1.stdin.write(b'\n')
  627. yield from alloc1.stdin.drain()
  628. try:
  629. alloc_out = yield from alloc1.stdout.readexactly(
  630. len('Stage1\nStage2\nStage3\n'))
  631. except asyncio.IncompleteReadError as e:
  632. alloc_out = e.partial
  633. if b'Stage3' not in alloc_out:
  634. # read stderr only in case of failed assert (), but still have nice
  635. # failure message (don't use self.fail() directly)
  636. #
  637. # stderr isn't always read, because on not-failed run, the process
  638. # is still running, so stderr.read() will wait (indefinitely).
  639. self.assertIn(b'Stage3', alloc_out,
  640. (yield from alloc1.stderr.read()))
  641. # now, launch some window - it should get fragmented composition buffer
  642. # it is important to have some changing content there, to generate
  643. # content update events (aka damage notify)
  644. proc = yield from self.testvm1.run(
  645. 'xterm -maximized -e top')
  646. if proc.returncode is not None:
  647. self.fail('xterm failed to start')
  648. # get window ID
  649. winid = yield from self.wait_for_window_coro(
  650. self.testvm1.name + ':xterm',
  651. search_class=True)
  652. xprop = yield from asyncio.get_event_loop().run_in_executor(None,
  653. subprocess.check_output,
  654. ['xprop', '-notype', '-id', winid, '_QUBES_VMWINDOWID'])
  655. vm_winid = xprop.decode().strip().split(' ')[4]
  656. # now free the fragmented memory and trigger compaction
  657. alloc1.stdin.write(b'\n')
  658. yield from alloc1.stdin.drain()
  659. yield from alloc1.wait()
  660. yield from self.testvm1.run_for_stdio(
  661. 'echo 1 > /proc/sys/vm/compact_memory', user='root')
  662. # now window may be already "broken"; to be sure, allocate (=zero)
  663. # some memory
  664. alloc2 = yield from self.testvm1.run(
  665. 'ulimit -l unlimited; /home/user/allocator {}'.format(memory_pages),
  666. user='root', stdout=subprocess.PIPE)
  667. yield from alloc2.stdout.read(len('Stage1\n'))
  668. # wait for damage notify - top updates every 3 sec by default
  669. yield from asyncio.sleep(6)
  670. # stop changing the window content
  671. subprocess.check_call(['xdotool', 'key', '--window', winid, 'd'])
  672. # now take screenshot of the window, from dom0 and VM
  673. # choose pnm format, as it doesn't have any useless metadata - easy
  674. # to compare
  675. vm_image, _ = yield from self.testvm1.run_for_stdio(
  676. 'import -window {} pnm:-'.format(vm_winid))
  677. dom0_image = yield from asyncio.get_event_loop().run_in_executor(None,
  678. subprocess.check_output, ['import', '-window', winid, 'pnm:-'])
  679. if vm_image != dom0_image:
  680. self.fail("Dom0 window doesn't match VM window content")
  681. class TC_10_Generic(qubes.tests.SystemTestCase):
  682. def setUp(self):
  683. super(TC_10_Generic, self).setUp()
  684. self.init_default_template()
  685. self.vm = self.app.add_new_vm(
  686. qubes.vm.appvm.AppVM,
  687. name=self.make_vm_name('vm'),
  688. label='red',
  689. template=self.app.default_template)
  690. self.loop.run_until_complete(self.vm.create_on_disk())
  691. self.app.save()
  692. self.vm = self.app.domains[self.vm.qid]
  693. def test_000_anyvm_deny_dom0(self):
  694. '''$anyvm in policy should not match dom0'''
  695. policy = open("/etc/qubes-rpc/policy/test.AnyvmDeny", "w")
  696. policy.write("%s $anyvm allow" % (self.vm.name,))
  697. policy.close()
  698. self.addCleanup(os.unlink, "/etc/qubes-rpc/policy/test.AnyvmDeny")
  699. flagfile = '/tmp/test-anyvmdeny-flag'
  700. if os.path.exists(flagfile):
  701. os.remove(flagfile)
  702. self.create_local_file('/etc/qubes-rpc/test.AnyvmDeny',
  703. 'touch {}\necho service output\n'.format(flagfile))
  704. self.loop.run_until_complete(self.vm.start())
  705. with self.qrexec_policy('test.AnyvmDeny', self.vm, '$anyvm'):
  706. with self.assertRaises(subprocess.CalledProcessError,
  707. msg='$anyvm matched dom0') as e:
  708. self.loop.run_until_complete(
  709. self.vm.run_for_stdio(
  710. '/usr/lib/qubes/qrexec-client-vm dom0 test.AnyvmDeny'))
  711. stdout = e.exception.output
  712. stderr = e.exception.stderr
  713. self.assertFalse(os.path.exists(flagfile),
  714. 'Flag file created (service was run) even though should be denied,'
  715. ' qrexec-client-vm output: {} {}'.format(stdout, stderr))
  716. def create_testcases_for_templates():
  717. return qubes.tests.create_testcases_for_templates('TC_00_AppVM',
  718. TC_00_AppVMMixin, qubes.tests.SystemTestCase,
  719. module=sys.modules[__name__])
  720. def load_tests(loader, tests, pattern):
  721. tests.addTests(loader.loadTestsFromNames(
  722. create_testcases_for_templates()))
  723. return tests
  724. qubes.tests.maybe_create_testcases_on_import(create_testcases_for_templates)