vm_qrexec_gui.py 44 KB

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