[PATCH net-next v18 3/6] driver core: add device_schedule_reprobe()

From: Daniel Golle

Date: Sat Sep 26 2026 - 09:52:02 EST


Drivers that need a deferred re-probe of their own device open-code a
work item in module text. iwlwifi (iwl_trans_schedule_reprobe(), for a
firmware crash a lighter restart cannot fix) and hci_h5
(h5_btrtl_resume(), RTL devices lose their firmware state over suspend)
both end that work function with put_device(); kfree();
module_put(THIS_MODULE);, where a concurrent rmmod can free the module
text the epilogue is still executing. Neither gates the re-probe on the
device still being bound to the driver that scheduled it, which a driver
cannot do from outside because device_reprobe() takes the device lock
internally. Their conversion is left to follow-up patches.

Add device_schedule_reprobe(), which detaches and re-probes a device
after a caller-specified delay. The work function is built-in text, so a
caller needs no module reference. The binding is recorded as the driver
pointer plus a copy of its name: the stored pointer is only ever
compared, never dereferenced, and the name copy rejects a freed struct
device_driver address the allocator later hands to a different driver.
The first user is the mxl862xx devlink flash path added later in this
series.

A failed re-probe leaves the device unbound, as a failed initial probe
would. device_attach() reports that as 0, a failed probe being folded
into "no driver matched", and 0 carries no errno for dev_err_probe(),
so the message is a plain dev_err() that skips only -EPROBE_DEFER, which
the deferred-probe machinery retries on its own.

Nothing is locked in the caller's context, and its checks are unlocked
snapshots the work repeats under the device lock, so the helper may be
called with the device lock held, as the PM callbacks, ->remove() and
->shutdown() hold it. The caller is the bound driver, in a context its
->remove() waits for, which keeps the driver name readable for the
copy. Buses that take the parent lock to bind are
refused with -EINVAL: that lock has to be taken before @dev's own, so
the parent would have to be recorded before either is held, where
device_move() can replace it without taking any device lock.
usb_bus_type is the only such bus and no caller needs it today.

While probing is blocked, which device_shutdown() and dpm_prepare() both
set before they touch any device, the work re-arms itself, so a request
pending across a system suspend runs once the system has resumed and
one that fires during a shutdown detaches nothing. Beyond that gate
this is device_reprobe() deferred and __device_release_driver() is
unchanged, so it carries device_reprobe()'s pre-existing limitations:
the detach and the re-attach are not one locked operation, so an
administrative unbind between them may be undone, and detaching a device
that has managed consumers unbinds them as any release does, so a
re-probe a concurrent device_shutdown() overtakes may run ->remove() in
place of ->shutdown(). None of this is specific to the helper.

