pci.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2016 Marek Marczykowski-Górecki
  5. # <marmarek@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. ''' Qubes PCI Extensions '''
  22. import functools
  23. import os
  24. import re
  25. import subprocess
  26. import libvirt
  27. import lxml
  28. import lxml.etree
  29. import qubes.devices
  30. import qubes.ext
  31. #: cache of PCI device classes
  32. pci_classes = None
  33. def load_pci_classes():
  34. ''' List of known device classes, subclasses and programming interfaces. '''
  35. # Syntax:
  36. # C class class_name
  37. # subclass subclass_name <-- single tab
  38. # prog-if prog-if_name <-- two tabs
  39. result = {}
  40. with open('/usr/share/hwdata/pci.ids') as pciids:
  41. class_id = None
  42. subclass_id = None
  43. for line in pciids.readlines():
  44. line = line.rstrip()
  45. if line.startswith('\t\t') and class_id and subclass_id:
  46. (progif_id, _, class_name) = line[2:].split(' ', 2)
  47. result[class_id + subclass_id + progif_id] = \
  48. class_name
  49. elif line.startswith('\t') and class_id:
  50. (subclass_id, _, class_name) = line[1:].split(' ', 2)
  51. # store both prog-if specific entry and generic one
  52. result[class_id + subclass_id + '00'] = \
  53. class_name
  54. result[class_id + subclass_id] = \
  55. class_name
  56. elif line.startswith('C '):
  57. (_, class_id, _, class_name) = line.split(' ', 3)
  58. result[class_id + '0000'] = class_name
  59. result[class_id + '00'] = class_name
  60. subclass_id = None
  61. return result
  62. def pcidev_class(dev_xmldesc):
  63. sysfs_path = dev_xmldesc.findtext('path')
  64. assert sysfs_path
  65. try:
  66. class_id = open(sysfs_path + '/class').read().strip()
  67. except OSError:
  68. return "Unknown"
  69. if not qubes.ext.pci.pci_classes:
  70. qubes.ext.pci.pci_classes = load_pci_classes()
  71. if class_id.startswith('0x'):
  72. class_id = class_id[2:]
  73. try:
  74. # ignore prog-if
  75. return qubes.ext.pci.pci_classes[class_id[0:4]]
  76. except KeyError:
  77. return "Unknown"
  78. def attached_devices(app):
  79. """Return map device->domain-name for all currently attached devices"""
  80. # Libvirt do not expose nice API to query where the device is
  81. # attached. The only way would be to query _all_ the domains (
  82. # each with separate libvirt call) and look if the device is
  83. # there. Horrible waste of resources.
  84. # Instead, do this on much lower level - xenstore info for
  85. # xen-pciback driver, where we get all the info at once
  86. xs = app.vmm.xs
  87. devices = {}
  88. for domid in xs.ls('', 'backend/pci') or []:
  89. for devid in xs.ls('', 'backend/pci/' + domid) or []:
  90. devpath = 'backend/pci/' + domid + '/' + devid
  91. domain_name = xs.read('', devpath + '/domain')
  92. try:
  93. domain = app.domains[domain_name]
  94. except KeyError:
  95. # unknown domain - maybe from another qubes.xml?
  96. continue
  97. devnum = xs.read('', devpath + '/num_devs')
  98. for dev in range(int(devnum)):
  99. dbdf = xs.read('', devpath + '/dev-' + str(dev))
  100. bdf = dbdf[len('0000:'):]
  101. devices[bdf.replace(':', '_')] = domain
  102. return devices
  103. def _device_desc(hostdev_xml):
  104. return '{devclass}: {vendor} {product}'.format(
  105. devclass=pcidev_class(hostdev_xml),
  106. vendor=hostdev_xml.findtext('capability/vendor'),
  107. product=hostdev_xml.findtext('capability/product'),
  108. )
  109. class PCIDevice(qubes.devices.DeviceInfo):
  110. # pylint: disable=too-few-public-methods
  111. regex = re.compile(
  112. r'^(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)\.(?P<function>[0-9a-f]+)$')
  113. libvirt_regex = re.compile(
  114. r'^pci_0000_(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)_'
  115. r'(?P<function>[0-9a-f]+)$')
  116. def __init__(self, backend_domain, ident, libvirt_name=None):
  117. if libvirt_name:
  118. dev_match = self.libvirt_regex.match(libvirt_name)
  119. assert dev_match
  120. ident = '{bus}_{device}.{function}'.format(**dev_match.groupdict())
  121. super(PCIDevice, self).__init__(backend_domain, ident, None)
  122. # lazy loading
  123. self._description = None
  124. @property
  125. def libvirt_name(self):
  126. # pylint: disable=no-member
  127. # noinspection PyUnresolvedReferences
  128. return 'pci_0000_{}_{}_{}'.format(self.bus, self.device, self.function)
  129. @property
  130. def description(self):
  131. if self._description is None:
  132. hostdev_details = \
  133. self.backend_domain.app.vmm.libvirt_conn.nodeDeviceLookupByName(
  134. self.libvirt_name
  135. )
  136. self._description = _device_desc(lxml.etree.fromstring(
  137. hostdev_details.XMLDesc()))
  138. return self._description
  139. @property
  140. def frontend_domain(self):
  141. # TODO: cache this
  142. all_attached = attached_devices(self.backend_domain.app)
  143. return all_attached.get(self.ident, None)
  144. class PCIDeviceExtension(qubes.ext.Extension):
  145. def __init__(self):
  146. super(PCIDeviceExtension, self).__init__()
  147. # lazy load this
  148. self.pci_classes = {}
  149. @qubes.ext.handler('device-list:pci')
  150. def on_device_list_pci(self, vm, event):
  151. # pylint: disable=unused-argument,no-self-use
  152. # only dom0 expose PCI devices
  153. if vm.qid != 0:
  154. return
  155. for dev in vm.app.vmm.libvirt_conn.listAllDevices():
  156. if 'pci' not in dev.listCaps():
  157. continue
  158. xml_desc = lxml.etree.fromstring(dev.XMLDesc())
  159. libvirt_name = xml_desc.findtext('name')
  160. yield PCIDevice(vm, None, libvirt_name=libvirt_name)
  161. @qubes.ext.handler('device-get:pci')
  162. def on_device_get_pci(self, vm, event, ident):
  163. # pylint: disable=unused-argument,no-self-use
  164. if not vm.app.vmm.offline_mode:
  165. yield _cache_get(vm, ident)
  166. @qubes.ext.handler('device-list-attached:pci')
  167. def on_device_list_attached(self, vm, event, **kwargs):
  168. # pylint: disable=unused-argument,no-self-use
  169. if not vm.is_running() or isinstance(vm, qubes.vm.adminvm.AdminVM):
  170. return
  171. xml_desc = lxml.etree.fromstring(vm.libvirt_domain.XMLDesc())
  172. for hostdev in xml_desc.findall('devices/hostdev'):
  173. if hostdev.get('type') != 'pci':
  174. continue
  175. address = hostdev.find('source/address')
  176. bus = address.get('bus')[2:]
  177. device = address.get('slot')[2:]
  178. function = address.get('function')[2:]
  179. ident = '{bus}_{device}.{function}'.format(
  180. bus=bus,
  181. device=device,
  182. function=function,
  183. )
  184. yield (PCIDevice(vm.app.domains[0], ident), {})
  185. @qubes.ext.handler('device-pre-attach:pci')
  186. def on_device_pre_attached_pci(self, vm, event, device, options):
  187. # pylint: disable=unused-argument
  188. if not os.path.exists('/sys/bus/pci/devices/0000:{}'.format(
  189. device.ident.replace('_', ':'))):
  190. raise qubes.exc.QubesException(
  191. 'Invalid PCI device: {}'.format(device.ident))
  192. if not vm.is_running():
  193. return
  194. try:
  195. device = _cache_get(vm, device.ident)
  196. self.bind_pci_to_pciback(vm.app, device)
  197. vm.libvirt_domain.attachDevice(
  198. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  199. device=device, vm=vm, options=options))
  200. except subprocess.CalledProcessError as e:
  201. vm.log.exception('Failed to attach PCI device {!r} on the fly,'
  202. ' changes will be seen after VM restart.'.format(
  203. device.ident), e)
  204. @qubes.ext.handler('device-pre-detach:pci')
  205. def on_device_pre_detached_pci(self, vm, event, device):
  206. # pylint: disable=unused-argument,no-self-use
  207. if not vm.is_running():
  208. return
  209. # this cannot be converted to general API, because there is no
  210. # provision in libvirt for extracting device-side BDF; we need it for
  211. # qubes.DetachPciDevice, which unbinds driver, not to oops the kernel
  212. device = _cache_get(vm, device.ident)
  213. p = subprocess.Popen(['xl', 'pci-list', str(vm.xid)],
  214. stdout=subprocess.PIPE)
  215. result = p.communicate()[0].decode()
  216. m = re.search(r'^(\d+.\d+)\s+0000:{}$'.format(device.ident.replace(
  217. '_', ':')),
  218. result,
  219. flags=re.MULTILINE)
  220. if not m:
  221. vm.log.error('Device %s already detached', device.ident)
  222. return
  223. vmdev = m.group(1)
  224. try:
  225. vm.run_service('qubes.DetachPciDevice',
  226. user='root', input='00:{}'.format(vmdev))
  227. vm.libvirt_domain.detachDevice(
  228. vm.app.env.get_template('libvirt/devices/pci.xml').render(
  229. device=device, vm=vm))
  230. except (subprocess.CalledProcessError, libvirt.libvirtError) as e:
  231. vm.log.exception('Failed to detach PCI device {!r} on the fly,'
  232. ' changes will be seen after VM restart.'.format(
  233. device.ident), e)
  234. raise
  235. @qubes.ext.handler('domain-pre-start')
  236. def on_domain_pre_start(self, vm, _event, **_kwargs):
  237. # Bind pci devices to pciback driver
  238. for assignment in vm.devices['pci'].persistent():
  239. device = _cache_get(vm, assignment.ident)
  240. self.bind_pci_to_pciback(vm.app, device)
  241. @staticmethod
  242. def bind_pci_to_pciback(app, device):
  243. '''Bind PCI device to pciback driver.
  244. :param qubes.devices.PCIDevice device: device to attach
  245. Devices should be unbound from their normal kernel drivers and bound to
  246. the dummy driver, which allows for attaching them to a domain.
  247. '''
  248. try:
  249. node = app.vmm.libvirt_conn.nodeDeviceLookupByName(
  250. device.libvirt_name)
  251. except libvirt.libvirtError as e:
  252. if e.get_error_code() == libvirt.VIR_ERR_NO_NODE_DEVICE:
  253. raise qubes.exc.QubesException(
  254. 'PCI device {!r} does not exist'.format(
  255. device))
  256. raise
  257. try:
  258. node.dettach()
  259. except libvirt.libvirtError as e:
  260. if e.get_error_code() == libvirt.VIR_ERR_INTERNAL_ERROR:
  261. # allreaddy dettached
  262. pass
  263. else:
  264. raise
  265. @functools.lru_cache(maxsize=None)
  266. def _cache_get(vm, ident):
  267. ''' Caching wrapper around `PCIDevice(vm, ident)`. '''
  268. return PCIDevice(vm, ident)