Skip to content

Commit b73fc8e

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 b73fc8e

13 files changed

Lines changed: 219 additions & 23 deletions

File tree

qubes/ext/pci.py

Lines changed: 160 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,153 @@ 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 format
146+
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+
bridge_match = regex.match(path_part)
182+
if not bridge_match:
183+
raise ValueError("Invalid bridge found: {!r}".format(path_part))
184+
assert int(bridge_match["bus"], 16) >= bus_offset
185+
bus_num = int(bridge_match["bus"], 16) - bus_offset
186+
bridge_str = (
187+
f"{bus_num:02x}_{bridge_match['device']}."
188+
f"{bridge_match['function']}"
189+
)
190+
result_list.append(bridge_str)
191+
try:
192+
with open(
193+
f"{SYSFS_PCI_DEVS_BASE}/{path_part}/secondary_bus_number"
194+
) as f:
195+
# this one is in decimal
196+
# this can raise ValueError, propagate it
197+
bus_offset = int(f.read())
198+
except FileNotFoundError:
199+
# last device in chain, set to None so any further arithmetic
200+
# will fail
201+
bus_offset = None
202+
203+
if segment == "0000":
204+
return "-".join(result_list)
205+
return segment + "_" + "-".join(result_list)
206+
207+
208+
def path_to_sbdf(path: str):
209+
"""
210+
Convert device path as done by *sbdf_to_path* back to SBDF
211+
:param path:
212+
:return:
213+
"""
214+
215+
regex = re.compile(
216+
r"\A(?P<bus>[0-9a-f]+)[_:]"
217+
r"(?P<device>[0-9a-f]+)[._](?P<function>[0-9a-f]+)\Z"
218+
)
219+
220+
segment_re = re.compile(r"\A(?P<segment>[0-9a-f]{4})[_:](?P<rest>.*)\Z")
221+
222+
# default segment
223+
segment = "0000"
224+
bus_offset = 0
225+
current_dev = ""
226+
for path_part in path.split("-"):
227+
# first part may include segment
228+
if bus_offset == 0:
229+
segment_match = segment_re.match(path_part)
230+
if segment_match:
231+
segment = segment_match["segment"]
232+
path_part = segment_match["rest"]
233+
part_match = regex.match(path_part)
234+
if not part_match:
235+
raise ValueError(
236+
"Invalid PCI device path at {!r}".format(path_part)
237+
)
238+
bus_num = int(part_match["bus"], 16) + bus_offset
239+
current_dev = (
240+
f"{segment}:{bus_num:02x}:{part_match['device']}."
241+
f"{part_match['function']}"
242+
)
243+
try:
244+
with open(
245+
f"{SYSFS_PCI_DEVS_BASE}/" f"{current_dev}/secondary_bus_number"
246+
) as f:
247+
# this one is in decimal
248+
# this can raise ValueError, propagate it
249+
bus_offset = int(f.read())
250+
except FileNotFoundError:
251+
# last device in chain, set to None so any further arithmetic
252+
# will fail
253+
bus_offset = None
254+
255+
return current_dev
256+
257+
258+
def is_pci_path(device_id: str):
259+
"""Check if given device id is already a device path.
260+
261+
:param device_id: device id to check
262+
:return:
263+
"""
264+
path_re = re.compile(
265+
r"\A([0-9a-f]{4}_)?00_[0-9a-f]{2}\.[0-9a-f]"
266+
r"(-[0-9a-f]{2}_[0-9a-f]{2}\.[0-9a-f])*\Z"
267+
)
268+
return bool(path_re.match(device_id))
269+
270+
138271
class PCIDevice(qubes.device_protocol.DeviceInfo):
139272
# pylint: disable=too-few-public-methods
140273
regex = re.compile(
141-
r"\A(?P<bus>[0-9a-f]+)_(?P<device>[0-9a-f]+)\."
142-
r"(?P<function>[0-9a-f]+)\Z"
274+
r"\A((?P<segment>[0-9a-f]{4})[_:])?(?P<bus>[0-9a-f]{2})[_:]"
275+
r"(?P<device>[0-9a-f]{2})\.(?P<function>[0-9a-f])\Z"
143276
)
144277
_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"
278+
r"\Apci_(?P<segment>[0-9a-f]{4})_(?P<bus>[0-9a-f]{2})_"
279+
r"(?P<device>[0-9a-f]{2})_(?P<function>[0-9a-f])\Z"
147280
)
148281

149282
def __init__(self, port: Port, libvirt_name=None):
150283
if libvirt_name:
151284
dev_match = self._libvirt_regex.match(libvirt_name)
152285
if not dev_match:
153286
raise UnsupportedDevice(libvirt_name)
154-
port_id = "{bus}_{device}.{function}".format(
155-
**dev_match.groupdict()
156-
)
287+
port_id = sbdf_to_path(libvirt_name)
157288
port = Port(
158289
backend_domain=port.backend_domain,
159290
port_id=port_id,
@@ -162,14 +293,24 @@ def __init__(self, port: Port, libvirt_name=None):
162293

163294
super().__init__(port)
164295

165-
dev_match = self.regex.match(port.port_id)
296+
if is_pci_path(port.port_id):
297+
sbdf = path_to_sbdf(port.port_id)
298+
else:
299+
sbdf = port.port_id
300+
dev_match = self.regex.match(sbdf)
166301
if not dev_match:
167302
raise ValueError(
168-
"Invalid device identifier: {!r}".format(port.port_id)
303+
"Invalid device identifier: {!r} (sbdf: {!r})".format(
304+
port.port_id, sbdf
305+
)
169306
)
170307

308+
self.sbdf = sbdf
309+
171310
for group in self.regex.groupindex:
172311
setattr(self, group, dev_match.group(group))
312+
if self.segment is None:
313+
self.segment = "0000"
173314

174315
# lazy loading
175316
self._description: Optional[str] = None
@@ -249,7 +390,7 @@ def parent_device(self) -> Optional[qubes.device_protocol.DeviceInfo]:
249390
def libvirt_name(self):
250391
# pylint: disable=no-member
251392
# noinspection PyUnresolvedReferences
252-
return f"pci_0000_{self.bus}_{self.device}_{self.function}"
393+
return f"pci_{self.segment}_{self.bus}_{self.device}_{self.function}"
253394

254395
@property
255396
def description(self):
@@ -380,31 +521,31 @@ def on_device_list_attached(self, vm, event, **kwargs):
380521
if hostdev.get("type") != "pci":
381522
continue
382523
address = hostdev.find("source/address")
524+
segment = address.get("domain")[2:]
383525
bus = address.get("bus")[2:]
384526
device = address.get("slot")[2:]
385527
function = address.get("function")[2:]
386528

387-
port_id = "{bus}_{device}.{function}".format(
529+
libvirt_name = "pci_{segment}_{bus}_{device}_{function}".format(
530+
segment=segment,
388531
bus=bus,
389532
device=device,
390533
function=function,
391534
)
392535
yield PCIDevice(
393536
Port(
394537
backend_domain=vm.app.domains[0],
395-
port_id=port_id,
538+
port_id=None,
396539
devclass="pci",
397-
)
540+
),
541+
libvirt_name=libvirt_name,
398542
), {}
399543

400544
@qubes.ext.handler("device-pre-attach:pci")
401545
def on_device_pre_attached_pci(self, vm, event, device, options):
402546
# pylint: disable=unused-argument
403-
if not os.path.exists(
404-
"/sys/bus/pci/devices/0000:{}".format(
405-
device.port_id.replace("_", ":")
406-
)
407-
):
547+
sbdf = path_to_sbdf(device.port_id)
548+
if sbdf is None or not os.path.exists(f"/sys/bus/pci/devices/{sbdf}"):
408549
raise qubes.exc.QubesException(
409550
"Invalid PCI device: {}".format(device.port_id)
410551
)
@@ -458,7 +599,7 @@ def on_device_pre_detached_pci(self, vm, event, port):
458599
) as p:
459600
result = p.communicate()[0].decode()
460601
m = re.search(
461-
r"^(\d+.\d+)\s+0000:{}$".format(device.port_id.replace("_", ":")),
602+
r"^(\d+.\d+)\s+{}$".format(device.sbdf),
462603
result,
463604
flags=re.MULTILINE,
464605
)

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)