Assisted-by: LLM
Signed-off-by: Daniel Golle <daniel@xxxxxxxxxxxxxx>
---
v18:
- re-arm the work while probing is blocked instead of dropping the
request: dpm_prepare() blocks probing as well, and a kexec jump or a
kernel without the suspend freezer reaches that window with the
workqueue running, which lost the request for good (found by Sashiko
AI review)
- log an error for every unbound outcome of device_attach() except a
deferred probe: a failed probe comes back as 0, which the check for a
negative value missed (found by Sashiko AI review)
- kernel-doc: the caller is the bound driver in a context its ->remove()
waits for, and a rebind of the same driver within the delay still
gets the re-probe (found by Sashiko AI review)
- commit message: the caller's checks are unlocked snapshots, only the
stored driver pointer is never dereferenced, and the drivers named as
motivation are converted later (found by Sashiko AI review)
v17:
- drop the abort_if_blocked flag and the bool return of
__device_release_driver(), leaving that function unchanged: the flag
left the device-links state half torn down when it fired and did not
cover the consumers unbound in the same window, so the shutdown-vs-
release window it targeted is documented as pre-existing to every
unbind path instead (found by Sashiko AI review)
- record the bound driver's name beside the pointer and compare both,
so a freed struct device_driver address reused by another driver is
not mistaken for the original binding (found by Sashiko AI review)
- kernel-doc: add a Context line and state the pre-existing limitations
shared with device_reprobe() (found by Sashiko AI review)
v16:
- commit message: device_shutdown() blocks probing only once
wait_for_device_probe() has returned, so a re-probe already past the
test detaches the device instead of leaving it bound for its
->shutdown()
- take no lock in the caller's context and drop the parent snapshot,
refusing buses that need the parent lock instead: the caller-context
device lock inverted against the devlink instance lock on the flash
path and against a synchronous work cancel on the rescue path, and a
pinned parent can be freed by device_move() (found by Sashiko AI
review)
- abandon the release when probing is blocked while the device links
loop has the locks dropped, rather than calling that window
pre-existing: a deferred re-probe is the one unbind that may be
abandoned, so it is the one that can close it (found by Sashiko AI
review)
- commit message: describe what this patch changes rather than bugs in
drivers it does not convert, and name the first user (found by
Sashiko AI review)
- kernel-doc: drop the promise that an administrative unbind always
wins, which unbind_store() does not guarantee (found by Sashiko AI
review)
v15:
- skip the detach while probing is blocked instead of adding a
per-device shutdown_done flag: device_shutdown() blocks probing
before its walk starts, so the flag left a window where the work
detached a device that then neither re-attached nor got its
->shutdown() call (found by Sashiko AI review)
- validate the device and snapshot the parent, its locking requirement
and the bound driver under the device lock, so an unregister racing
the allocation can neither leave a freed parent pinned nor pair a
NULL parent with a request to lock it (found by Sashiko AI review)
- keep -EPROBE_DEFER out of the re-probe error path, where
dev_err_probe() would record the message as the device's deferred
probe reason (found by Sashiko AI review)
- kernel-doc: a stale re-probe leaves an unbound device unbound, which
an unbind followed by a rebind within the delay does not (found by
Sashiko AI review)
v14: no changes
v13:
- queue the work on system_freezable_wq, so a re-probe pending across
system suspend can neither detach a device the PM core has suspended
nor race its late suspend callbacks; it runs after resume instead
(found by Sashiko AI review)
- record at scheduling time whether the parent needs locking, instead
of reading dev->bus in the work, which may be gone with its module
once the device has been unregistered (found by Sashiko AI review)
- let __device_release_driver() report whether it released the driver,
so an administrative unbind that wins the race inside the device
links loop is not undone by the re-attach (found by Sashiko AI
review)
- use dev_err_probe() for the re-probe error path, so a re-probe
deferred at resume no longer logs a spurious error (Hans de Goede,
on the standalone posting of this helper)
- describe the parent pinning and locking in the commit message, as in
the standalone posting

v12:
- pin the parent device across the deferred work; a reference on the
child alone left device_reprobe_work_fn() dereferencing a freed
dev->parent under __device_driver_lock() when the device was
unregistered before the work ran (found by Sashiko AI review)
- take the parent lock across device_attach() on buses that require
it, matching bus_rescan_devices_helper() (found by Sashiko AI review)

v11: new patch: add device_schedule_reprobe() to the driver core (posted
earlier as an RFC) so mxl862xx can schedule its post-flash and
post-drain re-probe through the core instead of open-coding a work
item

drivers/base/dd.c | 126 +++++++++++++++++++++++++++++++++++++++++
include/linux/device.h | 2 +
2 files changed, 128 insertions(+)

