pci.py 12 KB

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