Linux bluetooth development
 help / color / mirror / Atom feed
* [PATCH v4 0/4] device_schedule_reprobe(): core helper and conversions
@ 2026-10-17  8:55 Daniel Golle
  2026-10-17  8:56 ` [PATCH v4 1/4] driver core: add device_schedule_reprobe() Daniel Golle
                   ` (3 more replies)
  0 siblings, 4 replies; 10+ messages in thread
From: Daniel Golle @ 2026-10-17  8:55 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich,
	Marcel Holtmann, Luiz Augusto von Dentz, Miri Korenblit,
	Emmanuel Grumbach, Hans de Goede, driver-core, linux-kernel,
	linux-bluetooth, linux-wireless

iwlwifi and hci_h5 re-probe their own device from a work item in module
text ending in module_put(THIS_MODULE), which a concurrent rmmod can
race, and nothing ties the re-probe to the binding that asked for it.
btintel_pcie re-probes synchronously from its own reset work. This
series adds device_schedule_reprobe() to the driver core and converts
all three.

Tested on an Intel AX101 with patches 1 to 3, PROVE_LOCKING and
DEBUG_OBJECTS_WORK: a re-probe, and an unbind, rmmod, PCI removal,
suspend and reboot with one pending. The unbind and the removals
cancelled it, and it ran after resume. The stock iwlwifi code undid
the unbind, and rmmod with its work held just past module_put() oopsed
in lockdep. Patches 3 and 4 are untested for lack of hardware.

Changes since v3 [4]:
 - patch 1 was reworked as patch 3/6 of the mxl862xx devlink flash
   series, v13 to v19 [5][6], which netdev has deferred until this
   lands; its history summarises the changes
 - patch 1: Hans de Goede's tags dropped [7]; patch 3 keeps his, its
   code being unchanged, but a re-test would be welcome
 - patches 2 and 3: Fixes: tags
 - patches 2 to 4: commit messages updated; rebased onto bluetooth-next

Changes since v2 [3]:
 - patch 1: use dev_err_probe() for the re-probe error path so a
   re-probe deferred at resume no longer logs a spurious error; pick up
   Hans de Goede's Tested-by/Reviewed-by
 - patch 3: pick up Hans de Goede's Tested-by/Reviewed-by
 - patch 4: rebased onto bluetooth-next, whose btintel_pcie has four
   dump workers rather than the single one the v2 mainline snapshot had
 - base the series on bluetooth-next instead of v7.2-rc7 so the
   bluetooth patches apply for their maintainers

