[PATCH v4] usb: usbfs: Require claiming a hub interface for hub class requests
From: YuanZhe Liu
Date: Thu Nov 05 2026 - 12:37:32 EST
check_ctrlrecip() requires the matching interface to be claimed for
requests addressed to an interface or an endpoint, but requests
addressed to the device or to a port are passed through without any
check. For a hub that means userspace can send hub class requests
through usbfs without claiming the hub interface, while the hub driver
owns the ports. Port requests use USB_RT_PORT, whose recipient is
USB_RECIP_OTHER, and SetPortFeature(PORT_SUSPEND) is one of them.
That is more than a policy question. The request goes to the HCD, and
it can run concurrently with userspace releasing the hub interface
through USBDEVFS_DISCONNECT_CLAIM, which unbinds the hub driver and
disconnects the devices behind it. xhci_hcd tracks the device behind
each port and issues stop endpoint commands for it in its port suspend
path, and it ended up reading a freed xhci_virt_device.
Require interface 0 to be claimed for every non-GET request to a hub.
The hub driver matches devices by device class or by interface class,
but a hub has a single interface either way, and that is the one the
driver holds.
Userspace can no longer change port settings while the hub driver is
bound. USBDEVFS_CLAIM_PORT hands over the device behind a port, not the
port itself, so a request that pokes port state is not safe for a
claimed port either. Programs that used to do this have to unbind the
hub driver first.
Signed-off-by: YuanZhe Liu <25031212351@xxxxxxxxxxxxxxxxx>
---
Hi Alan,
v4 drops the port owner exception. You are right that
USBDEVFS_CLAIM_PORT hands over the device behind the port, not the port
itself: the device is driven and suspended through its own fd, so
letting a request poke port state on behalf of a claimed port does not
buy anything. With the exception gone the helper in hub.c and the
declaration in usb.h go away too, and this is a single file patch again.
I kept the interface class test. I agree it should not matter for a
device that follows the spec, but it costs nothing and it keeps the
check in line with the two entries in hub_id_table.
Then the -EBUSY for tools that switch port power without unbinding the
hub driver is intended, as you said.
No Fixes: tag and no Cc: stable, following your recommendation.
You mentioned that usb_hub_release_port() and usb_hub_release_all_ports()
should synchronize with outstanding URBs sent to the device on the port:
kill the current ones and keep new ones from being submitted. I would
like to write that as a separate patch. Should I send it now, on top of
this one, or wait until this one is settled?
YuanZhe
drivers/usb/core/devio.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/drivers/usb/core/devio.c b/drivers/usb/core/devio.c
index 486ced74405e..d39ef9a80ec2 100644
--- a/drivers/usb/core/devio.c
+++ b/drivers/usb/core/devio.c
@@ -878,6 +878,23 @@ static int findintfep(struct usb_device *dev, unsigned int ep)
return -ENOENT;
}
+/*
+ * Return true if @dev is a hub. The hub driver matches devices by device
+ * class or by interface class, and a hub has a single interface either way.
+ */
+static bool is_hub(struct usb_device *dev)
+{
+ struct usb_host_config *config = dev->actconfig;
+
+ if (dev->descriptor.bDeviceClass == USB_CLASS_HUB)
+ return true;
+ if (!config || config->desc.bNumInterfaces < 1)
+ return false;
+ return config->interface[0] && config->interface[0]->cur_altsetting &&
+ config->interface[0]->cur_altsetting->desc.bInterfaceClass ==
+ USB_CLASS_HUB;
+}
+
static int check_ctrlrecip(struct usb_dev_state *ps, unsigned int requesttype,
unsigned int request, unsigned int index)
{
@@ -891,6 +908,18 @@ static int check_ctrlrecip(struct usb_dev_state *ps, unsigned int requesttype,
if (USB_TYPE_VENDOR == (USB_TYPE_MASK & requesttype))
return 0;
+ /*
+ * A hub driver owns the ports and the devices behind them. Letting
+ * userspace change hub or port state behind its back, for example
+ * with a SET_FEATURE(PORT_SUSPEND) request while the hub interface is
+ * being released, is not safe: the request is passed down to the HCD,
+ * which may walk per-device state that is being torn down at the same
+ * time. Port requests use USB_RECIP_OTHER and hub-wide ones use
+ * USB_RECIP_DEVICE; the hub driver owns both.
+ */
+ if ((requesttype & USB_DIR_IN) == 0 && is_hub(ps->dev))
+ return checkintf(ps, 0);
+
/*
* check for the special corner case 'get_device_id' in the printer
* class specification, which we always want to allow as it is used
--
2.34.1