* [PATCH v3 1/2] usb: core: clean up before handing a device to the generic driver
2026-11-03 3:34 [PATCH v3 0/2] usb: core: fix failed device driver probes, add pre_probe James Clark
@ 2026-11-03 3:34 ` James Clark
2026-11-03 3:34 ` [PATCH v3 2/2] usb: core: add a pre_probe callback for device drivers that may defer James Clark
1 sibling, 0 replies; 4+ messages in thread
From: James Clark @ 2026-11-03 3:34 UTC (permalink / raw)
To: Greg Kroah-Hartman, Alan Stern
Cc: Johan Hovold, Bastien Nocera, Matthias Kaehlcke, Arnd Bergmann,
Bartosz Golaszewski, Douglas Anderson, Robert Nelson, linux-usb,
linux-kernel, linux-gpio, regressions
When a USB device driver's probe returns -ENODEV, usb_probe_device()
hands the device to the generic driver. It does not first undo what it
did before calling the probe. This causes two problems.
First, if the driver sets generic_subclass, the generic probe has
already run. It configured the device and sent a USB_DEVICE_ADD
notification. The generic driver then does both again. Notifier users
such as the usbport LED trigger see two USB_DEVICE_ADDs with no
USB_DEVICE_REMOVE in between.
Second, if the driver does not set supports_autosuspend, a runtime PM
reference was taken before the probe. Nothing drops it, so the device
can never be runtime suspended.
Clean up before handing the device over, in the same way as
usb_unbind_device() does when a driver is unbound: first, for a driver
that sets generic_subclass, call usb_generic_driver_disconnect();
second, for a driver that does not set supports_autosuspend, drop the
runtime PM reference with usb_autosuspend_device().
This leaves one limitation. Until the generic driver binds, the device
has no driver. If it is runtime suspended in that time, it can't be
resumed. This is transient: the generic driver resumes the device when
it binds.
This change does not alter what happens when the probe fails with any
other error. The device stays configured, so its interfaces keep
working. It keeps the runtime PM reference, because a device without a
driver can't be resumed. Its USB_DEVICE_ADD stays unpaired, because
sending USB_DEVICE_REMOVE would mean unconfiguring the device.
Fixes: 88b7381a939d ("USB: Select better matching USB drivers when available")
Signed-off-by: James Clark <jjc@jclark.com>
---
drivers/usb/core/driver.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c
index d3a61ebc6afb..9b8cd0e4219e 100644
--- a/drivers/usb/core/driver.c
+++ b/drivers/usb/core/driver.c
@@ -295,6 +295,10 @@ static int usb_probe_device(struct device *dev)
error = -EINVAL;
if (error == -ENODEV && udriver != &usb_generic_driver &&
(udriver->id_table || udriver->match)) {
+ if (udriver->generic_subclass)
+ usb_generic_driver_disconnect(udev);
+ if (!udriver->supports_autosuspend)
+ usb_autosuspend_device(udev);
udev->use_generic_driver = 1;
return -EPROBE_DEFER;
}
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH v3 2/2] usb: core: add a pre_probe callback for device drivers that may defer
2026-11-03 3:34 [PATCH v3 0/2] usb: core: fix failed device driver probes, add pre_probe James Clark
2026-11-03 3:34 ` [PATCH v3 1/2] usb: core: clean up before handing a device to the generic driver James Clark
@ 2026-11-03 3:34 ` James Clark
[not found] ` <sashiko-outbox-194542@kernel.org>
1 sibling, 1 reply; 4+ messages in thread
From: James Clark @ 2026-11-03 3:34 UTC (permalink / raw)
To: Greg Kroah-Hartman, Alan Stern
Cc: Johan Hovold, Bastien Nocera, Matthias Kaehlcke, Arnd Bergmann,
Bartosz Golaszewski, Douglas Anderson, Robert Nelson, linux-usb,
linux-kernel, linux-gpio, regressions
On a BeagleY-AI with BeagleBoard's device tree, kernels since commit
f918e3804498 ("arm64: enable HAVE_SHARED_GPIOS for all ARM64 platforms")
no longer boot to a usable system. The log repeats "hub 1-1:1.0: USB hub
found" for the on-board TUSB8041 hub every few milliseconds, with no USB
disconnect in between. The flood of uevents stalls udev and the boot ends
in emergency mode. Reverting that commit makes the board boot normally
again.
Both peer hub nodes in this device tree carry the same reset-gpios, so
the GPIO is now a shared one, and with CONFIG_GPIO_SHARED_PROXY=m (the
default) the hub's platform probe is legitimately deferred until udev has
loaded that module. The bootloader leaves the hub running, so the USB
core enumerates it long before, and the USB device probe finds the
platform device without driver data.
At that point, onboard_dev_usbdev_probe() returns -EPROBE_DEFER. This is
what causes the problem. The driver has generic_subclass set, so
usb_probe_device() has already run the generic probe, which configured
the device and bound the interface drivers. Each binding is a
deferred-probe trigger, and the driver core re-triggers when a trigger
happened during a probe that then deferred. So the device is re-probed
straight away, reconfigured, its interfaces bound again, and so on for
as long as the platform device remains unbound. A probe must not defer
after it has registered child devices, and with generic_subclass the
driver's probe has no way to defer before that. This is not specific to
shared GPIOs: it happens whenever the platform probe is deferred, and
briefly whenever onboard_usb_dev is a module.
Add a pre_probe callback to struct usb_device_driver, called before the
generic probe, so that a driver that may have to defer can do so before
the device is configured. -EPROBE_DEFER from it ends the probe with
nothing done. Any other error is handled as if probe() had returned it,
once the generic probe has run, so the -ENODEV fallback to the generic
driver works as before. Make onboard_usb_dev's USB device probe a
pre_probe; it does not need the device to be configured.
A device whose driver defers from pre_probe stays unconfigured until the
probe succeeds, and for good if it never does, e.g. an onboard hub whose
platform driver fails to probe. Before, such a device worked while being
re-probed. On the BeagleY-AI the hub is unconfigured for the few seconds
until gpio-shared-proxy has loaded and the platform driver has probed,
which resets it anyway; the board then boots normally with that commit
in place.
Fixes: 8bc063641ceb ("usb: misc: Add onboard_usb_hub driver")
Suggested-by: Johan Hovold <johan@kernel.org>
Signed-off-by: James Clark <jjc@jclark.com>
---
drivers/usb/core/driver.c | 38 ++++++++++++++++++++++++++----
drivers/usb/misc/onboard_usb_dev.c | 15 ++++++------
include/linux/usb.h | 14 +++++++++++
3 files changed, 55 insertions(+), 12 deletions(-)
diff --git a/drivers/usb/core/driver.c b/drivers/usb/core/driver.c
index 9b8cd0e4219e..137b53beb236 100644
--- a/drivers/usb/core/driver.c
+++ b/drivers/usb/core/driver.c
@@ -249,24 +249,41 @@ static int usb_probe_device(struct device *dev)
{
struct usb_device_driver *udriver = to_usb_device_driver(dev->driver);
struct usb_device *udev = to_usb_device(dev);
- int error = 0;
+ int error = 0, pre_error = 0;
dev_dbg(dev, "%s\n", __func__);
/* TODO: Add real matching code */
+ /*
+ * Give the driver a chance to defer before anything is done to the
+ * device. The generic probe below binds the interface drivers, and
+ * each binding triggers deferred probing, so a probe that deferred
+ * after it would be retried straight away and the device re-probed
+ * in a loop.
+ *
+ * Any other error from pre_probe() is not acted on until the generic
+ * probe has run, so that the -ENODEV fallback below works as it does
+ * for probe().
+ */
+ if (udriver->pre_probe) {
+ pre_error = udriver->pre_probe(udev);
+ if (pre_error == -EPROBE_DEFER)
+ return pre_error;
+ }
+
/* The device should always appear to be in use
* unless the driver supports autosuspend.
*/
if (!udriver->supports_autosuspend)
error = usb_autoresume_device(udev);
if (error)
- return error;
+ goto err_pre_probe;
if (udriver->generic_subclass)
error = usb_generic_driver_probe(udev);
if (error)
- return error;
+ goto err_pre_probe;
/* Probe the USB device with the driver in hand, but only
* defer to a generic driver in case the current USB
@@ -289,7 +306,9 @@ static int usb_probe_device(struct device *dev)
* specialised device drivers prior to setting the
* use_generic_driver bit.
*/
- if (udriver->probe)
+ if (pre_error)
+ error = pre_error;
+ else if (udriver->probe)
error = udriver->probe(udev);
else if (!udriver->generic_subclass)
error = -EINVAL;
@@ -300,8 +319,17 @@ static int usb_probe_device(struct device *dev)
if (!udriver->supports_autosuspend)
usb_autosuspend_device(udev);
udev->use_generic_driver = 1;
- return -EPROBE_DEFER;
+ error = -EPROBE_DEFER;
}
+ if (error)
+ goto err_pre_probe;
+
+ return 0;
+
+err_pre_probe:
+ /* The driver core doesn't call disconnect() for a failed probe. */
+ if (udriver->pre_probe && !pre_error && udriver->disconnect)
+ udriver->disconnect(udev);
return error;
}
diff --git a/drivers/usb/misc/onboard_usb_dev.c b/drivers/usb/misc/onboard_usb_dev.c
index c60406240416..33653d8f76d7 100644
--- a/drivers/usb/misc/onboard_usb_dev.c
+++ b/drivers/usb/misc/onboard_usb_dev.c
@@ -606,13 +606,14 @@ static struct onboard_dev *_find_onboard_dev(struct device *dev)
/*
* The presence of drvdata indicates that the platform driver finished
- * probing. This handles the case where (conceivably) we could be
- * running at the exact same time as the platform driver's probe. If
- * we detect the race we request probe deferral and we'll come back and
- * try again.
+ * probing. Without it, e.g. because the platform driver's probe was
+ * deferred or is running right now, request probe deferral and we'll
+ * come back and try again. This is allowed because we run from
+ * pre_probe(), before the USB core has configured the device.
*/
if (!onboard_dev)
- return ERR_PTR(-EPROBE_DEFER);
+ return ERR_PTR(dev_err_probe(dev, -EPROBE_DEFER,
+ "platform device not bound\n"));
return onboard_dev;
}
@@ -623,7 +624,7 @@ static bool onboard_dev_usbdev_match(struct usb_device *udev)
return !!udev->dev.of_node;
}
-static int onboard_dev_usbdev_probe(struct usb_device *udev)
+static int onboard_dev_usbdev_pre_probe(struct usb_device *udev)
{
struct device *dev = &udev->dev;
struct onboard_dev *onboard_dev;
@@ -695,7 +696,7 @@ MODULE_DEVICE_TABLE(usb, onboard_dev_id_table);
static struct usb_device_driver onboard_dev_usbdev_driver = {
.name = "onboard-usb-dev",
.match = onboard_dev_usbdev_match,
- .probe = onboard_dev_usbdev_probe,
+ .pre_probe = onboard_dev_usbdev_pre_probe,
.disconnect = onboard_dev_usbdev_disconnect,
.generic_subclass = 1,
.supports_autosuspend = 1,
diff --git a/include/linux/usb.h b/include/linux/usb.h
index 825f03b29067..6e07d485d49a 100644
--- a/include/linux/usb.h
+++ b/include/linux/usb.h
@@ -1280,6 +1280,19 @@ struct usb_driver {
* @name: The driver name should be unique among USB drivers,
* and should normally be the same as the module name.
* @match: If set, used for better device/driver matching.
+ * @pre_probe: If set, called first, before the device is resumed or
+ * configured (the generic USB driver's probe runs after it when
+ * @generic_subclass is set, then @probe), so it must not access the
+ * device. A driver that may have to defer probing must do so from here
+ * by returning -EPROBE_DEFER, not from @probe: by then the generic probe
+ * has bound the interface drivers, and each binding triggers deferred
+ * probing, so the device would be re-probed in a loop. Returning
+ * -EPROBE_DEFER ends the probe with the device left as it was, so the
+ * device stays unconfigured until the probe succeeds. Any other error is
+ * handled as if @probe had returned it, once the generic probe has run,
+ * and @probe is then not called. If @pre_probe succeeds and a later step
+ * of the probe fails, @disconnect is called to undo it, so a driver with
+ * both callbacks must make @disconnect cope with a failed @probe.
* @probe: Called to see if the driver is willing to manage a particular
* device. If it is, probe returns zero and uses dev_set_drvdata()
* to associate driver-specific data with the device. If unwilling
@@ -1309,6 +1322,7 @@ struct usb_device_driver {
const char *name;
bool (*match) (struct usb_device *udev);
+ int (*pre_probe) (struct usb_device *udev);
int (*probe) (struct usb_device *udev);
void (*disconnect) (struct usb_device *udev);
--
2.56.0
^ permalink raw reply [flat|nested] 4+ messages in thread