From: netdev-bot+sashiko@kernel.org
To: daniel@makrotopia.org
Cc: jiri@resnulli.us, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
corbet@lwn.net, skhan@linuxfoundation.org, rdunlap@infradead.org,
gregkh@linuxfoundation.org, rafael@kernel.org, dakr@kernel.org,
andrew@lunn.ch, olteanv@gmail.com, netdev@vger.kernel.org,
linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
driver-core@lists.linux.dev
Subject: Re: [PATCH net-next v16 3/6] driver core: add device_schedule_reprobe()
Date: Thu, 17 Sep 2026 16:11:26 +0000 [thread overview]
Message-ID: <178966148657.22033.10297729973512480999@kernel.org> (raw)
In-Reply-To: =?utf-8?q?=3Cb8cfff498ef5662093062ea03015cb3ccb95957b=2E1789477?= =?utf-8?q?568=2Egit=2Edaniel=40makrotopia=2Eorg=3E?=
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.
> 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?
[ ... ]
> @@ -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?
> @@ -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
next reply other threads:[~2026-09-17 16:11 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-17 16:11 netdev-bot+sashiko [this message]
-- strict thread matches above, loose matches on Subject: below --
2026-09-15 13:09 [PATCH net-next v16 0/6] net: dsa: mxl862xx: devlink flash and rescue Daniel Golle
2026-09-15 13:09 ` [PATCH net-next v16 3/6] driver core: add device_schedule_reprobe() Daniel Golle
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=178966148657.22033.10297729973512480999@kernel.org \
--to=netdev-bot+sashiko@kernel.org \
--cc=andrew@lunn.ch \
--cc=corbet@lwn.net \
--cc=dakr@kernel.org \
--cc=daniel@makrotopia.org \
--cc=davem@davemloft.net \
--cc=driver-core@lists.linux.dev \
--cc=edumazet@google.com \
--cc=gregkh@linuxfoundation.org \
--cc=horms@kernel.org \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=olteanv@gmail.com \
--cc=pabeni@redhat.com \
--cc=rafael@kernel.org \
--cc=rdunlap@infradead.org \
--cc=skhan@linuxfoundation.org \
/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
all inboxes | Powered by JetHome®