Linux wireless drivers development
 help / color / mirror / Atom feed
From: Hans de Goede <johannes.goede@oss.qualcomm.com>
To: Daniel Golle <daniel@makrotopia.org>,
	Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
	"Rafael J. Wysocki" <rafael@kernel.org>,
	Danilo Krummrich <dakr@kernel.org>,
	Marcel Holtmann <marcel@holtmann.org>,
	Luiz Augusto von Dentz <luiz.dentz@gmail.com>,
	Miri Korenblit <miriam.rachel.korenblit@intel.com>,
	driver-core@lists.linux.dev, linux-kernel@vger.kernel.org,
	linux-bluetooth@vger.kernel.org, linux-wireless@vger.kernel.org
Subject: Re: [PATCH v2 1/4] driver core: add device_schedule_reprobe()
Date: Thu, 20 Aug 2026 14:37:51 +0200	[thread overview]
Message-ID: <c461462f-de0b-43e8-ac9e-541013f5f8da@oss.qualcomm.com> (raw)
In-Reply-To: <08dc37c0d20896f5dc899a53c857935e91c6740f.1787185594.git.daniel@makrotopia.org>

Hi Daniel,

On 20-Aug-26 02:32, Daniel Golle wrote:
> Three in-tree drivers schedule a deferred re-probe of their own device
> from a work item whose work function lives in module text: iwlwifi
> (iwl_trans_schedule_reprobe(), firmware crash recovery when a lighter
> restart is not sufficient), hci_h5 (h5_btrtl_resume(), RTL devices
> lose their firmware state over suspend) and btintel_pcie (synchronous
> device_reprobe() from its own reset work, with a hand-rolled locking
> contract spanning several comments).
> 
> Two bug classes affect the hand-rolled implementations:
> 
> 1. The work function ends with put_device(); kfree();
>    module_put(THIS_MODULE); in module text. After the atomic decrement
>    a concurrent rmmod can free the module text before the function
>    epilogue has finished executing. This is exactly the race
>    module_put_and_kthread_exit() exists to close for kthreads; there
>    is no work-item equivalent.
> 
> 2. There is no synchronization between the deferred device_reprobe()
>    and device_shutdown() or a driver unbind. The drivers do not check
>    any bound state before calling device_reprobe(), so a stale
>    re-probe can undo an administrative unbind, and the detach half can
>    run against a device whose ->shutdown() callback has already run.
>    The core already blocks the attach half during shutdown
>    (device_shutdown() calls device_block_probing() before any
>    callback, and really_probe() honors defer_all_probes), but nothing
>    blocks the detach half. For drivers which clear their drvdata in
>    ->shutdown() so that a subsequent ->remove() becomes a no-op this
>    escalates to use-after-free of driver state which other subsystem
>    structures still reference.
> 
> Both classes disappear when the driver core owns the deferred work.
> Add device_schedule_reprobe(), which schedules a detach and re-probe
> of a device after a caller-specified delay:
> 
> - The work function is builtin text, so callers do not need to hold a
>   module reference. If the driver module is unloaded before the work
>   runs, driver_unregister() has already unbound the device, the bound
>   driver no longer matches the driver recorded at scheduling time and
>   the work does nothing.
> 
> - The recorded driver pointer is only ever compared, never
>   dereferenced, so it may legitimately point to freed memory.
> 
> - The bound-state check and __device_release_driver() run under a
>   single __device_driver_lock() hold, the same lock dance
>   device_release_driver_internal() uses. This closes the
>   check-vs-detach TOCTOU that drivers cannot close themselves,
>   because device_reprobe() takes the device lock internally.
> 
> - Both @dev and its parent are pinned for the lifetime of the work.
>   __device_driver_lock() and the attach half lock the parent, and an
>   unregister of @dev drops @dev's reference to the parent, so without
>   a reference of our own the parent could be freed before the work
>   runs.
> 
> - A new shutdown_done flag in struct device_private, set under the
>   device lock once device_shutdown() reaches a device, suppresses the
>   detach half during shutdown. It occupies a spare bit in an existing
>   byte, mirroring how kill_device() sets the dead flag.
> 
> - The attach half is plain device_attach(), which already honors both
>   the dead flag and defer_all_probes: a re-probe landing during
>   system suspend detaches immediately and the probe is deferred until
>   device_restore_probing() at resume time. The detach half
>   deliberately does not check defer_all_probes so that a re-probe
>   scheduled before suspend is not silently dropped. The parent is
>   re-locked across device_attach() on buses that set
>   need_parent_lock, mirroring bus_rescan_devices_helper().
> 
> One pre-existing window remains: __device_release_driver()
> transiently drops the locks while consumer device links are busy, so
> for devices with busy consumers a ->shutdown() can still interleave
> in the middle of the release. That window exists identically for
> every unbind path in the kernel, sysfs unbind included, and is not
> made worse by this helper.
> 
> Signed-off-by: Daniel Golle <daniel@makrotopia.org>
> ---
>  drivers/base/base.h    |  5 +++
>  drivers/base/core.c    |  3 ++
>  drivers/base/dd.c      | 99 ++++++++++++++++++++++++++++++++++++++++++
>  include/linux/device.h |  2 +
>  4 files changed, 109 insertions(+)
> 
> diff --git a/drivers/base/base.h b/drivers/base/base.h
> index a5b7abc10ff0..6234e37de7e9 100644
> --- a/drivers/base/base.h
> +++ b/drivers/base/base.h
> @@ -106,6 +106,10 @@ struct driver_private {
>   * @dead: This device is currently either in the process of or has been
>   *	  removed from the system. Any asynchronous events scheduled for this
>   *	  device should exit without taking any action.
> + * @shutdown_done: Set once device_shutdown() has reached this device, under
> + *	  the device lock, before any shutdown callback runs. Read under the
> + *	  device lock. A deferred re-probe scheduled with
> + *	  device_schedule_reprobe() must not detach the device anymore.
>   *
>   * Nothing outside of the driver core should ever touch these fields.
>   */
> @@ -120,6 +124,7 @@ struct device_private {
>  	char *deferred_probe_reason;
>  	struct device *device;
>  	u8 dead:1;
> +	u8 shutdown_done:1;
>  };
>  #define to_device_private_parent(obj)	\
>  	container_of(obj, struct device_private, knode_parent)
> diff --git a/drivers/base/core.c b/drivers/base/core.c
> index 4d026682944f..8a7dbe4e8362 100644
> --- a/drivers/base/core.c
> +++ b/drivers/base/core.c
> @@ -4906,6 +4906,9 @@ void device_shutdown(void)
>  			device_lock(parent);
>  		device_lock(dev);
>  
> +		if (dev->p)
> +			dev->p->shutdown_done = true;
> +
>  		/* Don't allow any more runtime suspends */
>  		pm_runtime_get_noresume(dev);
>  		pm_runtime_barrier(dev);
> diff --git a/drivers/base/dd.c b/drivers/base/dd.c
> index 60c005223844..3394b1c7ed18 100644
> --- a/drivers/base/dd.c
> +++ b/drivers/base/dd.c
> @@ -1436,3 +1436,102 @@ void driver_detach(const struct device_driver *drv)
>  		put_device(dev);
>  	}
>  }
> +
> +struct device_reprobe {
> +	struct delayed_work work;
> +	struct device *dev;
> +	struct device *parent;
> +	const struct device_driver *drv;
> +};
> +
> +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;
> +	struct device *parent = rp->parent;
> +	bool detached = false;
> +
> +	__device_driver_lock(dev, parent);
> +	/*
> +	 * rp->drv is only ever compared, never dereferenced: the driver it
> +	 * points to may have been unregistered and freed by now.
> +	 */
> +	if (!dev->p->dead && !dev->p->shutdown_done &&
> +	    dev->driver && dev->driver == rp->drv) {
> +		__device_release_driver(dev, parent);
> +		detached = true;
> +	}
> +	__device_driver_unlock(dev, parent);
> +
> +	if (detached) {
> +		/*
> +		 * device_attach() must run with the parent locked on buses
> +		 * that require it, mirroring bus_rescan_devices_helper().
> +		 */
> +		if (parent && dev->bus->need_parent_lock)
> +			device_lock(parent);
> +		if (device_attach(dev) < 0)
> +			dev_err(dev, "re-probe failed, device left unbound\n");

Testing suspend/resume with a hci_h5 BT HCI which needs to be re-probed
at resume has shown that this may fail with -EPROBE_DEFER when run during
resume.

The probe does get successfully retried later and then everything works,
but this failure caused the dev_err() to log a spurious error.

So this should be switched to using dev_err_probe(), e.g.
squash in this:

--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1451,6 +1451,7 @@ static void device_reprobe_work_fn(struct work_struct *work)
 	struct device *dev = rp->dev;
 	struct device *parent = rp->parent;
 	bool detached = false;
+	int ret;
 
 	__device_driver_lock(dev, parent);
 	/*
@@ -1471,8 +1472,9 @@ static void device_reprobe_work_fn(struct work_struct *work)
 		 */
 		if (parent && dev->bus->need_parent_lock)
 			device_lock(parent);
-		if (device_attach(dev) < 0)
-			dev_err(dev, "re-probe failed, device left unbound\n");
+		ret = device_attach(dev);
+		if (ret < 0)
+			dev_err_probe(dev, ret, "re-probe failed, device left unbound\n");
 		if (parent && dev->bus->need_parent_lock)
 			device_unlock(parent);
 	}

