From nobody Mon Sep 28 14:46:00 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9EE8C364029; Fri, 21 Aug 2026 03:08:35 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281717; cv=none; b=U5/ihzmJ93OqiYZXYoWEJt7INrZy6GZlFY+OhFxZaIrCS3AOqLPU8wcYh9lNuImPWaFkgWiPBe/T+V7zXXFNZcaz0HqvEjZ1S9jDcfHS5rP5ongjm7m91ZVUGWEIKRvwCU9KHVoPyzcZK73/lrapG7H34J4Hni7x5b18yNenYBI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281717; c=relaxed/simple; bh=vL/51s4zq5G9lDYLUhjPH5ksxW3Zm2y1BeU+EQEmO0A=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=NMNCsTFOSQ9iEPBfF+N3iBTmHsjOIVK7KxEsFv1Vcfd+eut4tUG2jshYNXbqAndVdJGR5OGHUS4pQmjTWR0IyWwKSYO4Ju7fjPGYca17e+PTidrfpfp6BrYxZ63DERemJzMCwWlpDbDx4BCfwnlXc5Egv+XkqgitpOg/QN8RGsw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wxFcP-000000005Tg-1AxV; Fri, 21 Aug 2026 03:08:33 +0000 Date: Fri, 21 Aug 2026 04:08:30 +0100 From: Daniel Golle To: Greg Kroah-Hartman , "Rafael J. Wysocki" , Danilo Krummrich , Marcel Holtmann , Luiz Augusto von Dentz , Miri Korenblit , driver-core@lists.linux.dev, linux-kernel@vger.kernel.org, linux-bluetooth@vger.kernel.org, linux-wireless@vger.kernel.org Cc: Hans de Goede Subject: [PATCH v3 1/4] driver core: add device_schedule_reprobe() Message-ID: <1b051bb8145febf69cb33d9e8a83444db079e8f2.1787281239.git.daniel@makrotopia.org> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 Tested-by: Hans de Goede Reviewed-by: Hans de Goede --- 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, und= er + * 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); =20 + if (dev->p) + dev->p->shutdown_done =3D 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 =3D container_of(work, struct device_reprobe, + work.work); + struct device *dev =3D rp->dev; + struct device *parent =3D rp->parent; + bool detached =3D 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 =3D=3D rp->drv) { + __device_release_driver(dev, parent); + detached =3D 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 =3D 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 =3D kzalloc_obj(*rp); + if (!rp) + return -ENOMEM; + + rp->dev =3D 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 =3D get_device(dev->parent); + rp->drv =3D 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); =20 bool device_is_bound(struct device *dev); =20 --=20 2.55.0 From nobody Mon Sep 28 14:46:00 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 091FF361975; Fri, 21 Aug 2026 03:08:46 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281728; cv=none; b=LW2wmu9AagEmdk39dhhNnJ0xiUO4J6Nm/yrmKgs6021J3fNyF95dMCYJRAW+Y/SxtgeT/JcRY6/1HrKhq0BI2rh0hZe8GmM1f1ZMg/0HEaYQoiua6Twr0kKHxBentuONsjYhqQzYGwYN85XaNsq0AUnUyFSkuYUyE1Ql60t9GxA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281728; c=relaxed/simple; bh=CpQsft4bkU5IPa2Odpd88k7pfvX0/CNch9+XSKc/gVs=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=VIRmMsv1lvDOzCVUazBBHquxGhEU+NhB7uB6/lWYnfMj2As8vy0L9ZblCKtVWkijsXuQvvi+k7FaArPNd0sZcGNal034NNyQEJbjQdmvqs8QSwDnQdXtK0/VQp9TXvt4W3sh6WRikUwBZLr8DF2g00M8/vLOwGM7m7qIcpxNCK4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wxFca-000000005Tw-1lMy; Fri, 21 Aug 2026 03:08:44 +0000 Date: Fri, 21 Aug 2026 04:08:41 +0100 From: Daniel Golle To: Greg Kroah-Hartman , "Rafael J. Wysocki" , Danilo Krummrich , Marcel Holtmann , Luiz Augusto von Dentz , Miri Korenblit , driver-core@lists.linux.dev, linux-kernel@vger.kernel.org, linux-bluetooth@vger.kernel.org, linux-wireless@vger.kernel.org Cc: Hans de Goede Subject: [PATCH v3 2/4] wifi: iwlwifi: use device_schedule_reprobe() Message-ID: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" iwl_trans_schedule_reprobe() open-codes a deferred re-probe: it takes a module reference, allocates a work item, and the work function calls device_reprobe() and then ends with put_device(); kfree(); module_put(THIS_MODULE); in module text. That final module_put() is racy: once the reference count is decremented a concurrent rmmod can free the module text before the work function's epilogue has finished executing. The work also does not synchronize against shutdown or unbind, so a stale re-probe could undo an administrative unbind or detach a device whose ->shutdown() callback has already run. Convert to the new device_schedule_reprobe() helper, whose work function is builtin text and which skips the re-probe when the device was removed, shutdown reached it, or it is no longer bound to the driver that scheduled the re-probe. Both call sites keep their delays (IWL_TRANS_TOP_FOLLOWER_WAIT for the TOP follower case, 0 for the escalated firmware error case). Behavioral changes: - A pending re-probe no longer pins the module: rmmod with a re-probe pending now succeeds immediately and the re-probe becomes a no-op, instead of rmmod failing with EBUSY. The "Module is being unloaded - abort" path disappears together with the try_module_get(). - A re-probe scheduled before a system shutdown or before an administrative unbind no longer detaches and rebinds the device afterwards. Signed-off-by: Daniel Golle --- v3: no changes v2: no changes v1: initial RFC .../net/wireless/intel/iwlwifi/iwl-trans.c | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/drivers/net/wireless/intel/iwlwifi/iwl-trans.c b/drivers/net/w= ireless/intel/iwlwifi/iwl-trans.c index 73aae1125042..5ae734cb9027 100644 --- a/drivers/net/wireless/intel/iwlwifi/iwl-trans.c +++ b/drivers/net/wireless/intel/iwlwifi/iwl-trans.c @@ -78,47 +78,11 @@ void iwl_trans_free_restart_list(void) } } =20 -struct iwl_trans_reprobe { - struct device *dev; - struct delayed_work work; -}; - -static void iwl_trans_reprobe_wk(struct work_struct *wk) -{ - struct iwl_trans_reprobe *reprobe; - - reprobe =3D container_of(wk, typeof(*reprobe), work.work); - - if (device_reprobe(reprobe->dev)) - dev_err(reprobe->dev, "reprobe failed!\n"); - put_device(reprobe->dev); - kfree(reprobe); - module_put(THIS_MODULE); -} - static void iwl_trans_schedule_reprobe(struct iwl_trans *trans, unsigned int delay_ms) { - struct iwl_trans_reprobe *reprobe; - - /* - * get a module reference to avoid doing this while unloading - * anyway and to avoid scheduling a work with code that's - * being removed. - */ - if (!try_module_get(THIS_MODULE)) { - IWL_ERR(trans, "Module is being unloaded - abort\n"); - return; - } - - reprobe =3D kzalloc_obj(*reprobe); - if (!reprobe) { - module_put(THIS_MODULE); - return; - } - reprobe->dev =3D get_device(trans->dev); - INIT_DELAYED_WORK(&reprobe->work, iwl_trans_reprobe_wk); - schedule_delayed_work(&reprobe->work, msecs_to_jiffies(delay_ms)); + if (device_schedule_reprobe(trans->dev, delay_ms)) + IWL_ERR(trans, "Could not schedule reprobe\n"); } =20 #define IWL_TRANS_RESET_OK_TIME 7 /* seconds */ --=20 2.55.0 From nobody Mon Sep 28 14:46:00 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 02A0E363C55; Fri, 21 Aug 2026 03:08:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281735; cv=none; b=dTmWcY3UWXenIL1xcPskfUowskuNw02C+Pg1HgsSOU5GNSLHrGXYJoeIVO8GXj4tGdE56QLZAc8Xn8zpXsmPwyeCbeUHspJj0TGEGdAPOkVygIoSlqBtbpT7p08YVGBt67ASKKvGP+UElDFOdiH6bvPqHrFpAiph10mfxQHCcAk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281735; c=relaxed/simple; bh=gnihcTbgcp3S30kcpp/YO1ReMgGT2xOVtZwaKMlzjRo=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=gwb0Z9RTg0WVbT2xRjDb1HZ4etOhKJWYia6vPM1kuGIFuAkwKTzT1dreER1RB2q6OhNkdoetWMWPEeEWJSj/UCxVAPkwU+hunTQkg10Q0gNndIOAnGwxJ+NJqe4D9PMPZal8Sgg/HmiWsKdZaBNGMi30YdcwbDBbqZqC86ZadQU= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wxFch-000000005UR-2mJ0; Fri, 21 Aug 2026 03:08:51 +0000 Date: Fri, 21 Aug 2026 04:08:49 +0100 From: Daniel Golle To: Greg Kroah-Hartman , "Rafael J. Wysocki" , Danilo Krummrich , Marcel Holtmann , Luiz Augusto von Dentz , Miri Korenblit , driver-core@lists.linux.dev, linux-kernel@vger.kernel.org, linux-bluetooth@vger.kernel.org, linux-wireless@vger.kernel.org Cc: Hans de Goede Subject: [PATCH v3 3/4] Bluetooth: hci_h5: use device_schedule_reprobe() Message-ID: <9b759e7d7fe751f3e5735404312d12140a0f23b8.1787281239.git.daniel@makrotopia.org> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" h5_btrtl_resume() open-codes a deferred re-probe for RTL devices that lose their firmware state over suspend: it takes a module reference, allocates a work item, and the work function calls device_reprobe() and then ends with put_device(); kfree(); module_put(THIS_MODULE); in module text. That final module_put() is racy: once the reference count is decremented a concurrent rmmod can free the module text before the work function's epilogue has finished executing. The work also does not synchronize against shutdown or unbind, so a stale re-probe could undo an administrative unbind or detach a device whose ->shutdown() callback has already run. Convert to the new device_schedule_reprobe() helper, whose work function is builtin text and which skips the re-probe when the device was removed, shutdown reached it, or it is no longer bound to the driver that scheduled the re-probe. The old worker suppressed its error message for -EPROBE_DEFER; the helper needs no equivalent because its attach half is device_attach(), which folds probe deferral into the deferred-probe machinery silently. Behavioral changes: - A pending re-probe no longer pins the module: rmmod with a re-probe pending now succeeds immediately and the re-probe becomes a no-op, instead of rmmod failing with EBUSY. - A re-probe scheduled before a system shutdown or before an administrative unbind no longer detaches and rebinds the device afterwards. - A re-probe racing the next suspend now detaches immediately while the probe is deferred until the following resume by the defer_all_probes machinery, instead of probing mid-suspend. Signed-off-by: Daniel Golle Tested-by: Hans de Goede Reviewed-by: Hans de Goede --- v3: picked up Hans de Goede's Tested-by/Reviewed-by v2: also correct the stale device_reprobe() reference in the h5_btrtl_open() comment v1: initial RFC drivers/bluetooth/hci_h5.c | 43 ++++++-------------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/drivers/bluetooth/hci_h5.c b/drivers/bluetooth/hci_h5.c index b1999e14aade..3fde1d5a5ae9 100644 --- a/drivers/bluetooth/hci_h5.c +++ b/drivers/bluetooth/hci_h5.c @@ -990,7 +990,7 @@ static int h5_btrtl_setup(struct h5 *h5) static void h5_btrtl_open(struct h5 *h5) { /* - * Since h5_btrtl_resume() does a device_reprobe() the suspend handling + * Since h5_btrtl_resume() schedules a device re-probe the suspend handli= ng * done by the hci_suspend_notifier is not necessary; it actually causes * delays and a bunch of errors to get logged, so disable it. */ @@ -1049,46 +1049,15 @@ static int h5_btrtl_suspend(struct h5 *h5) return 0; } =20 -struct h5_btrtl_reprobe { - struct device *dev; - struct work_struct work; -}; - -static void h5_btrtl_reprobe_worker(struct work_struct *work) -{ - struct h5_btrtl_reprobe *reprobe =3D - container_of(work, struct h5_btrtl_reprobe, work); - int ret; - - ret =3D device_reprobe(reprobe->dev); - if (ret && ret !=3D -EPROBE_DEFER) - dev_err(reprobe->dev, "Reprobe error %d\n", ret); - - put_device(reprobe->dev); - kfree(reprobe); - module_put(THIS_MODULE); -} - static int h5_btrtl_resume(struct h5 *h5) { - if (test_bit(H5_WAKEUP_DISABLE, &h5->flags)) { - struct h5_btrtl_reprobe *reprobe; - - reprobe =3D kzalloc_obj(*reprobe); - if (!reprobe) - return -ENOMEM; - - __module_get(THIS_MODULE); + if (test_bit(H5_WAKEUP_DISABLE, &h5->flags)) + return device_schedule_reprobe(&h5->hu->serdev->dev, 0); =20 - INIT_WORK(&reprobe->work, h5_btrtl_reprobe_worker); - reprobe->dev =3D get_device(&h5->hu->serdev->dev); - queue_work(system_long_wq, &reprobe->work); - } else { - gpiod_set_value_cansleep(h5->device_wake_gpio, 1); + gpiod_set_value_cansleep(h5->device_wake_gpio, 1); =20 - if (test_bit(H5_HW_FLOW_CONTROL, &h5->flags)) - serdev_device_set_flow_control(h5->hu->serdev, true); - } + if (test_bit(H5_HW_FLOW_CONTROL, &h5->flags)) + serdev_device_set_flow_control(h5->hu->serdev, true); =20 return 0; } --=20 2.55.0 From nobody Mon Sep 28 14:46:00 2026 Received: from pidgin.makrotopia.org (pidgin.makrotopia.org [185.142.180.65]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C8E673624C5; Fri, 21 Aug 2026 03:09:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=185.142.180.65 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281745; cv=none; b=CNKeaq8ec1GG1BigFx8C6wNo4sPeC4e0EZzejmWS3ETCK0qbfxtjHd+lbgFDMXQWrwCwidregyO8xmDZ9TnlTSOF9YxNdEn9bcgEUkRNv5p7X/8UPjMxVRw65fFpUTeKM3WzoMUhH/wSicFGTyRJK+g8jmcIyedzOjGl3Unt8vI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787281745; c=relaxed/simple; bh=G+mG2/kDQLIbjH/ympSUi/zNtcvx2RTFCSQDiqJo8J4=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=cNvdDpJ1nhXU9KVCItfeXoc4oQEKtv9RHcWQfwFBbzPMFBv2Nut0FJz4VIXBxBe0JCdf0x5Dzwnx3ImcQbRoxT+1qeHM+nFbMvYUHz6FX+JtjmPTjB8XDyEBMTUAOw4RtAKg4bvmlvxQuCNLAoK0EDM9KBKNvwe0tWYybZ3PiV0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org; spf=pass smtp.mailfrom=makrotopia.org; arc=none smtp.client-ip=185.142.180.65 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=makrotopia.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=makrotopia.org Received: from local by pidgin.makrotopia.org with esmtpsa (TLS1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.99) (envelope-from ) id 1wxFcr-000000005Uf-17pl; Fri, 21 Aug 2026 03:09:01 +0000 Date: Fri, 21 Aug 2026 04:08:58 +0100 From: Daniel Golle To: Greg Kroah-Hartman , "Rafael J. Wysocki" , Danilo Krummrich , Marcel Holtmann , Luiz Augusto von Dentz , Miri Korenblit , driver-core@lists.linux.dev, linux-kernel@vger.kernel.org, linux-bluetooth@vger.kernel.org, linux-wireless@vger.kernel.org Cc: Hans de Goede Subject: [PATCH v3 4/4] Bluetooth: btintel_pcie: use device_schedule_reprobe() after reset Message-ID: <3f190c8e1644d0b9c86c6c0d2bfca952b7eb7026.1787281239.git.daniel@makrotopia.org> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" btintel_pcie re-probes its own device synchronously from its own reset work via device_reprobe(), both after an FLR and after an ACPI PLDR. The synchronous call runs .remove() from inside the reset work, which requires a hand-rolled correctness contract spanning several comments: .remove() must skip draining the very reset work it is running from (the current_work() check), the reset must use pci_try_reset_function() to dodge a device_lock ABBA against .remove(), and both reset paths must not touch 'data' after the reprobe call because .remove() has freed it. Convert the two BT self re-probes to device_schedule_reprobe(). The driver core's builtin work item now triggers .remove(), never the reset work itself, so the current_work() check in .remove() is dead and is removed: disable_work_sync(&data->reset_work) now also guarantees the reset work has fully returned before 'data' is freed. The Wi-Fi sibling re-probe in the PLDR path is left untouched; the sibling is a different device re-probed from a bounded context and has neither of the bug classes the helper addresses. Safety of the window between the reset work returning and the deferred detach running, during which 'data' now stays alive: - hci callbacks: send_frame fails with -ENODEV while BTINTEL_PCIE_RECOVERY_IN_PROGRESS is set, and that bit is only cleared by a fresh probe. New reset requests coalesce into the in-flight one via the same bit. open/close are no-ops. - interrupts: the reset work masks all interrupt causes and synchronizes the IRQs before the reset, the dump workers stay disabled (disable count >=3D 1) until .remove(), rx_work is flushed, and the freshly reset device raises no traffic until the next probe re-initialises it. The same exposure already exists today in the window between pci_try_reset_function() and the synchronous re-probe; the conversion only lengthens it. - work disable counts: the success-path contract is unchanged in substance. The reset work's disable_work_sync() calls stay unbalanced on success, .remove() disables again, and the fresh probe re-INIT_WORKs the dump workers with disable count 0. pci_lock_rescan_remove() now only covers the reset itself, no longer the re-probe. Hot-removal between the reset and the deferred re-probe is handled by the helper's dead-device check. If scheduling the re-probe fails in the FLR path, the error is returned so the reset work re-enables the dump workers, matching the existing FLR failure handling; unlike before, 'data' is still alive in that case. The PLDR path keeps logging only, as before. Signed-off-by: Daniel Golle --- v3: rebased onto bluetooth-next, whose btintel_pcie has four dump workers (coredump/hwexp/fwtrigger/mbox) rather than the single one the mainline snapshot v2 targeted v2: reconciled to the current tree and adapted the commit message v1: initial RFC drivers/bluetooth/btintel_pcie.c | 49 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/drivers/bluetooth/btintel_pcie.c b/drivers/bluetooth/btintel_p= cie.c index baa621b3fef9..29be05d41429 100644 --- a/drivers/bluetooth/btintel_pcie.c +++ b/drivers/bluetooth/btintel_pcie.c @@ -3012,7 +3012,7 @@ static void btintel_pcie_perform_pldr(struct btintel_= pcie_data *data) * BT needs pci_save_state()/pci_restore_state() because the BT driver * is still partially attached when the _PRR runs (it hasn't been unbound= yet). * The PCI device needs to remain minimally functional so that - * device_reprobe(&pdev->dev) can work afterward + * the deferred re-probe of the BT device can work afterward */ ret =3D btintel_pcie_acpi_reset_method(data); =20 @@ -3023,14 +3023,16 @@ static void btintel_pcie_perform_pldr(struct btinte= l_pcie_data *data) } =20 if (!ret) { - if (device_reprobe(&pdev->dev)) - BT_ERR("BT reprobe failed for BDF:%s", pci_name(pdev)); + if (device_schedule_reprobe(&pdev->dev, 0)) + BT_ERR("BT reprobe scheduling failed for BDF:%s", + pci_name(pdev)); } } =20 /* - * Issue a Function Level Reset and hand teardown/re-init off to the PCI - * core via device_reprobe(), mirroring the PLDR path's contract. + * Issue a Function Level Reset and hand teardown/re-init off to the + * driver core via device_schedule_reprobe(), mirroring the PLDR path's + * contract. * * Caller must hold pci_lock_rescan_remove() and must have already * disabled interrupts and drained both rx_work and coredump_work. @@ -3052,14 +3054,12 @@ static int btintel_pcie_perform_flr(struct btintel_= pcie_data *data) return err; } =20 - /* device_reprobe() always detaches the driver first (running - * .remove(), which frees 'data'); any re-probe failure leaves the - * device unbound but 'data' is already gone, so just log it. - */ - if (device_reprobe(&pdev->dev)) - BT_ERR("BT reprobe failed for BDF:%s", pci_name(pdev)); + err =3D device_schedule_reprobe(&pdev->dev, 0); + if (err) + BT_ERR("BT reprobe scheduling failed for BDF:%s", + pci_name(pdev)); =20 - return 0; + return err; } =20 static void btintel_pcie_reset_work(struct work_struct *wk) @@ -3090,11 +3090,15 @@ static void btintel_pcie_reset_work(struct work_str= uct *wk) =20 bt_dev_dbg(data->hdev, "Release bluetooth interface"); =20 - /* Both reset paths follow the same contract: on success they - * destroy 'data' via device_reprobe() (a fresh probe re-INIT_WORKs - * the dump workers with disable count 0), so enable_work() must - * NOT be called on the success path. Only the FLR path can fail - * with 'data' still alive, in which case we balance the + /* Both reset paths follow the same contract: on success the + * deferred re-probe scheduled with device_schedule_reprobe() + * destroys 'data' by re-running .probe() (which re-INIT_WORKs the + * dump workers with disable count 0), so enable_work() must NOT be + * called on the success path. 'data' stays alive until the deferred + * detach runs; in this window new activity is fenced by + * BTINTEL_PCIE_RECOVERY_IN_PROGRESS, the masked interrupts and the + * disabled dump workers. Only the FLR path can fail with no + * re-probe scheduled, in which case we balance the * disable_work_sync() calls above so a later successful reset is * not permanently blocked. * @@ -3460,13 +3464,12 @@ static void btintel_pcie_remove(struct pci_dev *pde= v) disable_work_sync(&data->fwtrigger_work); disable_work_sync(&data->mbox_work); =20 - /* Cancel pending reset work. Skip only when remove() is called from - * within the reset work itself (PLDR device_reprobe path) to avoid - * deadlock. current_work() returns the work_struct of the caller if - * we are in a workqueue context. + /* The deferred re-probe triggers .remove() from the driver core's + * work item, never from reset_work itself, so this no longer runs + * nested in reset_work; disable_work_sync() also guarantees the + * reset work has fully returned before 'data' is freed. */ - if (current_work() !=3D &data->reset_work) - disable_work_sync(&data->reset_work); + disable_work_sync(&data->reset_work); =20 btintel_pcie_disable_interrupts(data); =20 --=20 2.55.0