Thread (1 message) 1 message, 1 author, 8d ago

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

From: netdev-bot+sashiko@kernel.org
Date: 2026-09-17 16:11:28
Also in: driver-core, linux-doc, lkml

Thank you for your contribution! Sashiko AI review found 8 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 3 · Low: 4

- [High] The new `abort_if_blocked` protection stops at @dev.
- [Medium] `__device_release_driver()` (drivers/base/dd.c:1341-1345) can
  now abandon a release *inside* the `while (device_links_busy(dev))`…
- [Medium] Only the detach half of the re-probe is gated on
  `defer_all_probes`; the attach half is not.
- [Medium] The detach and the re-attach are not one serialized operation,
  so an administrative unbind can be acknowledged and then undone.
- [Low] `device_schedule_reprobe()` stores only a raw `const struct
  device_driver *` (`drv = READ_ONCE(dev->driver)`, no reference, no…
- [Low] The kernel-doc of `device_schedule_reprobe()` says "Nothing is
  locked in the caller's context, so this may be called from any process…
- [Low] Reported for completeness; my baseline decision is that this is
  NOT a defect to act on.
- [Low] Reported for completeness; my baseline decision is FALSE
  POSITIVE.

--

driver core: add device_schedule_reprobe()

This adds a driver-core helper that schedules a delayed detach and
re-probe of a device from built-in work, replacing the open-coded work
items drivers use today. __device_release_driver() gains a return value
and an abort_if_blocked flag so a re-probe can abandon the unbind when
probing has been blocked meanwhile.

A few questions about the new helper and the new flag.
quoted hunk
diff --git a/drivers/base/dd.c b/drivers/base/dd.c
index f6525a7ee8c54..f521ddefc4a88 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1314,8 +1314,12 @@ EXPORT_SYMBOL_GPL(driver_attach);
 /*
  * __device_release_driver() must be called with @dev lock held.
  * When called for a USB interface, @dev->parent lock must be held as well.
+ * @abort_if_blocked gives up the release instead when probing has been
+ * blocked while the locks were dropped; only a caller that may abandon
+ * the unbind altogether can ask for it.
  */
-static void __device_release_driver(struct device *dev, struct device *parent)
+static bool __device_release_driver(struct device *dev, struct device *parent,
+				    bool abort_if_blocked)
 {
 	struct device_driver *drv;
 
@@ -1334,9 +1338,10 @@ static void __device_release_driver(struct device *dev, struct device *parent)
 			 * have released the driver successfully while this one
 			 * was waiting, so check for that.
 			 */
-			if (dev->driver != drv) {
+			if (dev->driver != drv ||
+			    (abort_if_blocked && defer_all_probes)) {
 				pm_runtime_put(dev);
-				return;
+				return false;
 			}
 		}
 
[Severity: Medium]
When the new abort_if_blocked && defer_all_probes arm fires, is the
device-links state left half torn down?

By the time this check runs, device_links_busy() has already done:

drivers/base/core.c:device_links_busy() {
	...
		WRITE_ONCE(link->status, DL_STATE_SUPPLIER_UNBIND);
	}

	dev->links.status = DL_DEV_UNBINDING;
	...
}

and device_links_unbind_consumers() has already force-released the drivers
of the active consumers.

Returning false here leaves @dev bound, so device_links_driver_cleanup() -
the only code that moves those links back to DL_STATE_DORMANT and clears
DL_DEV_UNBINDING - never runs, and device_links_driver_bound() cannot run
either because the supplier never rebinds.

Can the already-detached consumers then re-probe?
device_links_check_suppliers() looks like it would defer them forever:

drivers/base/core.c:device_links_check_suppliers() {
	...
		if (link->status != DL_STATE_AVAILABLE &&
		    !device_link_test(link, DL_FLAG_SYNC_STATE_ONLY)) {
			...
			ret = dev_err_probe(dev, -EPROBE_DEFER, ...);
	...
}

and any new device_link_add() against this still-bound supplier starts at
DL_STATE_SUPPLIER_UNBIND because supplier->links.status is DL_DEV_UNBINDING.

The pre-existing dev->driver != drv arm is different in that a concurrent
releaser completes the state machine. Here the only caller passing true,
device_reprobe_work_fn(), treats false as "nothing happened" and does no
compensating cleanup. Should the kernel-doc and commit message mention this,
or should the abort undo the links state it inherits?

[ ... ]
quoted hunk
@@ -1369,7 +1377,7 @@ void device_release_driver_internal(struct device *dev,
 	__device_driver_lock(dev, parent);
 
 	if (!drv || drv == dev->driver)
-		__device_release_driver(dev, parent);
+		__device_release_driver(dev, parent, false);
 
 	__device_driver_unlock(dev, parent);
 }
[Severity: High]
Does the new flag actually close the ->shutdown()/->remove() interleaving
for the consumers, or only for @dev?

The commit message says:

  "__device_release_driver() drops the device lock while busy consumer
   links are unbound, which would let a ->shutdown() interleave in the
   middle of a release already under way, so it now re-checks on
   re-acquisition and abandons the release."

But the consumer unbinds happen inside that dropped-lock window, before the
new check is evaluated:

drivers/base/dd.c:__device_release_driver() {
	...
		while (device_links_busy(dev)) {
			__device_driver_unlock(dev, parent);

			device_links_unbind_consumers(dev);

			__device_driver_lock(dev, parent);
	...
}

and each consumer goes through this wrapper, which hardcodes the flag off:

drivers/base/core.c:device_links_unbind_consumers() {
	...
			device_release_driver_internal(consumer, NULL,
						       consumer->parent);
	...
}

So the sequence looks reachable:

device_reprobe_work_fn()
    device_lock(dev)                       /* defer_all_probes == false */
    __device_release_driver(dev, NULL, true)
        device_links_busy(dev) == true
        device_unlock(dev)
        device_links_unbind_consumers(dev)

meanwhile:

device_shutdown()
    device_block_probing()                 /* defer_all_probes = true */
    device_lock(consumer)
    consumer->driver->shutdown(consumer)
    device_unlock(consumer)

back in device_links_unbind_consumers():

    device_release_driver_internal(consumer, ...)
        __device_release_driver(consumer, ..., false)
            device_remove(consumer)        /* consumer ->remove() */

Is the consumer's ->remove() then run after its ->shutdown() has already
quiesced it? Nothing else serialises the two beyond the consumer's own
device lock, and wait_for_device_probe() cannot see the new delayed work:

drivers/base/dd.c:wait_for_device_probe() {
	flush_work(&deferred_probe_work);
	wait_event(probe_waitqueue, atomic_read(&probe_count) == 0);
	async_synchronize_full();
}

Should the abort decision be propagated into the consumer releases before
their removal callbacks run, or should shutdown be synchronised against the
whole in-flight re-probe?
quoted hunk
@@ -1436,3 +1444,98 @@ void driver_detach(const struct device_driver *drv)
 		put_device(dev);
 	}
 }
+
+struct device_reprobe {
+	struct delayed_work work;
+	const struct device_driver *drv;
+	struct device *dev;
+};
+
+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 ever compared, never dereferenced: the driver it
+	 * points to may have been unregistered and freed by now.
+	 * device_shutdown() blocks probing before its walk reaches @dev.
+	 */
+	if (!defer_all_probes && !dev->p->dead && dev->driver == rp->drv)
+		detached = __device_release_driver(dev, NULL, true);
+	device_unlock(dev);
+
+	if (detached) {
+		ret = device_attach(dev);
+		if (ret < 0 && ret != -EPROBE_DEFER)
+			dev_err_probe(dev, ret,
+				      "re-probe failed, device left unbound\n");
+	}
[Severity: Medium]
Only the detach half is gated on defer_all_probes here; is the attach half
also meant to be?

device_attach() runs after device_unlock(dev) with no re-check, and
__device_attach() resumes the parent before anything tests defer_all_probes:

drivers/base/dd.c:__device_attach() {
	...
		if (dev->parent)
			pm_runtime_get_sync(dev->parent);

		ret = bus_for_each_drv(dev->bus, NULL, &data,
					__device_attach_driver);
	...
}

really_probe() only rejects the probe later. device_shutdown() itself only
does pm_runtime_get_noresume() plus a barrier, so a parent that is
RPM_SUSPENDED stays suspended - can this work then runtime-resume a parent
whose ->shutdown() has already run, and add @dev to the deferred-probe list
while probing is blocked?

Nothing cancels or flushes this work either, and wait_for_device_probe()
(which device_block_probing() relies on) only flushes deferred_probe_work,
waits for probe_count and calls async_synchronize_full(), none of which
observe a system_freezable_wq delayed work.

[Severity: Medium]
Can an administrative unbind be acknowledged and then undone here, since the
detach and the re-attach are not one serialised operation?

drivers/base/bus.c:unbind_store() {
	...
	if (dev && dev->driver == drv) {
		device_driver_detach(dev);
		err = count;
	...
}

That dev->driver == drv test is unlocked, and device_driver_detach() ends up
in device_release_driver_internal(), which blocks on the device lock this
work holds. When it gets the lock, dev->driver is NULL, so it does nothing,
and unbind_store() still returns count. Then this work calls
device_attach(dev) and the device is bound again. The same applies to an
unbind arriving in the window between device_unlock(dev) and
device_attach(dev).

The commit message lists "a stale re-probe undoes an administrative unbind"
among the things the new helper addresses, while the new return value only
covers an unbind that wins inside the busy-consumer-links loop. Could that
wording be narrowed, or the gap between detach and re-attach closed?
+
+	put_device(dev);
+	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.
+ * The re-probe is skipped if, by the time the scheduled work runs, the
+ * device has been removed, probing has been blocked for a system
+ * shutdown, or @dev is no longer bound to the driver that was bound at
+ * scheduling time.
+ *
+ * The work function is built-in text, so the bound driver may call this
+ * from its own code without holding a module reference. If the driver
+ * module is unloaded before the work runs, driver unregistration unbinds
+ * @dev first and the scheduled work does nothing.
[ ... ]
+ *
+ * Nothing is locked in the caller's context, so this may be called from
+ * any process context, @dev's own device lock held included, but not
+ * from @dev's ->probe(), which the scheduled work would detach.
[ ... ]
+ */
+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);
[Severity: Low]
The recorded identity of the binding is a bare struct device_driver pointer,
with no reference, no module pin and no generation counter, and
device_reprobe_work_fn() gates the destructive detach only on

	dev->driver == rp->drv

Is pointer identity enough here? An unbind followed by a rebind of the same
driver within @delay_ms makes the test pass for a binding the caller never
scheduled against. After a module unload, can the same address be reused by
another module's struct device_driver, in which case the kernel-doc promise

  "If the driver module is unloaded before the work runs, driver
   unregistration unbinds @dev first and the scheduled work does nothing."

no longer holds, and the work would drive __device_release_driver() -
including device_links_unbind_consumers() and an unrelated driver's
->remove() - followed by device_attach()?

There is also no cancel or flush counterpart, so a caller cannot gate this
itself. Would a bind generation counter, a per-binding token, or at least
comparing the driver name as well, match the documented semantics?
+	/*
+	 * 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;
[Severity: Low]
The kernel-doc says this "may be called from any process context", but
kzalloc_obj() with no gfp argument resolves to GFP_KERNEL and may sleep:

include/linux/gfp.h:
	#define __default_gfp(a,b,...) b
	#define default_gfp(...) __default_gfp(,##__VA_ARGS__,GFP_KERNEL)

The commit message invites callers from the PM callbacks, whose _late and
_noirq phases run with interrupts disabled. Could a Context: line stating
that this helper may sleep be added, or the gfp flags be passed explicitly?
+
+	rp->dev = get_device(dev);
+	rp->drv = drv;
+
+	INIT_DELAYED_WORK(&rp->work, device_reprobe_work_fn);
+	queue_delayed_work(system_freezable_wq, &rp->work,
+			   msecs_to_jiffies(delay_ms));
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(device_schedule_reprobe);
[Severity: Low]
This isn't a bug, just a wording note. The commit message opens by
describing live defects in iwlwifi's iwl_trans_reprobe_wk() and hci_h5's
h5_btrtl_reprobe_worker():

  "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 driver is converted here and there is no Fixes: tag, while the newly
exported symbol has no in-tree caller until the mxl862xx patches later in
the series. Splitting the core helper from its first user is normal, so
nothing needs changing in the code; would leading with the motivation rather
than with those two drivers read more clearly?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1789477568.git.daniel%40makrotopia.org
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help