diff --git a/drivers/base/dd.c b/drivers/base/dd.c
index f6525a7ee8c5..823a25f9c089 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1436,3 +1436,129 @@ void driver_detach(const struct device_driver *drv)
put_device(dev);
}
}
+
+struct device_reprobe {
+ struct delayed_work work;
+ const struct device_driver *drv;
+ const char *drv_name;
+ struct device *dev;
+ unsigned long delay;
+};
+
+static void device_reprobe_work_fn(struct work_struct *work)
+{
+ struct device_reprobe *rp = container_of(work, struct device_reprobe,
+ work.work);
+ struct device *dev = rp->dev;
+ bool detached = false;
+ int ret;
+
+ device_lock(dev);
+ /*
+ * rp->drv is only compared, never dereferenced: the driver it points
+ * to may have been unregistered and freed. The saved name rejects a
+ * freed address the allocator has since handed to another driver.
+ */
+ if (!dev->p->dead && dev->driver == rp->drv &&
+ !strcmp(dev->driver->name, rp->drv_name)) {
+ if (defer_all_probes) {
+ device_unlock(dev);
+ queue_delayed_work(system_freezable_wq, &rp->work,
+ rp->delay);
+ return;
+ }
+ __device_release_driver(dev, NULL);
+ detached = true;
+ }
+ device_unlock(dev);
+
+ if (detached) {
+ ret = device_attach(dev);
+ /* 0 is unbound too: a failed probe is folded into "no match" */
+ if (ret <= 0 && ret != -EPROBE_DEFER)
+ dev_err(dev, "re-probe left the device unbound\n");
+ }
+
+ put_device(dev);
+ kfree(rp->drv_name);
+ kfree(rp);
+}
+
+/**
+ * device_schedule_reprobe - schedule a deferred detach and re-probe
+ * @dev: device to detach and re-probe
+ * @delay_ms: delay in milliseconds before the re-probe runs
+ *
+ * Schedule a detach and re-probe of @dev after @delay_ms milliseconds,
+ * from built-in driver-core work rather than a driver-owned work item,
+ * so the bound driver may call it without pinning its own module. The
+ * binding is recorded as the driver pointer plus a copy of its name; the
+ * pointer is only ever compared, never dereferenced, and the name copy
+ * guards against a freed &struct device_driver address the allocator
+ * later hands to a different driver.
+ *
+ * The re-probe is skipped when the work runs if @dev has since been
+ * removed, is no longer bound or is bound to a different driver; an
+ * unbind followed by a rebind of the same driver within the delay still
+ * gets it. While probing is blocked, for a system suspend or shutdown,
+ * the work re-arms itself after @delay_ms, so a request pending across
+ * suspend runs once the system has resumed and one that fires during a
+ * shutdown detaches nothing. A failed re-probe leaves @dev unbound, as a
+ * failed initial probe would.
+ *
+ * This is device_reprobe() deferred, and shares its limitations;
+ * __device_release_driver() is unchanged. The detach and the re-attach
+ * are not one locked operation, so an administrative unbind arriving
+ * between them may be undone, and the attach half runs the normal probe
+ * path with no shutdown re-check of its own. If @dev has managed
+ * consumers, detaching it unbinds them as any driver release does, so a
+ * re-probe a concurrent device_shutdown() overtakes may run ->remove()
+ * in place of ->shutdown(). None of this is specific to this helper.
+ *
+ * Buses that take the parent lock to bind (only usb_bus_type) are refused
+ * with -EINVAL: the parent would have to be recorded before either lock
+ * is held, where device_move() can replace it.
+ *
+ * Context: May sleep (allocates with %GFP_KERNEL). Must be called by the
+ * driver bound to @dev, from a process context its ->remove() waits for,
+ * so that the binding outlives the call; @dev's own device lock may be
+ * held, but not from ->probe(), which the scheduled work would detach.
+ *
+ * Returns: 0 on success, -EINVAL if @dev is not a registered device
+ * bound to a driver or sits on a bus which takes the parent lock to
+ * bind, -ENOMEM on allocation failure.
+ */
+int device_schedule_reprobe(struct device *dev, unsigned int delay_ms)
+{
+ const struct device_driver *drv;
+ struct device_reprobe *rp;
+
+ drv = READ_ONCE(dev->driver);
+ /*
+ * A bus taking the parent lock would need @dev's parent pinned until
+ * the work runs, which device_move() can invalidate.
+ */
+ if (!drv || !dev->bus || dev->bus->need_parent_lock || !dev->p ||
+ dev->p->dead || !device_is_registered(dev))
+ return -EINVAL;
+
+ rp = kzalloc_obj(*rp);
+ if (!rp)
+ return -ENOMEM;
+
+ rp->drv_name = kstrdup(drv->name, GFP_KERNEL);
+ if (!rp->drv_name) {
+ kfree(rp);
+ return -ENOMEM;
+ }
+
+ rp->dev = get_device(dev);
+ rp->drv = drv;
+ rp->delay = msecs_to_jiffies(delay_ms);
+
+ INIT_DELAYED_WORK(&rp->work, device_reprobe_work_fn);
+ queue_delayed_work(system_freezable_wq, &rp->work, rp->delay);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(device_schedule_reprobe);
diff --git a/include/linux/device.h b/include/linux/device.h
index aee79fd6b32b..7a9916950577 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -1314,6 +1314,8 @@ int __must_check device_attach(struct device *dev);
int __must_check driver_attach(const struct device_driver *drv);
void device_initial_probe(struct device *dev);
int __must_check device_reprobe(struct device *dev);
+int __must_check device_schedule_reprobe(struct device *dev,
+ unsigned int delay_ms);

bool device_is_bound(struct device *dev);

--
2.55.0