Skip to content

Commit 6ac4aa7

Browse files
committed
ext/pci: implement PCI path for device identification
PCI SBDF may change if _another_ device is added/removed from the system. Use PCI device path to avoid this issue. The PCI device path is more or less sysfs path, but with bus number behind bridges replaced with relative bus number to bridge's secondary bus number. Each component of the path is separated with '-', as that is allowed in qrexec arguments. For example device 0000:c1:00.0 behind bridge at 0000:00:08.1 will get path 0000_00_08.1-00_00.0 (assuming bridge 0000:00:08.1 has secondary bus at 0xc1). Resolving device path requires access to the system's sysfs. PCIDevice object will automatically resolve old device identifiers, which partially covers migration path. TODO: - finalize migration path - adjust device assignments too - consider still allowing attaching via SBDF, or otherwise expose function to translate SBDF to PCI path (an Admin API call?) - fix salt listing/attaching devices to sys-net/sys-usb - depending on the above Fixes QubesOS/qubes-issues#8681
1 parent f8ffb89 commit 6ac4aa7

13 files changed

Lines changed: 221 additions & 23 deletions

File tree

qubes/ext/pci.py

Lines changed: 162 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
unsupported_devices_warned = set()
4545

4646

47+
SYSFS_PCI_DEVS_BASE = "/sys/bus/pci/devices"
48+
49+
4750
class UnsupportedDevice(Exception):
4851
pass
4952

@@ -135,25 +138,155 @@ def _device_desc(hostdev_xml):
135138
)
136139

137140

141+
def sbdf_to_path(device_id: str):
142+
"""
143+
Lookup full path for a given device
144+
145+
:param device_id: sbdf, for example 0000:02:03.0; accepts also libvirt
146+
format like 0000_02_03_0
147+
:return: converted identifier of None if device is not found
148+
"""
149+
regex = re.compile(
150+
r"\A(?:pci_)?((?P<segment>[0-9a-f]{4})[_:])?(?P<bus>[0-9a-f]{2})[_:]"
151+
r"(?P<device>[0-9a-f]{2})[._](?P<function>[0-9a-f])\Z"
152+
)
153+
154+
dev_match = regex.match(device_id)
155+
if not dev_match:
156+
raise ValueError("Invalid device identifier: {!r}".format(device_id))
157+
if dev_match["segment"] is not None:
158+
segment = dev_match["segment"]
159+
else:
160+
segment = "0000"
161+
if int(dev_match["bus"], 16) == 0:
162+
return (f"{segment}_" if segment != "0000" else "") + (
163+
f"{dev_match['bus']}_"
164+
f"{dev_match['device']}.{dev_match['function']}"
165+
)
166+
sbdf = (
167+
f"{segment}:{dev_match['bus']}:"
168+
f"{dev_match['device']}.{dev_match['function']}"
169+
)
170+
try:
171+
sysfs_path = os.readlink(f"{SYSFS_PCI_DEVS_BASE}/{sbdf}")
172+
except FileNotFoundError:
173+
return None
174+
# example: ../../../devices/pci0000:00/0000:00:1a.0/0000:02:00.0
175+
path = sysfs_path.partition(f"/pci{segment}:")[2]
176+
# drop also 00/ part, which may be also 40/, 80/ etc
177+
path = path[3:]
178+
bus_offset = 0
179+
result_list = []
180+
for path_part in path.split("/"):
181+
assert bus_offset != -1
182+
bridge_match = regex.match(path_part)
183+
if not bridge_match:
184+
raise ValueError("Invalid bridge found: {!r}".format(path_part))
185+
assert int(bridge_match["bus"], 16) >= bus_offset
186+
bus_num = int(bridge_match["bus"], 16) - bus_offset
187+
bridge_str = (
188+
f"{bus_num:02x}_{bridge_match['device']}."
189+
f"{bridge_match['function']}"
190+
)
191+
result_list.append(bridge_str)
192+
try:
193+
with open(
194+
f"{SYSFS_PCI_DEVS_BASE}/{path_part}/secondary_bus_number",
195+
encoding="ascii",
196+
) as f_bus_num:
197+
# this one is in decimal
198+
# this can raise ValueError, propagate it
199+
bus_offset = int(f_bus_num.read())
200+
except FileNotFoundError:
201+
# last device in chain
202+
bus_offset = -1
203+
204+
if segment == "0000":
205+
return "-".join(result_list)
206+
return segment + "_" + "-".join(result_list)
207+
208+
209+
def path_to_sbdf(path: str):
210+
"""
211+
Convert device path as done by *sbdf_to_path* back to SBDF
212+
:param path:
213+
:return:
214+
"""
215+
216+
regex = re.compile(
217+
r"\A(?P<bus>[0-9a-f]+)[_:]"
218+
r"(?P<device>[0-9a-f]+)[._](?P<function>[0-9a-f]+)\Z"
219+
)
220+
221+
segment_re = re.compile(r"\A(?P<segment>[0-9a-f]{4})[_:](?P<rest>.*)\Z")
222+
223+
# default segment
224+
segment = "0000"
225+
bus_offset = 0
226+
current_dev = ""
227+
for path_part in path.split("-"):
228+
assert bus_offset != -1
229+
# first part may include segment
230+
if bus_offset == 0:
231+
segment_match = segment_re.match(path_part)
232+
if segment_match:
233+
segment = segment_match["segment"]
234+
path_part = segment_match["rest"]
235+
part_match = regex.match(path_part)
236+
if not part_match:
237+
raise ValueError(
238+
"Invalid PCI device path at {!r}".format(path_part)
239+
)
240+
bus_num = int(part_match["bus"], 16) + bus_offset
241+
current_dev = (
242+
f"{segment}:{bus_num:02x}:{part_match['device']}."
243+
f"{part_match['function']}"
244+
)
245+
try:
246+
with open(
247+
f"{SYSFS_PCI_DEVS_BASE}/" f"{current_dev}/secondary_bus_number",
248+
encoding="ascii",
249+
) as f_bus_num:
250+
# this one is in decimal
251+
# this can raise ValueError, propagate it
252+
bus_offset = int(f_bus_num.read())
253+
except FileNotFoundError:
254+
# last device in chain
255+
bus_offset = -1
256+
257+
return current_dev
258+
259+
260+
def is_pci_path(device_id: str):
261+
"""Check if given device id is already a device path.
262+
263+
:param device_id: device id to check
264+
:return:
265+
"""
266+
path_re = re.compile(
267+
r"\A([0-9a-f]{4}_)?00_[0-9a-f]{2}\.[0-9a-f]"
268+
r"(-[0-9a-f]{2}_[0-9a-f]{2}\.[0-9a-f])*\Z"
269+
)
270+
return bool(path_re.match(device_id))
271+
272+
138273
class PCIDevice(qubes.device_protocol.DeviceInfo):
139274
# pylint: disable=too-few-public-methods
140275
regex = re.compile(
141-
r"\A(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)\."
142-
r"(?P<function>[0-9a-f]+)\Z"
276+
r"\A((?P<segment>[0-9a-f]{4})[_:])?(?P<bus>[0-9a-f]{2})[_:]"
277+
r"(?P<device>[0-9a-f]{2})\.(?P<function>[0-9a-f])\Z"
143278
)
144279
_libvirt_regex = re.compile(
145-
r"\Apci_0000_(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)_"
146-
r"(?P<function>[0-9a-f]+)\Z"
280+
r"\Apci_(?P<segment>[0-9a-f]{4})_(?P<bus>[0-9a-f]{2})_"
281+
r"(?P<device>[0-9a-f]{2})_(?P<function>[0-9a-f])\Z"
147282
)
148283

