From: sashiko-bot@kernel.org
To: "YuanZhe Liu" <25031212351@stu.xidian.edu.cn>
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH v3] usb: usbfs: Require claiming a hub interface for hub class requests
Date: Thu, 05 Nov 2026 07:14:40 +0000 [thread overview]
Message-ID: <sashiko-outbox-198647@kernel.org> (raw)
In-Reply-To: <20261105065627.5626-1-25031212351@stu.xidian.edu.cn>
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] usb: usbfs: check_ctrlrecip() port ownership check allows hub unbind race
- [High] usb: usbfs: check_ctrlrecip() hub check bypasses interface and ep checks
- [High] usb: usbfs: unserialized hub control URB and port_owner bypass in check_ctrlrecip()
- [Medium] usb: usbfs: hub_interface_number() returns array index instead of ifnum
--
commit 1f77616af0c7f5d12550f1a327a5a04f16360e0d
Author: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
usb: usbfs: Require claiming a hub interface for hub class requests
This commit updates check_ctrlrecip() in usbfs to require the hub interface
to be claimed before sending non-GET control requests to a hub, while
allowing USB_RECIP_OTHER requests to ports claimed by the caller through
USBDEVFS_CLAIM_PORT.
> diff --git a/drivers/usb/core/devio.c b/drivers/usb/core/devio.c
> index 101cb94254809..81085bfed4c10 100644
> --- a/drivers/usb/core/devio.c
> +++ b/drivers/usb/core/devio.c
> @@ -878,10 +878,36 @@ static int findintfep(struct usb_device *dev, unsigned int ep)
> return -ENOENT;
> }
>
> +/*
> + * Return the interface number through which the hub driver drives this
> + * device, or -1 if it is not a hub. Hubs match the driver by device class
> + * or by interface class, and a hub embedded in a composite device uses the
> + * latter.
> + */
> +static int hub_interface_number(struct usb_device *dev)
> +{
> + struct usb_host_config *config = dev->actconfig;
> + int i;
> +
> + if (dev->descriptor.bDeviceClass == USB_CLASS_HUB)
> + return 0;
> + if (!config)
> + return -1;
> + for (i = 0; i < config->desc.bNumInterfaces; i++) {
> + struct usb_interface *intf = config->interface[i];
> +
> + if (intf && intf->cur_altsetting &&
> + intf->cur_altsetting->desc.bInterfaceClass == USB_CLASS_HUB)
> + return i;
> + }
> + return -1;
> +}
[Severity: Medium]
Should hub_interface_number() return
intf->cur_altsetting->desc.bInterfaceNumber instead of the loop index i?
In checkintf() and claimintf(), the ifnum parameter is a USB interface
descriptor's bInterfaceNumber: ps->ifclaimed is indexed by bInterfaceNumber,
and claimintf() resolves the interface via usb_ifnum_to_if(dev, ifnum):
drivers/usb/core/usb.c:usb_ifnum_to_if() {
...
for (i = 0; i < config->desc.bNumInterfaces; i++)
if (config->interface[i]->altsetting[0]
.desc.bInterfaceNumber == ifnum)
return config->interface[i];
...
}
Because config->interface[] is stored in descriptor parse order rather than
indexed by bInterfaceNumber (which is why findintfep() right above returns
alts->desc.bInterfaceNumber), returning i or a hardcoded 0 when
bInterfaceNumber differs from the array index causes checkintf(ps, hub_ifnum)
to test the wrong bit in ps->ifclaimed and call claimintf(ps, i) on an
unrelated interface or fail with -ENOENT.
Also, if dev->descriptor.bDeviceClass == USB_CLASS_HUB while dev->actconfig
is NULL (for example in USB_STATE_ADDRESS), returning 0 before checking
!config causes checkintf() to reject the transfer with -EHOSTUNREACH.
> +
> static int check_ctrlrecip(struct usb_dev_state *ps, unsigned int requesttype,
> unsigned int request, unsigned int index)
> {
> int ret = 0;
> + int hub_ifnum;
> struct usb_host_interface *alt_setting;
>
> if (ps->dev->state != USB_STATE_UNAUTHENTICATED
> @@ -891,6 +917,23 @@ 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, hub-wide ones use
> + * USB_RECIP_DEVICE. A port claimed with USBDEVFS_CLAIM_PORT belongs to
> + * the caller though, and the hub driver leaves devices on such a port
> + * alone, so requests for it are allowed.
> + */
> + hub_ifnum = hub_interface_number(ps->dev);
> + if (hub_ifnum >= 0 && (requesttype & USB_DIR_IN) == 0 &&
> + !((requesttype & USB_RECIP_MASK) == USB_RECIP_OTHER &&
> + usb_hub_port_is_owned(ps->dev, index & 0xff, ps)))
> + return checkintf(ps, hub_ifnum);
[Severity: High]
Does returning checkintf(ps, hub_ifnum) early here bypass the recipient
checks for USB_RECIP_INTERFACE and USB_RECIP_ENDPOINT below?
When hub_ifnum >= 0 and (requesttype & USB_DIR_IN) == 0, check_ctrlrecip()
immediately returns checkintf(ps, hub_ifnum) for all recipients instead of
restricting the hub check to USB_RECIP_DEVICE and USB_RECIP_OTHER (or falling
through to the switch statement):
drivers/usb/core/devio.c:check_ctrlrecip() {
...
index &= 0xff;
switch (requesttype & USB_RECIP_MASK) {
case USB_RECIP_ENDPOINT:
if ((index & ~USB_DIR_IN) == 0)
return 0;
ret = findintfep(ps->dev, index);
...
if (ret >= 0)
ret = checkintf(ps, ret);
break;
case USB_RECIP_INTERFACE:
ret = checkintf(ps, index);
break;
}
return ret;
}
Note also that hub_probe() in drivers/usb/core/hub.c rejects devices with
hdev->actconfig->desc.bNumInterfaces > 1. On a multi-interface composite
device containing a USB_CLASS_HUB interface alongside other interfaces:
1. A caller that claims only the (unbound) hub interface gets 0 from
checkintf(ps, hub_ifnum) and bypasses checkintf() for all other interfaces
and endpoints on the device, allowing OUT control requests to interfaces
bound to kernel drivers or disallowed by ps->interface_allowed_mask.
2. Conversely, a caller that claimed a non-hub interface cannot send
non-vendor USB_RECIP_INTERFACE or USB_RECIP_ENDPOINT OUT requests to its own
claimed interface or endpoint without also claiming hub_ifnum.
3. Even on a single-interface hub where hub_ifnum is claimed, OUT requests
with USB_RECIP_INTERFACE or USB_RECIP_ENDPOINT specifying a non-existent
interface or invalid endpoint in wIndex return 0 instead of failing with
-ENOENT or -EINVAL in checkintf(ps, index) or findintfep(ps->dev, index).
[Severity: High]
As reported by Sashiko on v2 [1], can a SetPortFeature(PORT_SUSPEND) control
request still race with USBDEVFS_DISCONNECT_CLAIM and trigger the
use-after-free of xhci_virt_device in xhci_stop_device()?
There appear to be two ways this race can still happen:
First, through the usb_hub_port_is_owned() exemption in check_ctrlrecip().
The comment and commit message state that the hub driver does not connect
devices to a port claimed with USBDEVFS_CLAIM_PORT, but in
drivers/usb/core/hub.c, usb_hub_claim_port() only works while the hub driver
is bound and does not check or disconnect an existing port_dev->child:
drivers/usb/core/hub.c:usb_hub_claim_port() {
int rc;
struct usb_dev_state **powner;
rc = find_port_owner(hdev, port1, &powner);
if (rc)
return rc;
if (*powner)
return -EBUSY;
*powner = owner;
return rc;
}
Furthermore, if a device is connected to a claimed port later,
hub_port_connect() still allocates udev, calls hub_port_init() ->
xhci_alloc_dev() (which allocates xhci->devs[slot_id] and sets
port->slot_id), sets port_dev->child = udev, and calls usb_new_device(udev);
port_owner only causes usb_device_is_owned() to skip configuration selection
and interface driver probing on the child device. When the caller sends
SetPortFeature(PORT_SUSPEND) on that claimed port via do_proc_control() or
proc_do_submiturb(), check_ctrlrecip() sees usb_hub_port_is_owned() == true
and skips checkintf(ps, hub_ifnum). Once do_proc_control() drops
usb_lock_device(dev) before usbfs_start_wait_urb(), a concurrent
USBDEVFS_DISCONNECT_CLAIM on the hub interface unbinds the hub driver via
hub_disconnect() -> hub_quiesce():
drivers/usb/core/hub.c:hub_quiesce() {
...
if (type != HUB_SUSPEND) {
/* Disconnect all the children */
for (i = 0; i < hdev->maxchild; ++i) {
if (hub->ports[i]->child)
usb_disconnect(&hub->ports[i]->child);
}
}
...
}
which disconnects hub->ports[i]->child regardless of port_owner and frees
xhci_virt_device in xhci_free_dev() while xhci_hub_control() ->
xhci_stop_device() is running.
Second, even when checkintf(ps, hub_ifnum) is reached, do_proc_control()
verifies the claim under usb_lock_device(dev) and then drops the device lock
before submitting the URB:
drivers/usb/core/devio.c:do_proc_control() {
...
ret = check_ctrlrecip(ps, ctrl.bRequestType, ctrl.bRequest,
ctrl.wIndex);
...
usb_unlock_device(dev);
i = usbfs_start_wait_urb(urb, tmo, &actlen);
...
}
Similarly, in proc_do_submiturb(), endpoint 0 control URBs leave ifnum as -1
(stored in as->ifnum), so proc_releaseinterface() ->
destroy_async_on_interface() does not cancel in-flight control URBs when the
interface is released. After the device lock is dropped (and while
xhci_hub_control() sleeps in msleep(10)), can another thread release the hub
interface via USBDEVFS_RELEASEINTERFACE, rebind hub_driver via
USBDEVFS_CONNECT so a port is enumerated, and then unbind hub_driver via
USBDEVFS_DISCONNECT_CLAIM while the control URB is still executing?
[1] https://lore.kernel.org/r/20261105054333.7811-1-25031212351@stu.xidian.edu.cn
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105065627.5626-1-25031212351@stu.xidian.edu.cn?part=1
next prev parent reply other threads:[~2026-11-05 7:14 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20261105054333.7811-25031212351@stu.xidian.edu.cn>
2026-11-05 6:56 ` [PATCH v3] usb: usbfs: Require claiming a hub interface for hub class requests YuanZhe Liu
2026-11-05 7:14 ` sashiko-bot [this message]
2026-11-05 17:11 ` Alan Stern
2026-11-05 17:23 ` [PATCH v4] " YuanZhe Liu
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-198647@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=25031212351@stu.xidian.edu.cn \
--cc=linux-usb@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox