[PATCH v2 4/4] Bluetooth: btintel_pcie: use device_schedule_reprobe() after reset

From: Daniel Golle

Date: Wed Aug 19 2026 - 20:33:35 EST


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 coredump worker stays
disabled (disable count >= 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() call stays
unbalanced on success, .remove() disables again, and the fresh
probe re-INIT_WORKs the coredump worker 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 coredump worker, 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 <daniel@xxxxxxxxxxxxxx>
---
drivers/bluetooth/btintel_pcie.c | 51 +++++++++++++++++---------------
1 file changed, 27 insertions(+), 24 deletions(-)

diff --git a/drivers/bluetooth/btintel_pcie.c b/drivers/bluetooth/btintel_pcie.c
index 2b7231be5973..2bd318f0d36c 100644
--- a/drivers/bluetooth/btintel_pcie.c
+++ b/drivers/bluetooth/btintel_pcie.c
@@ -2582,7 +2582,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 = btintel_pcie_acpi_reset_method(data);

@@ -2593,14 +2593,16 @@ static void btintel_pcie_perform_pldr(struct btintel_pcie_data *data)
}

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));
}
}

/*
- * 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.
@@ -2622,14 +2624,12 @@ static int btintel_pcie_perform_flr(struct btintel_pcie_data *data)
return err;
}

- /* 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 = device_schedule_reprobe(&pdev->dev, 0);
+ if (err)
+ BT_ERR("BT reprobe scheduling failed for BDF:%s",
+ pci_name(pdev));

- return 0;
+ return err;
}

static void btintel_pcie_reset_work(struct work_struct *wk)
@@ -2657,11 +2657,15 @@ static void btintel_pcie_reset_work(struct work_struct *wk)

bt_dev_dbg(data->hdev, "Release bluetooth interface");

- /* Both reset paths follow the same contract: on success they
- * destroy 'data' via device_reprobe() (a fresh probe re-INIT_WORKs
- * the coredump_work 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
+ * coredump_work 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 coredump_work. Only the FLR path can fail with no
+ * re-probe scheduled, in which case we balance the
* disable_work_sync() above so a later successful reset is not
* permanently blocked.
*
@@ -2941,19 +2945,18 @@ static void btintel_pcie_remove(struct pci_dev *pdev)
}

/* Permanently block coredump triggers and drain the worker before
- * tearing down. Must run before cancel_work_sync(&reset_work) so
+ * tearing down. Must run before disable_work_sync(&reset_work) so
* the disable counter stays >= 1 even after reset_work()'s
* balanced enable_work() (counter 2 -> 1, never reaching 0).
*/
disable_work_sync(&data->coredump_work);

- /* 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() != &data->reset_work)
- disable_work_sync(&data->reset_work);
+ disable_work_sync(&data->reset_work);

btintel_pcie_disable_interrupts(data);

--
2.55.0