149284
def __init__(self, port: Port, libvirt_name=None):
150285
if libvirt_name:
151286
dev_match = self._libvirt_regex.match(libvirt_name)
152287
if not dev_match:
153288
raise UnsupportedDevice(libvirt_name)
154-
port_id = "{bus}_{device}.{function}".format(
155-
**dev_match.groupdict()
156-
)
289+
port_id = sbdf_to_path(libvirt_name)
157290
port = Port(
158291
backend_domain=port.backend_domain,
159292
port_id=port_id,
@@ -162,14 +295,24 @@ def __init__(self, port: Port, libvirt_name=None):
162295

163296
super().__init__(port)
164297

165-
dev_match = self.regex.match(port.port_id)
298+
if is_pci_path(port.port_id):
299+
sbdf = path_to_sbdf(port.port_id)
300+
else:
301+
sbdf = port.port_id
302+
dev_match = self.regex.match(sbdf)
166303
if not dev_match:
167304
raise ValueError(
168-
"Invalid device identifier: {!r}".format(port.port_id)
305+
"Invalid device identifier: {!r} (sbdf: {!r})".format(
306+
port.port_id, sbdf
307+
)
169308
)
170309

310+
self.sbdf = sbdf
311+
171312
for group in self.regex.groupindex:
172313
setattr(self, group, dev_match.group(group))
314+
if getattr(self, "segment") is None:
315+
self.segment = "0000"
173316

174317
# lazy loading
175318
self._description: Optional[str] = None
@@ -249,7 +392,7 @@ def parent_device(self) -> Optional[qubes.device_protocol.DeviceInfo]:
249392
def libvirt_name(self):
250393
# pylint: disable=no-member
251394
# noinspection PyUnresolvedReferences
252-
return f"pci_0000_{self.bus}_{self.device}_{self.function}"
395+
return f"pci_{self.segment}_{self.bus}_{self.device}_{self.function}"
253396

254397
@property
255398
def description(self):
@@ -380,31 +523,31 @@ def on_device_list_attached(self, vm, event, **kwargs):
380523
if hostdev.get("type") != "pci":
381524
continue
382525
address = hostdev.find("source/address")
526+
segment = address.get("domain")[2:]
383527
bus = address.get("bus")[2:]
384528
device = address.get("slot")[2:]
385529
function = address.get("function")[2:]
386530

387-
port_id = "{bus}_{device}.{function}".format(
531+
libvirt_name = "pci_{segment}_{bus}_{device}_{function}".format(
532+
segment=segment,
388533
bus=bus,
389534
device=device,
390535
function=function,
391536
)
392537
yield PCIDevice(
393538
Port(
394539
backend_domain=vm.app.domains[0],
395-
port_id=port_id,
540+
port_id=None,
396541
devclass="pci",
397-
)
542+
),
543+
libvirt_name=libvirt_name,
398544
), {}
399545

400546
@qubes.ext.handler("device-pre-attach:pci")
401547
def on_device_pre_attached_pci(self, vm, event, device, options):
402548
# pylint: disable=unused-argument
403-
if not os.path.exists(
404-
"/sys/bus/pci/devices/0000:{}".format(
405-
device.port_id.replace("_", ":")
406-
)
407-
):
549+
sbdf = path_to_sbdf(device.port_id)
550+
if sbdf is None or not os.path.exists(f"/sys/bus/pci/devices/{sbdf}"):
408551
raise qubes.exc.QubesException(
409552
"Invalid PCI device: {}".format(device.port_id)
410553
)
@@ -458,7 +601,7 @@ def on_device_pre_detached_pci(self, vm, event, port):
458601
) as p:
459602
result = p.communicate()[0].decode()
460603
m = re.search(
461-
r"^(\d+.\d+)\s+0000:{}$".format(device.port_id.replace("_", ":")),
604+
r"^(\d+.\d+)\s+{}$".format(device.sbdf),
462605
result,
463606
flags=re.MULTILINE,
464607
)

qubes/tests/devices_pci.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
1717
#
1818
# You should have received a copy of the GNU Lesser General Public
1919
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
20+
import os.path
21+
import unittest
2022
from unittest import mock
2123

2224
import qubes.tests
2325
import qubes.ext.pci
2426
from qubes.device_protocol import DeviceInterface
27+
from qubes.ext.pci import sbdf_to_path, path_to_sbdf
28+
29+
orig_open = open
2530

2631

2732
class TestVM(object):
@@ -107,17 +112,55 @@ def mock_file_open(filename: str, *_args, **_kwargs):
107112
\t09 CANBUS
108113
\t80 Serial bus controller
109114
"""
110-
elif filename.startswith("/sys/devices/pci"):
111-
content = "0x0c0330"
112115
else:
113-
raise OSError()
116+
return orig_open(filename, *_args, **_kwargs)
114117

115118
file_object = mock.mock_open(read_data=content).return_value
116119
file_object.__iter__.return_value = content
117120
return file_object
118121

119122

120-
class TC_00_Block(qubes.tests.QubesTestCase):
123+
# prefer location in git checkout
124+
tests_sysfs_path = (
125+
os.path.dirname(__file__) + "/../../tests-data/sysfs/sys/bus/pci/devices"
126+
)
127+
if not os.path.exists(tests_sysfs_path):
128+
# but if not there, look for package installed one
129+
tests_sysfs_path = "/usr/share/qubes/tests-data/sysfs/sys/bus/pci/devices"
130+
131+
132+
@mock.patch("qubes.ext.pci.SYSFS_PCI_DEVS_BASE", tests_sysfs_path)
133+
class TC_00_helpers(qubes.tests.QubesTestCase):
134+
def test_000_sbdf_to_path1(self):
135+
path = sbdf_to_path("0000:c6:00.0")
136+
self.assertEqual(path, "c0_03.5-00_00.0-00_00.0")
137+
138+
def test_001_sbdf_to_path2(self):
139+
path = sbdf_to_path("0000:00:18.4")
140+
self.assertEqual(path, "00_18.4")
141+
142+
def test_002_sbdf_to_path_libvirt(self):
143+
path = sbdf_to_path("pci_0000_00_18_4")
144+
self.assertEqual(path, "00_18.4")
145+
146+
def test_003_sbdf_to_path_default_segment1(self):
147+
path = sbdf_to_path("00:18.4")
148+
self.assertEqual(path, "00_18.4")
149+
150+
def test_004_sbdf_to_path_default_segment2(self):
151+
path = sbdf_to_path("0000:00:18.4")
152+
self.assertEqual(path, "00_18.4")
153+
154+
def test_010_path_to_sbdf1(self):
155+
path = path_to_sbdf("0000_c0_03.5-00_00.0-00_00.0")
156+
self.assertEqual(path, "0000:c6:00.0")
157+
158+
def test_011_path_to_sbdf2(self):
159+
path = path_to_sbdf("0000_00_18.4")
160+
self.assertEqual(path, "0000:00:18.4")
161+
162+
163+
class TC_10_PCI(qubes.tests.QubesTestCase):
121164
def setUp(self):
122165
super().setUp()
123166
self.ext = qubes.ext.pci.PCIDeviceExtension()

qubes/tests/vm/qubesvm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,7 @@ def test_600_libvirt_xml_hvm_pcidev(self):
15271527
<hostdev type="pci" managed="yes">
15281528
<source>
15291529
<address
1530+
domain="0x0000"
15301531
bus="0x00"
15311532
slot="0x00"
15321533
function="0x0" />
@@ -1637,6 +1638,7 @@ def test_600_libvirt_xml_hvm_pcidev_s0ix(self):
16371638
<source
16381639
powerManagementFiltering="no">
16391640
<address
1641+
domain="0x0000"
16401642
bus="0x00"
16411643
slot="0x00"
16421644
function="0x0" />

rpm_spec/core-dom0.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ done
568568
/usr/share/qubes/templates/libvirt/devices/pci.xml
569569
/usr/share/qubes/templates/libvirt/devices/net.xml
570570
/usr/share/qubes/tests-data/dispvm-open-thunderbird-attachment
571+
/usr/share/qubes/tests-data/sysfs
571572
/usr/lib/tmpfiles.d/qubes.conf
572573
%attr(0755,root,root) /usr/lib/qubes/create-snapshot
573574
%attr(0755,root,root) /usr/lib/qubes/destroy-snapshot

templates/libvirt/devices/pci.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
{% endif %}
1313
>
1414
<address
15+
domain="0x{{ device.segment }}"
1516
bus="0x{{ device.bus }}"
1617
slot="0x{{ device.device }}"
1718
function="0x{{ device.function }}" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../devices/pci0000:00/0000:00:18.4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../devices/pci0000:c0/0000:c0:03.5
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../devices/pci0000:c0/0000:c0:03.5/0000:c5:00.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../devices/pci0000:c0/0000:c0:03.5/0000:c5:00.0/0000:c6:00.0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
198

0 commit comments

Comments
 (0)