Changes since the RFC (v1) [1]:
 - pin the parent device across the deferred work and re-lock it across
   device_attach() on need_parent_lock buses (found reviewing the
   mxl862xx posting [2])
 - schedule the work on system_dfl_wq instead of the deprecated
   system_unbound_wq
 - thread the series properly (the RFC's patches were unthreaded)
 - drop RFC status

[1] https://lore.kernel.org/all/anpxFdwNxk0XwPjQ@makrotopia.org/
[2] https://lore.kernel.org/all/cover.1786922210.git.daniel@makrotopia.org/
[3] https://lore.kernel.org/all/cover.1787185594.git.daniel@makrotopia.org/
[4] https://lore.kernel.org/all/cover.1787281239.git.daniel@makrotopia.org/
[5] https://lore.kernel.org/all/cover.1788783126.git.daniel@makrotopia.org/
[6] https://lore.kernel.org/all/cover.1791035885.git.daniel@makrotopia.org/
[7] https://lore.kernel.org/all/aqlPPyblTK8c6-zZ@makrotopia.org/

---
On LLM use: device_schedule_reprobe() was designed and first
implemented by hand. Its later revisions answer AI review (Sashiko on
the mxl862xx postings, and local runs of the same prompts); an LLM
helped reproduce those findings and draft the changes and changelogs.
Every decision and the final review were mine; the Assisted-by tag on
patch 1 records this.

Daniel Golle (4):
  driver core: add device_schedule_reprobe()
  wifi: iwlwifi: use device_schedule_reprobe()
  Bluetooth: hci_h5: use device_schedule_reprobe()
  Bluetooth: btintel_pcie: use device_schedule_reprobe() after reset

 drivers/base/base.h                           |   4 +
 drivers/base/dd.c                             | 171 ++++++++++++++++++
 drivers/bluetooth/btintel_pcie.c              |  47 +++--
 drivers/bluetooth/hci_h5.c                    |  43 +----
 .../net/wireless/intel/iwlwifi/iwl-trans.c    |  40 +---
 include/linux/device.h                        |   2 +
 6 files changed, 208 insertions(+), 99 deletions(-)


base-commit: 185c11232d5f586489bb3b74e6bcc7a6560ddd40
-- 
2.56.0

^ permalink raw reply	[flat|nested] 10+ messages in thread
* [PATCH v3 1/4] driver core: add device_schedule_reprobe()
@ 2026-08-21  3:08 Daniel Golle
  2026-08-21  4:26 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
  2026-09-10 19:07 ` bluez.test.bot
  0 siblings, 2 replies; 10+ messages in thread
From: Daniel Golle @ 2026-08-21  3:08 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich,
	Marcel Holtmann, Luiz Augusto von Dentz, Miri Korenblit,
	driver-core, linux-kernel, linux-bluetooth, linux-wireless
  Cc: Hans de Goede

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>
Tested-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
Reviewed-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
---
v3: 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)
v2: pin the parent across the deferred work and re-lock it across
    device_attach() on need_parent_lock buses; schedule on system_dfl_wq
    rather than the deprecated system_unbound_wq (found reviewing the
    mxl862xx v12 posting)
v1: initial RFC

 drivers/base/base.h    |   5 ++
 drivers/base/core.c    |   3 ++
 drivers/base/dd.c      | 102 +++++++++++++++++++++++++++++++++++++++++
 include/linux/device.h |   2 +
 4 files changed, 112 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..d765e1ae5614 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1436,3 +1436,105 @@ 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;
+	int ret;
+
+	__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);
+		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);
+	}
+
+	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 7b2baffdd2f5..1cdd40a6a48d 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -1312,6 +1312,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

^ permalink raw reply related	[flat|nested] 10+ messages in thread
* [PATCH v2 1/4] driver core: add device_schedule_reprobe()
@ 2026-08-20  0:32 Daniel Golle
  2026-08-20  2:01 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
  0 siblings, 1 reply; 10+ messages in thread
From: Daniel Golle @ 2026-08-20  0:32 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich,
	Marcel Holtmann, Luiz Augusto von Dentz, Miri Korenblit,
	driver-core, linux-kernel, linux-bluetooth, linux-wireless
  Cc: Hans de Goede

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");
+		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);
 
-- 
2.55.0

^ permalink raw reply related	[flat|nested] 10+ messages in thread
* [RFC PATCH 1/4] driver core: add device_schedule_reprobe()
@ 2026-08-11  0:48 Daniel Golle
  2026-08-11  3:28 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
  0 siblings, 1 reply; 10+ messages in thread
From: Daniel Golle @ 2026-08-11  0:48 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rafael J. Wysocki, Danilo Krummrich,
	driver-core, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Andrew Lunn, Vladimir Oltean
  Cc: netdev, linux-kernel, Miri Korenblit, linux-wireless,
	Marcel Holtmann, Luiz Augusto von Dentz, linux-bluetooth,
	Hans de Goede

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.

- 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.

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      | 82 ++++++++++++++++++++++++++++++++++++++++++
 include/linux/device.h |  2 ++
 4 files changed, 92 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 4c0c373998a1..3dcd8a3c3aa5 100644
--- a/drivers/base/core.c
+++ b/drivers/base/core.c
@@ -4910,6 +4910,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 f6525a7ee8c5..fc23f5bebd20 100644
--- a/drivers/base/dd.c
+++ b/drivers/base/dd.c
@@ -1436,3 +1436,85 @@ void driver_detach(const struct device_driver *drv)
 		put_device(dev);
 	}
 }
+
+struct device_reprobe {
+	struct delayed_work work;
+	struct device *dev;
+	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 = dev->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(dev) < 0)
+		dev_err(dev, "re-probe failed, device left unbound\n");
+
+	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, 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);
+	rp->drv = READ_ONCE(dev->driver);
+	INIT_DELAYED_WORK(&rp->work, device_reprobe_work_fn);
+	queue_delayed_work(system_unbound_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);
 
-- 
2.55.0

^ permalink raw reply related	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-10-17 10:32 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-17  8:55 [PATCH v4 0/4] device_schedule_reprobe(): core helper and conversions Daniel Golle
2026-10-17  8:56 ` [PATCH v4 1/4] driver core: add device_schedule_reprobe() Daniel Golle
2026-10-17 10:32   ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
2026-10-17  8:56 ` [PATCH v4 2/4] wifi: iwlwifi: use device_schedule_reprobe() Daniel Golle
2026-10-17  8:56 ` [PATCH v4 3/4] Bluetooth: hci_h5: " Daniel Golle
2026-10-17  8:57 ` [PATCH v4 4/4] Bluetooth: btintel_pcie: use device_schedule_reprobe() after reset Daniel Golle
  -- strict thread matches above, loose matches on Subject: below --
2026-08-21  3:08 [PATCH v3 1/4] driver core: add device_schedule_reprobe() Daniel Golle
2026-08-21  4:26 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
2026-09-10 19:07 ` bluez.test.bot
2026-08-20  0:32 [PATCH v2 1/4] driver core: add device_schedule_reprobe() Daniel Golle
2026-08-20  2:01 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot
2026-08-11  0:48 [RFC PATCH 1/4] driver core: add device_schedule_reprobe() Daniel Golle
2026-08-11  3:28 ` device_schedule_reprobe(): core helper and conversions bluez.test.bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox