
On 3/17/25 21:59, Maximilian Martin via Devel wrote:
From: Maximilian Martin <maximilian_martin@gmx.de>
Currently, only vendor/product and bus/device matching are supported for USB host devices. Neither of these provide a stable and persistent way of assigning a guest a specific host device. Vendor/product can be ambiguous. Device numbers change on every enumeration.
This patch adds a bus/port matching, which allows a specific port on the host to be specified using the dotted notation found in Linux's "devpath" sysfs attribute.
This patch is based on the previous work of Thomas Hebb: https://lists.libvirt.org/archives/list/devel@lists.libvirt.org/message/7U3H...
Resolves: https://gitlab.com/libvirt/libvirt/-/issues/513
Signed-off-by: Maximilian Martin <maximilian_martin@gmx.de> ---
There's too much going on in a single patch. Please split the change into multiple patches.
docs/formatdomain.rst | 29 ++-- src/conf/domain_conf.c | 70 +++++++- src/conf/domain_conf.h | 1 + src/conf/schemas/domaincommon.rng | 11 +- src/hypervisor/virhostdev.c | 131 +++++++++------ src/libvirt_private.syms | 2 - src/util/virusb.c | 159 ++++++------------ src/util/virusb.h | 32 ++-- tests/virusbtest.c | 149 +++++++++++----- .../sys_bus_usb/devices/1-1.5.3.1/devpath | 1 + .../sys_bus_usb/devices/1-1.5.3.3/devpath | 1 + .../sys_bus_usb/devices/1-1.5.3/devpath | 1 + .../sys_bus_usb/devices/1-1.5.4/devpath | 1 + .../sys_bus_usb/devices/1-1.5.5/devpath | 1 + .../sys_bus_usb/devices/1-1.5.6/devpath | 1 + .../sys_bus_usb/devices/1-1.5/devpath | 1 + .../sys_bus_usb/devices/1-1.6/devpath | 1 + .../sys_bus_usb/devices/1-1/devpath | 1 + .../sys_bus_usb/devices/2-1.2/devpath | 1 + .../sys_bus_usb/devices/2-1/devpath | 1 + .../sys_bus_usb/devices/usb1/devpath | 1 + .../sys_bus_usb/devices/usb2/devpath | 1 + .../sys_bus_usb/devices/usb3/devpath | 1 + .../sys_bus_usb/devices/usb4/devpath | 1 + 24 files changed, 355 insertions(+), 244 deletions(-) create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.1/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3.3/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.3/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.4/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.5/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5.6/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.5/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1.6/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/1-1/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/2-1.2/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/2-1/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb1/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb2/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb3/devpath create mode 100644 tests/virusbtestdata/sys_bus_usb/devices/usb4/devpath
diff --git a/docs/formatdomain.rst b/docs/formatdomain.rst index 543b36cb2e..bc3c11613f 100644 --- a/docs/formatdomain.rst +++ b/docs/formatdomain.rst @@ -4696,19 +4696,22 @@ or: tweak the loading process further using the ``bar`` or ``file`` attributes will be rejected. :since:`Since 4.3.0 (QEMU and KVM only)`. ``address`` - The ``address`` element for USB devices has a ``bus`` and ``device`` - attribute to specify the USB bus and device number the device appears at on - the host. The values of these attributes can be given in decimal, hexadecimal - (starting with 0x) or octal (starting with 0) form. For PCI devices the - element carries 4 attributes allowing to designate the device as can be found - with the ``lspci`` or with ``virsh nodedev-list``. For SCSI devices a 'drive' - address type must be used. For mediated devices, which are software-only - devices defining an allocation of resources on the physical parent device, - the address type used must conform to the ``model`` attribute of element - ``hostdev``, e.g. any address type other than PCI for ``vfio-pci`` device API - or any address type other than CCW for ``vfio-ccw`` device API will result in - an error. See the `Device Addresses`_ section for more details on the address - element. + The ``address`` element for USB devices has a ``bus`` attribute to specify + the USB bus. In addition, either a ``device`` attribute or a ``port`` + attribute is required to identify the device on the host. While the device + number is assigned upon connection of the device, the port number is a + stable identifier of the physical host port. Bus and device number can be + given in decimal, hexadecimal (starting with 0x) or octal (starting with 0) + form. The port number is a dotted path (examples: ``2``, ``1.2.5``). For PCI + devices the element carries 4 attributes allowing to designate the device as + can be found with the ``lspci`` or with ``virsh nodedev-list``. For SCSI + devices a 'drive' address type must be used. For mediated devices, which are + software-only devices defining an allocation of resources on the physical + parent device, the address type used must conform to the ``model`` attribute + of element ``hostdev``, e.g. any address type other than PCI for ``vfio-pci`` + device API or any address type other than CCW for ``vfio-ccw`` device API + will result in an error. See the `Device Addresses`_ section for more details + on the address element. ``driver`` PCI hostdev devices can have an optional ``driver`` subelement that specifies which host driver to bind to the device when preparing it diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c index b94cf99236..bd9c651a07 100644 --- a/src/conf/domain_conf.c +++ b/src/conf/domain_conf.c @@ -2657,6 +2657,15 @@ virDomainHostdevSubsysSCSIClear(virDomainHostdevSubsysSCSI *scsisrc) } }
+static void +virDomainHostdevSubsysUSBClear(virDomainHostdevSubsysUSB *usbsrc) +{ + if (!usbsrc) + return; + + VIR_FREE(usbsrc->port); +} +
static void virDomainHostdevDefClear(virDomainHostdevDef *def) @@ -2700,6 +2709,8 @@ virDomainHostdevDefClear(virDomainHostdevDef *def) g_clear_pointer(&def->source.subsys.u.pci.origstates, virBitmapFree); break; case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB: + virDomainHostdevSubsysUSBClear(&def->source.subsys.u.usb); + break; case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_MDEV: case VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_LAST: break; @@ -5841,6 +5852,9 @@ virDomainHostdevSubsysUSBDefParseXML(xmlNodePtr node, xmlNodePtr productNode; xmlNodePtr addressNode; virTristateBool autoAddress; + bool found_device, found_port; + char *port; + int ret = -1;
These variables are only used ...
VIR_XPATH_NODE_AUTORESTORE(ctxt)
ctxt->node = node; @@ -5891,13 +5905,45 @@ virDomainHostdevSubsysUSBDefParseXML(xmlNodePtr node, }
if ((addressNode = virXPathNode("./address", ctxt))) { - if (virXMLPropUInt(addressNode, "bus", 0, - VIR_XML_PROP_REQUIRED, &usbsrc->bus) < 0)
... inside of this block. Declare them in it please.
+ found_device = false; + found_port = false; + + ret = virXMLPropUInt(addressNode, "bus", 0, + VIR_XML_PROP_REQUIRED, &usbsrc->bus); + if (ret < 0) { + return -1; + } else if (ret == 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("missing bus")); + return -1; + } + + ret = virXMLPropUInt(addressNode, "device", 0, + VIR_XML_PROP_NONE, &usbsrc->device); + if (ret < 0) return -1; + else if (ret > 0) + found_device = true;
- if (virXMLPropUInt(addressNode, "device", 0, - VIR_XML_PROP_REQUIRED, &usbsrc->device) < 0) + port = virXMLPropString(addressNode, "port"); + if (port) { + if (*port) { + usbsrc->port = port; + found_port = true; + } else { + VIR_FREE(port); + } + } + + if (!found_device && !found_port) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("usb address needs either device id or port")); return -1; + } else if (found_device && found_port) { + virReportError(VIR_ERR_INTERNAL_ERROR, + "%s", _("found both device id and port in usb address (ambiguous setting)")); + return -1; + } }
return 0; @@ -14522,8 +14568,13 @@ virDomainHostdevMatchSubsysUSB(virDomainHostdevDef *first, virDomainHostdevSubsysUSB *first_usbsrc = &first->source.subsys.u.usb; virDomainHostdevSubsysUSB *second_usbsrc = &second->source.subsys.u.usb;
- if (first_usbsrc->bus && first_usbsrc->device) { - /* specified by bus location on host */ + if (first_usbsrc->bus && first_usbsrc->port) { + /* specified by bus and port on host */ + if (first_usbsrc->bus == second_usbsrc->bus && + STREQ_NULLABLE(first_usbsrc->port, second_usbsrc->port)) + return 1; + } else if (first_usbsrc->bus && first_usbsrc->device) { + /* specified by bus and device id on host */ if (first_usbsrc->bus == second_usbsrc->bus && first_usbsrc->device == second_usbsrc->device) return 1; @@ -23957,10 +24008,15 @@ virDomainHostdevDefFormatSubsysUSB(virBuffer *buf, virBufferAsprintf(&sourceChildBuf, "<product id='0x%.4x'/>\n", usbsrc->product); }
- if (usbsrc->bus || usbsrc->device) + if (usbsrc->bus && usbsrc->port) { + virBufferAsprintf(&sourceChildBuf, "<address %sbus='%d' port='%s'/>\n", + includeTypeInAddr ? "type='usb' " : "", + usbsrc->bus, usbsrc->port); + } else if (usbsrc->bus || usbsrc->device) { virBufferAsprintf(&sourceChildBuf, "<address %sbus='%d' device='%d'/>\n", includeTypeInAddr ? "type='usb' " : "", usbsrc->bus, usbsrc->device); + }
virXMLFormatElement(buf, "source", &sourceAttrBuf, &sourceChildBuf); } diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h index 3a97fd866c..e5f53c09ac 100644 --- a/src/conf/domain_conf.h +++ b/src/conf/domain_conf.h @@ -228,6 +228,7 @@ struct _virDomainHostdevSubsysUSB { on vendor/product */ unsigned bus; unsigned device; + char *port;
unsigned vendor; unsigned product; diff --git a/src/conf/schemas/domaincommon.rng b/src/conf/schemas/domaincommon.rng index 3276569325..00d97f0cd8 100644 --- a/src/conf/schemas/domaincommon.rng +++ b/src/conf/schemas/domaincommon.rng @@ -6687,9 +6687,14 @@ <attribute name="bus"> <ref name="usbAddr"/> </attribute> - <attribute name="device"> - <ref name="usbAddr"/> - </attribute> + <choice> + <attribute name="device"> + <ref name="usbAddr"/> + </attribute> + <attribute name="port"> + <ref name="usbPort"/> + </attribute> + </choice> </element> </define> <define name="scsiaddress"> diff --git a/src/hypervisor/virhostdev.c b/src/hypervisor/virhostdev.c index 0a1d8500d4..9e77477a9b 100644 --- a/src/hypervisor/virhostdev.c +++ b/src/hypervisor/virhostdev.c @@ -1358,6 +1358,42 @@ virHostdevMarkUSBDevices(virHostdevManager *mgr, return -1; }
+static int +virHostdevFindUSBDeviceWithFlags(virDomainHostdevDef *hostdev, + bool mandatory, + unsigned int flags, + virUSBDevice **usb) +{ + virDomainHostdevSubsysUSB *usbsrc = &hostdev->source.subsys.u.usb; + unsigned vendor = usbsrc->vendor; + unsigned product = usbsrc->product; + unsigned bus = usbsrc->bus; + char *port = usbsrc->port; + unsigned device = usbsrc->device; + virUSBDeviceList *devs; + int rc; + + rc = virUSBDeviceFind(vendor, product, bus, device, port, NULL, + mandatory, flags, &devs); + if (rc < 0) + return -1; + + if (rc == 1) { + *usb = virUSBDeviceListGet(devs, 0); + virUSBDeviceListSteal(devs, *usb); + } + virObjectUnref(devs); + + if (rc > 1) { + virReportError(VIR_ERR_OPERATION_FAILED, + _("Multiple USB devices for %1$x:%2$x, use <address> to specify one"), + vendor, product); + return -1; + } + + return rc; +} +
int virHostdevFindUSBDevice(virDomainHostdevDef *hostdev, @@ -1366,77 +1402,64 @@ virHostdevFindUSBDevice(virDomainHostdevDef *hostdev, { virDomainHostdevSubsysUSB *usbsrc = &hostdev->source.subsys.u.usb; unsigned vendor = usbsrc->vendor; - unsigned product = usbsrc->product; unsigned bus = usbsrc->bus; unsigned device = usbsrc->device; + char *port = usbsrc->port; bool autoAddress = usbsrc->autoAddress; + unsigned int flags = 0; int rc;
*usb = NULL;
- if (vendor && bus) { - rc = virUSBDeviceFind(vendor, product, bus, device, - NULL, - autoAddress ? false : mandatory, - usb); - if (rc < 0) { - return -1; - } else if (!autoAddress) { - goto out; - } else { - VIR_INFO("USB device %x:%x could not be found at previous" - " address (bus:%u device:%u)", - vendor, product, bus, device); - } + if (vendor) + flags |= USB_DEVICE_FIND_BY_VENDOR; + if (device) + flags |= USB_DEVICE_FIND_BY_DEVICE; + if (port) + flags |= USB_DEVICE_FIND_BY_PORT; + + /* Rule out invalid cases. */ + if (vendor && device && port) { + VIR_WARN("Cannot match USB device on vendor/product, bus/device, and bus/port at once. Ignoring bus/device."); + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); + } else if (device && port) { + VIR_WARN("Cannot match USB device on bus/device and bus/port at once. Ignoring bus/device."); + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); + } else if (!vendor && !device && !port) { + VIR_WARN("No matching fields for USB device found. Vendor/product, bus/device, or bus/port required."); + return -1; }
- /* When vendor is specified, its USB address is either unspecified or the - * device could not be found at the USB device where it had been - * automatically found before. - */ - if (vendor) { - g_autoptr(virUSBDeviceList) devs = NULL; + /* First attempt, matching on all valid fields. */ + rc = virHostdevFindUSBDeviceWithFlags(hostdev, + autoAddress ? false : mandatory, + flags, usb); + if (rc < 0) + return -1;
- rc = virUSBDeviceFindByVendor(vendor, product, NULL, mandatory, &devs); - if (rc < 0) { - return -1; - } else if (rc == 0) { - goto out; - } else if (rc > 1) { - if (autoAddress) { - virReportError(VIR_ERR_OPERATION_FAILED, - _("Multiple USB devices for %1$x:%2$x were found, but none of them is at bus:%3$u device:%4$u"), - vendor, product, bus, device); - } else { - virReportError(VIR_ERR_OPERATION_FAILED, - _("Multiple USB devices for %1$x:%2$x, use <address> to specify one"), - vendor, product); - } + if (rc != 1 && autoAddress && device) { + VIR_INFO("USB device could not be found at previous address " + "(bus:%u device:%u)", bus, device); + + /* Second attempt, for when the device number has changed. */ + flags &= ~((unsigned int) USB_DEVICE_FIND_BY_DEVICE); + usbsrc->device = 0; + + rc = virHostdevFindUSBDeviceWithFlags(hostdev, mandatory, + flags, usb); + if (rc < 0) return -1; - }
- *usb = virUSBDeviceListGet(devs, 0); - virUSBDeviceListSteal(devs, *usb); + usbsrc->autoAddress = true; + }
+ if (!*usb) { + hostdev->missing = true; + } else if (!usbsrc->bus || !usbsrc->device) { usbsrc->bus = virUSBDeviceGetBus(*usb); usbsrc->device = virUSBDeviceGetDevno(*usb); - usbsrc->autoAddress = true; - - if (autoAddress) { - VIR_INFO("USB device %x:%x found at bus:%u device:%u (moved" - " from bus:%u device:%u)", - vendor, product, - usbsrc->bus, usbsrc->device, - bus, device); - } - } else if (bus) { - if (virUSBDeviceFindByBus(bus, device, NULL, mandatory, usb) < 0) - return -1; }
- out: - if (!*usb) - hostdev->missing = true; return 0; }
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index e63939e2b5..dbc0778fdb 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -3668,8 +3668,6 @@ virURIResolveAlias; # util/virusb.h virUSBDeviceFileIterate; virUSBDeviceFind; -virUSBDeviceFindByBus; -virUSBDeviceFindByVendor; virUSBDeviceFree; virUSBDeviceGetBus; virUSBDeviceGetDevno; diff --git a/src/util/virusb.c b/src/util/virusb.c index cf3461f80a..d193f8f3ba 100644 --- a/src/util/virusb.c +++ b/src/util/virusb.c @@ -61,12 +61,6 @@ struct _virUSBDeviceList { virUSBDevice **devs; };
-typedef enum { - USB_DEVICE_ALL = 0, - USB_DEVICE_FIND_BY_VENDOR = 1 << 0, - USB_DEVICE_FIND_BY_BUS = 1 << 1, -} virUSBDeviceFindFlags; - static virClass *virUSBDeviceListClass;
static void virUSBDeviceListDispose(void *obj); @@ -102,11 +96,27 @@ static int virUSBSysReadFile(const char *f_name, const char *d_name, return 0; }
+static int virUSBSysReadFileStr(const char *f_name, const char *d_name, + char **value) +{ + char *buf = NULL; + g_autofree char *filename = NULL; + + filename = g_strdup_printf(USB_SYSFS "/devices/%s/%s", d_name, f_name); + + if (virFileReadAll(filename, 1024, &buf) < 0) + return -1; + + *value = buf; + return 0; +} + static virUSBDeviceList * virUSBDeviceSearch(unsigned int vendor, unsigned int product, unsigned int bus, unsigned int devno, + const char *port, const char *vroot, unsigned int flags) { @@ -127,6 +137,8 @@ virUSBDeviceSearch(unsigned int vendor,
while ((direrr = virDirRead(dir, &de, USB_SYSFS "/devices")) > 0) { unsigned int found_prod, found_vend, found_bus, found_devno; + char *found_port; + bool port_matches; char *tmpstr = de->d_name;
if (strchr(de->d_name, ':')) @@ -154,16 +166,32 @@ virUSBDeviceSearch(unsigned int vendor, 10, &found_devno) < 0) goto cleanup;
- if ((flags & USB_DEVICE_FIND_BY_VENDOR) && - (found_prod != product || found_vend != vendor)) - continue; + if (virUSBSysReadFileStr("devpath", de->d_name, + &found_port) < 0) { + goto cleanup; + } else { + found_port[strlen(found_port) - 1] = '\0'; /* remove newline */ + port_matches = STREQ_NULLABLE(found_port, port); + VIR_FREE(found_port); + }
- if (flags & USB_DEVICE_FIND_BY_BUS) { + if (flags & USB_DEVICE_FIND_BY_VENDOR) { + if (found_prod != product || found_vend != vendor) + continue; + } + + if (flags & USB_DEVICE_FIND_BY_DEVICE) { if (found_bus != bus || found_devno != devno) continue; found = true; }
+ if (flags & USB_DEVICE_FIND_BY_PORT) { + if (found_bus != bus || !port_matches) + continue; + found = true; + } + usb = virUSBDeviceNew(found_bus, found_devno, vroot);
if (!usb) @@ -186,36 +214,41 @@ virUSBDeviceSearch(unsigned int vendor, }
int -virUSBDeviceFindByVendor(unsigned int vendor, - unsigned int product, - const char *vroot, - bool mandatory, - virUSBDeviceList **devices) +virUSBDeviceFind(unsigned int vendor, + unsigned int product, + unsigned int bus, + unsigned int devno, + const char *port, + const char *vroot, + bool mandatory, + unsigned int flags, + virUSBDeviceList **devices) { virUSBDeviceList *list; int count;
- if (!(list = virUSBDeviceSearch(vendor, product, 0, 0, - vroot, - USB_DEVICE_FIND_BY_VENDOR))) + if (!(list = virUSBDeviceSearch(vendor, product, bus, devno, port, + vroot, flags))) return -1;
- if (list->count == 0) { + count = list->count; + if (count == 0) { virObjectUnref(list); if (!mandatory) { - VIR_DEBUG("Did not find USB device %04x:%04x", - vendor, product); if (devices) *devices = NULL; return 0; }
virReportError(VIR_ERR_INTERNAL_ERROR, - _("Did not find USB device %1$04x:%2$04x"), vendor, product); + _("Did not find matching USB device (matching on%1$s%2$s%3$s others are ignored): vid:%4$04x, pid:%5$04x, bus:%6$u, device:%7$u, port:%8$s"), + flags & USB_DEVICE_FIND_BY_VENDOR ? " vid/pid," : "", + flags & USB_DEVICE_FIND_BY_DEVICE ? " bus/device," : "", + flags & USB_DEVICE_FIND_BY_PORT ? " bus/port," : "", + vendor, product, bus, devno, port ? port : "(null)");
This is going to be horrible for translators.
return -1; }
- count = list->count; if (devices) *devices = list; else
Michal