mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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

             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®