With that fixed this looks good to me:

Tested-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
Reviewed-by: Hans de Goede <johannes.goede@oss.qualcomm.com>

Regards,

Hans






> +		if (parent && dev->bus->need_parent_lock)
> +			device_unlock(parent);
> +	}
> +
> +	put_device(dev);
> +	put_device(parent);
> +	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, the system shutdown sequence has reached the
> + * device, or @dev is no longer bound to the driver that was bound at
> + * scheduling time. In particular an administrative unbind is never
> + * undone by a stale re-probe.
> + *
> + * 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.
> + *
> + * Multiple pending re-probes for the same device are individually safe;
> + * a caller that wants at most one pending re-probe must gate scheduling
> + * itself.
> + *
> + * May only be called from process context.
> + *
> + * Returns: 0 on success, -EINVAL if @dev is not a registered device
> + * bound to a driver, -ENOMEM on allocation failure.
> + */
> +int device_schedule_reprobe(struct device *dev, unsigned int delay_ms)
> +{
> +	struct device_reprobe *rp;
> +
> +	if (!dev->bus || !dev->p || !device_is_registered(dev))
> +		return -EINVAL;
> +	if (!dev->driver)
> +		return -EINVAL;
> +
> +	rp = kzalloc_obj(*rp);
> +	if (!rp)
> +		return -ENOMEM;
> +
> +	rp->dev = get_device(dev);
> +	/*
> +	 * Pin the parent too: the work locks it, and an unregister of @dev
> +	 * would otherwise drop the last reference before the work runs.
> +	 */
> +	rp->parent = get_device(dev->parent);
> +	rp->drv = READ_ONCE(dev->driver);
> +	INIT_DELAYED_WORK(&rp->work, device_reprobe_work_fn);
> +	queue_delayed_work(system_dfl_wq, &rp->work,
> +			   msecs_to_jiffies(delay_ms));
> +
> +	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);
>  


  reply	other threads:[~2026-08-20 12:37 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-20  0:32 [PATCH v2 0/4] device_schedule_reprobe(): core helper and conversions Daniel Golle
2026-08-20  0:32 ` [PATCH v2 1/4] driver core: add device_schedule_reprobe() Daniel Golle
2026-08-20 12:37   ` Hans de Goede [this message]
2026-08-20  0:32 ` [PATCH v2 2/4] wifi: iwlwifi: use device_schedule_reprobe() Daniel Golle
2026-08-20  0:32 ` [PATCH v2 3/4] Bluetooth: hci_h5: " Daniel Golle
2026-08-20 12:48   ` Hans de Goede
2026-08-20  0:33 ` [PATCH v2 4/4] Bluetooth: btintel_pcie: use device_schedule_reprobe() after reset 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=c461462f-de0b-43e8-ac9e-541013f5f8da@oss.qualcomm.com \
    --to=johannes.goede@oss.qualcomm.com \
    --cc=dakr@kernel.org \
    --cc=daniel@makrotopia.org \
    --cc=driver-core@lists.linux.dev \
    --cc=gregkh@linuxfoundation.org \
    --cc=linux-bluetooth@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-wireless@vger.kernel.org \
    --cc=luiz.dentz@gmail.com \
    --cc=marcel@holtmann.org \
    --cc=miriam.rachel.korenblit@intel.com \
    --cc=rafael@kernel.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