* [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 13:33 ` Loic Poulain
2026-10-21 9:09 ` [PATCH ath-next v2 2/8] wifi: ath10k: wake blocked waiters when entering WEDGED state Kang Yang
` (6 subsequent siblings)
7 siblings, 1 reply; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Recovery monitoring runs on workqueue_aux, which is also
used by SDIO RX dispatch and WMI SERVICE_READY handling.
A single firmware crash may generate multiple recovery
triggers. Recovery detection currently handles each one by
waiting for recovery completion on workqueue_aux. On SDIO,
this blocks the same workqueue needed to deliver recovery
traffic, preventing recovery from completing.
As a result, otherwise recoverable firmware crashes fail
with:
ctl_resp never came in (-110)
Treat duplicate recovery triggers as part of a single
recovery attempt by gating recovery startup in
ath10k_core_start_recovery() with an atomic_cmpxchg() on
pending_recovery, and by making the monitor path check the
elapsed time since the attempt started instead of blocking
on a completion.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Fixes: f35a07a4842a ("wifi: ath10k: move recovery check logic into a new work")
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 64 +++++++++++++++++++---------------
drivers/net/wireless/ath/ath10k/core.h | 2 ++
2 files changed, 37 insertions(+), 29 deletions(-)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 7c2939cbde5f..07beefa06c4c 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2494,46 +2494,52 @@ static int ath10k_init_hw_params(struct ath10k *ar)
static void ath10k_core_recovery_check_work(struct work_struct *work)
{
struct ath10k *ar = container_of(work, struct ath10k, recovery_check_work);
- long time_left;
+ unsigned long recovery_start = READ_ONCE(ar->recovery_start_jiffies);
+ int fail_count;
- /* Sometimes the recovery will fail and then the next all recovery fail,
- * so avoid infinite recovery.
- */
- if (atomic_read(&ar->fail_cont_count) >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
- ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
- atomic_read(&ar->fail_cont_count));
- ar->state = ATH10K_STATE_WEDGED;
+ if (ar->state == ATH10K_STATE_WEDGED)
return;
- }
- ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d", ++ar->recovery_count);
+ /*
+ * Pure monitor: only ath10k_core_start_recovery() starts a fresh
+ * cycle. Bail if there is nothing in flight.
+ */
+ if (!atomic_read(&ar->pending_recovery))
+ return;
- if (atomic_read(&ar->pending_recovery)) {
- /* Sometimes it happened another recovery work before the previous one
- * completed, then the second recovery work will destroy the previous
- * one, thus below is to avoid that.
- */
- time_left = wait_for_completion_timeout(&ar->driver_recovery,
- ATH10K_RECOVERY_TIMEOUT_HZ);
- if (time_left) {
- ath10k_warn(ar, "previous recovery succeeded, skip this!\n");
- return;
- }
+ if (!time_after(jiffies, recovery_start + ATH10K_RECOVERY_TIMEOUT_HZ))
+ return;
- /* Record the continuous recovery fail count when recovery failed. */
- atomic_inc(&ar->fail_cont_count);
+ /* Record the continuous recovery fail count when recovery is late. */
+ fail_count = atomic_inc_return(&ar->fail_cont_count);
- /* Avoid having multiple recoveries at the same time. */
- return;
+ if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
+ ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
+ fail_count);
+ ar->state = ATH10K_STATE_WEDGED;
}
-
- atomic_inc(&ar->pending_recovery);
- queue_work(ar->workqueue, &ar->restart_work);
}
void ath10k_core_start_recovery(struct ath10k *ar)
{
- /* Use workqueue_aux to avoid blocking recovery tracking */
+ if (ar->state == ATH10K_STATE_WEDGED)
+ return;
+
+ /*
+ * SDIO raises several triggers within microseconds of one crash;
+ * only the first winner starts a fresh cycle and stamps the
+ * deadline. Losers do nothing while the owner runs recovery.
+ */
+ if (atomic_cmpxchg(&ar->pending_recovery, 0, 1) != 0)
+ return;
+ ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d",
+ ++ar->recovery_count);
+ /* Start a fresh failure budget; the last cycle may not have completed. */
+ atomic_set(&ar->fail_cont_count, 0);
+ WRITE_ONCE(ar->recovery_start_jiffies, jiffies);
+ queue_work(ar->workqueue, &ar->restart_work);
+
+ /* Start monitoring after publishing the recovery timestamp. */
queue_work(ar->workqueue_aux, &ar->recovery_check_work);
}
EXPORT_SYMBOL(ath10k_core_start_recovery);
diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
index dfee432615eb..c739fbadc8bb 100644
--- a/drivers/net/wireless/ath/ath10k/core.h
+++ b/drivers/net/wireless/ath/ath10k/core.h
@@ -1215,6 +1215,8 @@ struct ath10k {
unsigned int recovery_count;
/* continuous recovery fail count */
atomic_t fail_cont_count;
+ /* jiffies when the current recovery attempt started */
+ unsigned long recovery_start_jiffies;
/* cycle count is reported twice for each visited channel during scan.
* access protected by data_lock
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection
2026-10-21 9:09 ` [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection Kang Yang
@ 2026-10-21 13:33 ` Loic Poulain
2026-10-22 2:06 ` Kang Yang
0 siblings, 1 reply; 14+ messages in thread
From: Loic Poulain @ 2026-10-21 13:33 UTC (permalink / raw)
To: Kang Yang
Cc: Jeff Johnson, Baochen Qiang, Jeff Johnson, linux-wireless,
ath10k, linux-kernel
On Wed, Oct 21, 2026 at 11:09 AM Kang Yang <kang.yang@oss.qualcomm.com> wrote:
>
> Recovery monitoring runs on workqueue_aux, which is also
> used by SDIO RX dispatch and WMI SERVICE_READY handling.
>
> A single firmware crash may generate multiple recovery
> triggers. Recovery detection currently handles each one by
> waiting for recovery completion on workqueue_aux. On SDIO,
> this blocks the same workqueue needed to deliver recovery
> traffic, preventing recovery from completing.
>
> As a result, otherwise recoverable firmware crashes fail
> with:
> ctl_resp never came in (-110)
>
> Treat duplicate recovery triggers as part of a single
> recovery attempt by gating recovery startup in
> ath10k_core_start_recovery() with an atomic_cmpxchg() on
> pending_recovery, and by making the monitor path check the
> elapsed time since the attempt started instead of blocking
> on a completion.
>
> Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
> Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
>
> Fixes: f35a07a4842a ("wifi: ath10k: move recovery check logic into a new work")
> Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
> ---
> drivers/net/wireless/ath/ath10k/core.c | 64 +++++++++++++++++++---------------
> drivers/net/wireless/ath/ath10k/core.h | 2 ++
> 2 files changed, 37 insertions(+), 29 deletions(-)
>
> diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
> index 7c2939cbde5f..07beefa06c4c 100644
> --- a/drivers/net/wireless/ath/ath10k/core.c
> +++ b/drivers/net/wireless/ath/ath10k/core.c
> @@ -2494,46 +2494,52 @@ static int ath10k_init_hw_params(struct ath10k *ar)
> static void ath10k_core_recovery_check_work(struct work_struct *work)
> {
> struct ath10k *ar = container_of(work, struct ath10k, recovery_check_work);
> - long time_left;
> + unsigned long recovery_start = READ_ONCE(ar->recovery_start_jiffies);
> + int fail_count;
>
> - /* Sometimes the recovery will fail and then the next all recovery fail,
> - * so avoid infinite recovery.
> - */
> - if (atomic_read(&ar->fail_cont_count) >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
> - ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
> - atomic_read(&ar->fail_cont_count));
> - ar->state = ATH10K_STATE_WEDGED;
> + if (ar->state == ATH10K_STATE_WEDGED)
> return;
> - }
>
> - ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d", ++ar->recovery_count);
> + /*
> + * Pure monitor: only ath10k_core_start_recovery() starts a fresh
> + * cycle. Bail if there is nothing in flight.
> + */
> + if (!atomic_read(&ar->pending_recovery))
> + return;
>
> - if (atomic_read(&ar->pending_recovery)) {
> - /* Sometimes it happened another recovery work before the previous one
> - * completed, then the second recovery work will destroy the previous
> - * one, thus below is to avoid that.
> - */
> - time_left = wait_for_completion_timeout(&ar->driver_recovery,
> - ATH10K_RECOVERY_TIMEOUT_HZ);
> - if (time_left) {
> - ath10k_warn(ar, "previous recovery succeeded, skip this!\n");
> - return;
> - }
> + if (!time_after(jiffies, recovery_start + ATH10K_RECOVERY_TIMEOUT_HZ))
You can use time_is_after_jiffies() for this.
> + return;
>
> - /* Record the continuous recovery fail count when recovery failed. */
> - atomic_inc(&ar->fail_cont_count);
> + /* Record the continuous recovery fail count when recovery is late. */
> + fail_count = atomic_inc_return(&ar->fail_cont_count);
>
> - /* Avoid having multiple recoveries at the same time. */
> - return;
> + if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
> + ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
> + fail_count);
> + ar->state = ATH10K_STATE_WEDGED;
> }
> -
> - atomic_inc(&ar->pending_recovery);
> - queue_work(ar->workqueue, &ar->restart_work);
So we are not re-arming anymore for additional tries? or I miss something.
> }
>
> void ath10k_core_start_recovery(struct ath10k *ar)
> {
> - /* Use workqueue_aux to avoid blocking recovery tracking */
> + if (ar->state == ATH10K_STATE_WEDGED)
> + return;
> +
> + /*
> + * SDIO raises several triggers within microseconds of one crash;
> + * only the first winner starts a fresh cycle and stamps the
> + * deadline. Losers do nothing while the owner runs recovery.
> + */
> + if (atomic_cmpxchg(&ar->pending_recovery, 0, 1) != 0)
> + return;
> + ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d",
> + ++ar->recovery_count);
> + /* Start a fresh failure budget; the last cycle may not have completed. */
> + atomic_set(&ar->fail_cont_count, 0);
> + WRITE_ONCE(ar->recovery_start_jiffies, jiffies);
> + queue_work(ar->workqueue, &ar->restart_work);
> +
> + /* Start monitoring after publishing the recovery timestamp. */
> queue_work(ar->workqueue_aux, &ar->recovery_check_work);
So now that the recovery check is no longer blocking, it seems to me
that this could be implemented as a simple timer (alternatively, a
delayed work) as it would avoid having to perform the timeout
calculation...
> }
> EXPORT_SYMBOL(ath10k_core_start_recovery);
> diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
> index dfee432615eb..c739fbadc8bb 100644
> --- a/drivers/net/wireless/ath/ath10k/core.h
> +++ b/drivers/net/wireless/ath/ath10k/core.h
> @@ -1215,6 +1215,8 @@ struct ath10k {
> unsigned int recovery_count;
> /* continuous recovery fail count */
> atomic_t fail_cont_count;
> + /* jiffies when the current recovery attempt started */
> + unsigned long recovery_start_jiffies;
>
> /* cycle count is reported twice for each visited channel during scan.
> * access protected by data_lock
>
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection
2026-10-21 13:33 ` Loic Poulain
@ 2026-10-22 2:06 ` Kang Yang
0 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-22 2:06 UTC (permalink / raw)
To: Loic Poulain
Cc: Jeff Johnson, Baochen Qiang, Jeff Johnson, linux-wireless,
ath10k, linux-kernel
On 10/21/2026 9:33 PM, Loic Poulain wrote:
> On Wed, Oct 21, 2026 at 11:09 AM Kang Yang <kang.yang@oss.qualcomm.com> wrote:
>>
>> Recovery monitoring runs on workqueue_aux, which is also
>> used by SDIO RX dispatch and WMI SERVICE_READY handling.
>>
>> A single firmware crash may generate multiple recovery
>> triggers. Recovery detection currently handles each one by
>> waiting for recovery completion on workqueue_aux. On SDIO,
>> this blocks the same workqueue needed to deliver recovery
>> traffic, preventing recovery from completing.
>>
>> As a result, otherwise recoverable firmware crashes fail
>> with:
>> ctl_resp never came in (-110)
>>
>> Treat duplicate recovery triggers as part of a single
>> recovery attempt by gating recovery startup in
>> ath10k_core_start_recovery() with an atomic_cmpxchg() on
>> pending_recovery, and by making the monitor path check the
>> elapsed time since the attempt started instead of blocking
>> on a completion.
>>
>> Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
>> Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
>>
>> Fixes: f35a07a4842a ("wifi: ath10k: move recovery check logic into a new work")
>> Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
>> ---
>> drivers/net/wireless/ath/ath10k/core.c | 64 +++++++++++++++++++---------------
>> drivers/net/wireless/ath/ath10k/core.h | 2 ++
>> 2 files changed, 37 insertions(+), 29 deletions(-)
>>
>> diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
>> index 7c2939cbde5f..07beefa06c4c 100644
>> --- a/drivers/net/wireless/ath/ath10k/core.c
>> +++ b/drivers/net/wireless/ath/ath10k/core.c
>> @@ -2494,46 +2494,52 @@ static int ath10k_init_hw_params(struct ath10k *ar)
>> static void ath10k_core_recovery_check_work(struct work_struct *work)
>> {
>> struct ath10k *ar = container_of(work, struct ath10k, recovery_check_work);
>> - long time_left;
>> + unsigned long recovery_start = READ_ONCE(ar->recovery_start_jiffies);
>> + int fail_count;
>>
>> - /* Sometimes the recovery will fail and then the next all recovery fail,
>> - * so avoid infinite recovery.
>> - */
>> - if (atomic_read(&ar->fail_cont_count) >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
>> - ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
>> - atomic_read(&ar->fail_cont_count));
>> - ar->state = ATH10K_STATE_WEDGED;
>> + if (ar->state == ATH10K_STATE_WEDGED)
>> return;
>> - }
>>
>> - ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d", ++ar->recovery_count);
>> + /*
>> + * Pure monitor: only ath10k_core_start_recovery() starts a fresh
>> + * cycle. Bail if there is nothing in flight.
>> + */
>> + if (!atomic_read(&ar->pending_recovery))
>> + return;
>>
>> - if (atomic_read(&ar->pending_recovery)) {
>> - /* Sometimes it happened another recovery work before the previous one
>> - * completed, then the second recovery work will destroy the previous
>> - * one, thus below is to avoid that.
>> - */
>> - time_left = wait_for_completion_timeout(&ar->driver_recovery,
>> - ATH10K_RECOVERY_TIMEOUT_HZ);
>> - if (time_left) {
>> - ath10k_warn(ar, "previous recovery succeeded, skip this!\n");
>> - return;
>> - }
>> + if (!time_after(jiffies, recovery_start + ATH10K_RECOVERY_TIMEOUT_HZ))
>
> You can use time_is_after_jiffies() for this.
>
>> + return;
>>
>> - /* Record the continuous recovery fail count when recovery failed. */
>> - atomic_inc(&ar->fail_cont_count);
>> + /* Record the continuous recovery fail count when recovery is late. */
>> + fail_count = atomic_inc_return(&ar->fail_cont_count);
>>
>> - /* Avoid having multiple recoveries at the same time. */
>> - return;
>> + if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
>> + ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
>> + fail_count);
>> + ar->state = ATH10K_STATE_WEDGED;
>> }
>> -
>> - atomic_inc(&ar->pending_recovery);
>> - queue_work(ar->workqueue, &ar->restart_work);
>
> So we are not re-arming anymore for additional tries? or I miss something.
>
>> }
>>
>> void ath10k_core_start_recovery(struct ath10k *ar)
>> {
>> - /* Use workqueue_aux to avoid blocking recovery tracking */
>> + if (ar->state == ATH10K_STATE_WEDGED)
>> + return;
>> +
>> + /*
>> + * SDIO raises several triggers within microseconds of one crash;
>> + * only the first winner starts a fresh cycle and stamps the
>> + * deadline. Losers do nothing while the owner runs recovery.
>> + */
>> + if (atomic_cmpxchg(&ar->pending_recovery, 0, 1) != 0)
>> + return;
>> + ath10k_dbg(ar, ATH10K_DBG_BOOT, "total recovery count: %d",
>> + ++ar->recovery_count);
>> + /* Start a fresh failure budget; the last cycle may not have completed. */
>> + atomic_set(&ar->fail_cont_count, 0);
>> + WRITE_ONCE(ar->recovery_start_jiffies, jiffies);
>> + queue_work(ar->workqueue, &ar->restart_work);
>> +
>> + /* Start monitoring after publishing the recovery timestamp. */
>> queue_work(ar->workqueue_aux, &ar->recovery_check_work);
>
> So now that the recovery check is no longer blocking, it seems to me
> that this could be implemented as a simple timer (alternatively, a
> delayed work) as it would avoid having to perform the timeout
> calculation...
>
Patch 5 will change it into delayed work, let me think if i need to
merge patch 5 into patch 1.
>> }
>> EXPORT_SYMBOL(ath10k_core_start_recovery);
>> diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
>> index dfee432615eb..c739fbadc8bb 100644
>> --- a/drivers/net/wireless/ath/ath10k/core.h
>> +++ b/drivers/net/wireless/ath/ath10k/core.h
>> @@ -1215,6 +1215,8 @@ struct ath10k {
>> unsigned int recovery_count;
>> /* continuous recovery fail count */
>> atomic_t fail_cont_count;
>> + /* jiffies when the current recovery attempt started */
>> + unsigned long recovery_start_jiffies;
>>
>> /* cycle count is reported twice for each visited channel during scan.
>> * access protected by data_lock
>>
>> --
>> 2.34.1
>>
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH ath-next v2 2/8] wifi: ath10k: wake blocked waiters when entering WEDGED state
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 3/8] wifi: ath10k: also wake remaining blocked waiters Kang Yang
` (5 subsequent siblings)
7 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Commit c256a94d1b1b ("wifi: ath10k: shutdown driver when
hardware is unreliable") added a fail-count based transition
to ATH10K_STATE_WEDGED after repeated recovery failures.
When recovery starts, ath10k_core_restart() sets
ATH10K_FLAG_CRASH_FLUSH and wakes waiters already blocked at
the time the failure is detected. However, additional waits
may be created later while the recovery attempt is still in
progress.
If recovery ultimately fails and the device transitions to
ATH10K_STATE_WEDGED, those later waiters continue sleeping
until their individual timeouts expire even though no further
progress is possible.
On suspend/resume, these extra wait timeouts consume watchdog
budget. Crash logs show cases where one or two such timeouts
are enough to cause the suspend watchdog to fire before the
driver finishes shutting down.
Apply the same CRASH_FLUSH, queue-stop and waiter wakeup
handling when entering ATH10K_STATE_WEDGED so blocked callers
fail immediately once the driver gives up.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Fixes: c256a94d1b1b ("wifi: ath10k: shutdown driver when hardware is unreliable")
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 43 +++++++++++++++++++++++-----------
1 file changed, 29 insertions(+), 14 deletions(-)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 07beefa06c4c..93140f1ffb7b 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2491,6 +2491,32 @@ static int ath10k_init_hw_params(struct ath10k *ar)
return 0;
}
+static void ath10k_core_wake_all_waiters(struct ath10k *ar)
+{
+ complete(&ar->scan.started);
+ complete(&ar->scan.completed);
+ complete(&ar->scan.on_channel);
+ complete(&ar->offchan_tx_completed);
+ complete(&ar->install_key_done);
+ complete(&ar->vdev_setup_done);
+ complete(&ar->vdev_delete_done);
+ complete(&ar->thermal.wmi_sync);
+ complete(&ar->bss_survey_done);
+ wake_up(&ar->htt.empty_tx_wq);
+ wake_up(&ar->wmi.tx_credits_wq);
+ wake_up(&ar->peer_mapping_wq);
+}
+
+static void ath10k_core_enter_wedged(struct ath10k *ar)
+{
+ ar->state = ATH10K_STATE_WEDGED;
+
+ set_bit(ATH10K_FLAG_CRASH_FLUSH, &ar->dev_flags);
+
+ ieee80211_stop_queues(ar->hw);
+ ath10k_core_wake_all_waiters(ar);
+}
+
static void ath10k_core_recovery_check_work(struct work_struct *work)
{
struct ath10k *ar = container_of(work, struct ath10k, recovery_check_work);
@@ -2516,7 +2542,7 @@ static void ath10k_core_recovery_check_work(struct work_struct *work)
if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
fail_count);
- ar->state = ATH10K_STATE_WEDGED;
+ ath10k_core_enter_wedged(ar);
}
}
@@ -2585,18 +2611,7 @@ static void ath10k_core_restart(struct work_struct *work)
ieee80211_stop_queues(ar->hw);
ath10k_drain_tx(ar);
- complete(&ar->scan.started);
- complete(&ar->scan.completed);
- complete(&ar->scan.on_channel);
- complete(&ar->offchan_tx_completed);
- complete(&ar->install_key_done);
- complete(&ar->vdev_setup_done);
- complete(&ar->vdev_delete_done);
- complete(&ar->thermal.wmi_sync);
- complete(&ar->bss_survey_done);
- wake_up(&ar->htt.empty_tx_wq);
- wake_up(&ar->wmi.tx_credits_wq);
- wake_up(&ar->peer_mapping_wq);
+ ath10k_core_wake_all_waiters(ar);
/* TODO: We can have one instance of cancelling coverage_class_work by
* moving it to ath10k_halt(), so that both stop() and restart() would
@@ -2624,7 +2639,7 @@ static void ath10k_core_restart(struct work_struct *work)
/* hw restart might be requested from multiple places */
break;
case ATH10K_STATE_RESTARTED:
- ar->state = ATH10K_STATE_WEDGED;
+ ath10k_core_enter_wedged(ar);
fallthrough;
case ATH10K_STATE_WEDGED:
ath10k_warn(ar, "device is wedged, will not restart\n");
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH ath-next v2 3/8] wifi: ath10k: also wake remaining blocked waiters
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 1/8] wifi: ath10k: fix SDIO aux workqueue starvation in recovery detection Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 2/8] wifi: ath10k: wake blocked waiters when entering WEDGED state Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 13:43 ` Loic Poulain
2026-10-21 9:09 ` [PATCH ath-next v2 4/8] wifi: ath10k: bound recovery time before entering WEDGED Kang Yang
` (4 subsequent siblings)
7 siblings, 1 reply; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
ath10k_core_wake_all_waiters() releases callers that would
otherwise remain blocked after the driver has given up on the
device.
However, peer_delete_done, target_suspend, wow.wakeup_completed,
peer_stats_info_complete and wmi.radar_confirm are not included
in that list. Callers waiting on any of them continue sleeping
until their individual timeouts expire, even though no further
progress can be made. The WoW and suspend waiters are on the
suspend/resume path, where these extra timeouts consume the
watchdog budget.
Add these completions to ath10k_core_wake_all_waiters() so
blocked callers are released immediately instead of waiting
for unnecessary timeout delays.
peer_delete_done is the only one of these that is never
reinitialized before its waiters run. A completion left over
from a wake during recovery would make the next peer delete
wait return immediately, so reinit it before each peer delete
command is sent.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 5 +++++
drivers/net/wireless/ath/ath10k/mac.c | 3 +++
2 files changed, 8 insertions(+)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 93140f1ffb7b..677e582b87e4 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2500,7 +2500,12 @@ static void ath10k_core_wake_all_waiters(struct ath10k *ar)
complete(&ar->install_key_done);
complete(&ar->vdev_setup_done);
complete(&ar->vdev_delete_done);
+ complete(&ar->peer_delete_done);
+ complete(&ar->target_suspend);
+ complete(&ar->wow.wakeup_completed);
+ complete(&ar->peer_stats_info_complete);
complete(&ar->thermal.wmi_sync);
+ complete(&ar->wmi.radar_confirm);
complete(&ar->bss_survey_done);
wake_up(&ar->htt.empty_tx_wq);
wake_up(&ar->wmi.tx_credits_wq);
diff --git a/drivers/net/wireless/ath/ath10k/mac.c b/drivers/net/wireless/ath/ath10k/mac.c
index 8e604697d6c2..45a3082d5a64 100644
--- a/drivers/net/wireless/ath/ath10k/mac.c
+++ b/drivers/net/wireless/ath/ath10k/mac.c
@@ -843,6 +843,7 @@ static int ath10k_peer_delete(struct ath10k *ar, u32 vdev_id, const u8 *addr)
lockdep_assert_held(&ar->conf_mutex);
+ reinit_completion(&ar->peer_delete_done);
ret = ath10k_wmi_peer_delete(ar, vdev_id, addr);
if (ret)
return ret;
@@ -5890,6 +5891,7 @@ static int ath10k_add_interface(struct ieee80211_hw *hw,
err_peer_delete:
if (arvif->vdev_type == WMI_VDEV_TYPE_AP ||
arvif->vdev_type == WMI_VDEV_TYPE_IBSS) {
+ reinit_completion(&ar->peer_delete_done);
ath10k_wmi_peer_delete(ar, arvif->vdev_id, vif->addr);
ath10k_wait_for_peer_delete_done(ar, arvif->vdev_id,
vif->addr);
@@ -5952,6 +5954,7 @@ static void ath10k_remove_interface(struct ieee80211_hw *hw,
if (arvif->vdev_type == WMI_VDEV_TYPE_AP ||
arvif->vdev_type == WMI_VDEV_TYPE_IBSS) {
+ reinit_completion(&ar->peer_delete_done);
ret = ath10k_wmi_peer_delete(arvif->ar, arvif->vdev_id,
vif->addr);
if (ret)
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH ath-next v2 3/8] wifi: ath10k: also wake remaining blocked waiters
2026-10-21 9:09 ` [PATCH ath-next v2 3/8] wifi: ath10k: also wake remaining blocked waiters Kang Yang
@ 2026-10-21 13:43 ` Loic Poulain
0 siblings, 0 replies; 14+ messages in thread
From: Loic Poulain @ 2026-10-21 13:43 UTC (permalink / raw)
To: Kang Yang
Cc: Jeff Johnson, Baochen Qiang, Jeff Johnson, linux-wireless,
ath10k, linux-kernel
On Wed, Oct 21, 2026 at 11:09 AM Kang Yang <kang.yang@oss.qualcomm.com> wrote:
>
> ath10k_core_wake_all_waiters() releases callers that would
> otherwise remain blocked after the driver has given up on the
> device.
>
> However, peer_delete_done, target_suspend, wow.wakeup_completed,
> peer_stats_info_complete and wmi.radar_confirm are not included
> in that list. Callers waiting on any of them continue sleeping
> until their individual timeouts expire, even though no further
> progress can be made. The WoW and suspend waiters are on the
> suspend/resume path, where these extra timeouts consume the
> watchdog budget.
>
> Add these completions to ath10k_core_wake_all_waiters() so
> blocked callers are released immediately instead of waiting
> for unnecessary timeout delays.
>
> peer_delete_done is the only one of these that is never
> reinitialized before its waiters run. A completion left over
> from a wake during recovery would make the next peer delete
> wait return immediately, so reinit it before each peer delete
> command is sent.
>
> Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
> Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
>
> Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
Reviewed-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
> ---
> drivers/net/wireless/ath/ath10k/core.c | 5 +++++
> drivers/net/wireless/ath/ath10k/mac.c | 3 +++
> 2 files changed, 8 insertions(+)
>
> diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
> index 93140f1ffb7b..677e582b87e4 100644
> --- a/drivers/net/wireless/ath/ath10k/core.c
> +++ b/drivers/net/wireless/ath/ath10k/core.c
> @@ -2500,7 +2500,12 @@ static void ath10k_core_wake_all_waiters(struct ath10k *ar)
> complete(&ar->install_key_done);
> complete(&ar->vdev_setup_done);
> complete(&ar->vdev_delete_done);
> + complete(&ar->peer_delete_done);
> + complete(&ar->target_suspend);
> + complete(&ar->wow.wakeup_completed);
> + complete(&ar->peer_stats_info_complete);
> complete(&ar->thermal.wmi_sync);
> + complete(&ar->wmi.radar_confirm);
> complete(&ar->bss_survey_done);
> wake_up(&ar->htt.empty_tx_wq);
> wake_up(&ar->wmi.tx_credits_wq);
> diff --git a/drivers/net/wireless/ath/ath10k/mac.c b/drivers/net/wireless/ath/ath10k/mac.c
> index 8e604697d6c2..45a3082d5a64 100644
> --- a/drivers/net/wireless/ath/ath10k/mac.c
> +++ b/drivers/net/wireless/ath/ath10k/mac.c
> @@ -843,6 +843,7 @@ static int ath10k_peer_delete(struct ath10k *ar, u32 vdev_id, const u8 *addr)
>
> lockdep_assert_held(&ar->conf_mutex);
>
> + reinit_completion(&ar->peer_delete_done);
> ret = ath10k_wmi_peer_delete(ar, vdev_id, addr);
> if (ret)
> return ret;
> @@ -5890,6 +5891,7 @@ static int ath10k_add_interface(struct ieee80211_hw *hw,
> err_peer_delete:
> if (arvif->vdev_type == WMI_VDEV_TYPE_AP ||
> arvif->vdev_type == WMI_VDEV_TYPE_IBSS) {
> + reinit_completion(&ar->peer_delete_done);
> ath10k_wmi_peer_delete(ar, arvif->vdev_id, vif->addr);
> ath10k_wait_for_peer_delete_done(ar, arvif->vdev_id,
> vif->addr);
> @@ -5952,6 +5954,7 @@ static void ath10k_remove_interface(struct ieee80211_hw *hw,
>
> if (arvif->vdev_type == WMI_VDEV_TYPE_AP ||
> arvif->vdev_type == WMI_VDEV_TYPE_IBSS) {
> + reinit_completion(&ar->peer_delete_done);
> ret = ath10k_wmi_peer_delete(arvif->ar, arvif->vdev_id,
> vif->addr);
> if (ret)
>
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH ath-next v2 4/8] wifi: ath10k: bound recovery time before entering WEDGED
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
` (2 preceding siblings ...)
2026-10-21 9:09 ` [PATCH ath-next v2 3/8] wifi: ath10k: also wake remaining blocked waiters Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 5/8] wifi: ath10k: check the recovery deadline on its own timeline Kang Yang
` (3 subsequent siblings)
7 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Commit c256a94d1b1b ("wifi: ath10k: shutdown driver when
hardware is unreliable") added fail_cont_count tracking and
declared the device wedged after a fixed number of recovery
failures.
This relies solely on the number of observed failures and
does not place any bound on the total time spent in recovery.
As a result, the time required to reach ATH10K_STATE_WEDGED
depends on how recovery failures are observed rather than on
a fixed recovery budget.
In practice, recovery-related delays can stretch the interval
between failures and allow recovery attempts to continue far
longer than intended before the driver finally gives up.
Add ATH10K_RECOVERY_DEADLINE_HZ (15 seconds) as an independent upper
bound for each recovery attempt and declare the device wedged once either
the deadline expires or fail_cont_count reaches
ATH10K_RECOVERY_MAX_FAIL_COUNT.
Also reduce ATH10K_RECOVERY_MAX_FAIL_COUNT from 4 to 2 so
repeated failures reach the WEDGED state sooner.
This keeps recovery time bounded and helps avoid suspend
watchdog timeouts on platforms with tighter watchdog limits.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Fixes: c256a94d1b1b ("wifi: ath10k: shutdown driver when hardware is unreliable")
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 8 +++++---
drivers/net/wireless/ath/ath10k/core.h | 8 +++++++-
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 677e582b87e4..25e9a8f0ec8a 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -15,6 +15,7 @@
#include <linux/ctype.h>
#include <linux/pm_qos.h>
#include <linux/nvmem-consumer.h>
+#include <linux/jiffies.h>
#include <asm/byteorder.h>
#include "core.h"
@@ -2544,9 +2545,10 @@ static void ath10k_core_recovery_check_work(struct work_struct *work)
/* Record the continuous recovery fail count when recovery is late. */
fail_count = atomic_inc_return(&ar->fail_cont_count);
- if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT) {
- ath10k_err(ar, "consecutive fail %d times, will shutdown driver!",
- fail_count);
+ if (fail_count >= ATH10K_RECOVERY_MAX_FAIL_COUNT ||
+ time_after(jiffies, recovery_start + ATH10K_RECOVERY_DEADLINE_HZ)) {
+ ath10k_err(ar, "recovery timeout %ums, fail count %d, shutting down driver!",
+ jiffies_to_msecs(jiffies - recovery_start), fail_count);
ath10k_core_enter_wedged(ar);
}
}
diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
index c739fbadc8bb..0319da3ec022 100644
--- a/drivers/net/wireless/ath/ath10k/core.h
+++ b/drivers/net/wireless/ath/ath10k/core.h
@@ -88,7 +88,13 @@
#define ATH10K_ITER_RESUME_FLAGS (IEEE80211_IFACE_ITER_RESUME_ALL |\
IEEE80211_IFACE_SKIP_SDATA_NOT_IN_DRIVER)
#define ATH10K_RECOVERY_TIMEOUT_HZ (5 * HZ)
-#define ATH10K_RECOVERY_MAX_FAIL_COUNT 4
+#define ATH10K_RECOVERY_MAX_FAIL_COUNT 2
+/*
+ * Some platforms configure suspend watchdogs as low as 25s; cap the
+ * total recovery time well under that so the WEDGED transition and
+ * any teardown that follows still have room to run before it fires.
+ */
+#define ATH10K_RECOVERY_DEADLINE_HZ (15 * HZ)
struct ath10k;
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH ath-next v2 5/8] wifi: ath10k: check the recovery deadline on its own timeline
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
` (3 preceding siblings ...)
2026-10-21 9:09 ` [PATCH ath-next v2 4/8] wifi: ath10k: bound recovery time before entering WEDGED Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state Kang Yang
` (2 subsequent siblings)
7 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Recovery timeout enforcement currently advances only when
new recovery triggers arrive.
In practice, the interval between triggers is not bounded.
Additional timeout handling, workqueue congestion, or other
recovery-related activities may delay subsequent triggers,
causing the effective recovery timeout to extend well beyond
the intended ATH10K_RECOVERY_DEADLINE_HZ limit.
If recovery becomes stuck and no further trigger is generated,
deadline enforcement may stop progressing entirely.
Convert recovery_check_work() into self-rearming delayed_work
so timeout monitoring follows its own timeline while recovery
is in progress, independent of future recovery triggers.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 18 ++++++++++--------
drivers/net/wireless/ath/ath10k/core.h | 2 +-
drivers/net/wireless/ath/ath10k/mac.c | 5 ++++-
3 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 25e9a8f0ec8a..6b6b80242cd1 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2525,7 +2525,8 @@ static void ath10k_core_enter_wedged(struct ath10k *ar)
static void ath10k_core_recovery_check_work(struct work_struct *work)
{
- struct ath10k *ar = container_of(work, struct ath10k, recovery_check_work);
+ struct ath10k *ar = container_of(work, struct ath10k,
+ recovery_check_work.work);
unsigned long recovery_start = READ_ONCE(ar->recovery_start_jiffies);
int fail_count;
@@ -2539,9 +2540,6 @@ static void ath10k_core_recovery_check_work(struct work_struct *work)
if (!atomic_read(&ar->pending_recovery))
return;
- if (!time_after(jiffies, recovery_start + ATH10K_RECOVERY_TIMEOUT_HZ))
- return;
-
/* Record the continuous recovery fail count when recovery is late. */
fail_count = atomic_inc_return(&ar->fail_cont_count);
@@ -2550,7 +2548,12 @@ static void ath10k_core_recovery_check_work(struct work_struct *work)
ath10k_err(ar, "recovery timeout %ums, fail count %d, shutting down driver!",
jiffies_to_msecs(jiffies - recovery_start), fail_count);
ath10k_core_enter_wedged(ar);
+ return;
}
+
+ /* Keep checking on our own even if no further trigger arrives. */
+ queue_delayed_work(ar->workqueue_aux, &ar->recovery_check_work,
+ ATH10K_RECOVERY_TIMEOUT_HZ);
}
void ath10k_core_start_recovery(struct ath10k *ar)
@@ -2571,9 +2574,8 @@ void ath10k_core_start_recovery(struct ath10k *ar)
atomic_set(&ar->fail_cont_count, 0);
WRITE_ONCE(ar->recovery_start_jiffies, jiffies);
queue_work(ar->workqueue, &ar->restart_work);
-
- /* Start monitoring after publishing the recovery timestamp. */
- queue_work(ar->workqueue_aux, &ar->recovery_check_work);
+ queue_delayed_work(ar->workqueue_aux, &ar->recovery_check_work,
+ ATH10K_RECOVERY_TIMEOUT_HZ);
}
EXPORT_SYMBOL(ath10k_core_start_recovery);
@@ -3755,7 +3757,7 @@ struct ath10k *ath10k_core_create(size_t priv_size, struct device *dev,
INIT_WORK(&ar->register_work, ath10k_core_register_work);
INIT_WORK(&ar->restart_work, ath10k_core_restart);
- INIT_WORK(&ar->recovery_check_work, ath10k_core_recovery_check_work);
+ INIT_DELAYED_WORK(&ar->recovery_check_work, ath10k_core_recovery_check_work);
INIT_WORK(&ar->set_coverage_class_work,
ath10k_core_set_coverage_class_work);
diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
index 0319da3ec022..a1ded2cba103 100644
--- a/drivers/net/wireless/ath/ath10k/core.h
+++ b/drivers/net/wireless/ath/ath10k/core.h
@@ -1213,7 +1213,7 @@ struct ath10k {
struct work_struct register_work;
struct work_struct restart_work;
- struct work_struct recovery_check_work;
+ struct delayed_work recovery_check_work;
struct work_struct bundle_tx_work;
struct work_struct tx_complete_work;
diff --git a/drivers/net/wireless/ath/ath10k/mac.c b/drivers/net/wireless/ath/ath10k/mac.c
index 45a3082d5a64..693b935536c2 100644
--- a/drivers/net/wireless/ath/ath10k/mac.c
+++ b/drivers/net/wireless/ath/ath10k/mac.c
@@ -5428,7 +5428,7 @@ static void ath10k_stop(struct ieee80211_hw *hw, bool suspend)
cancel_work_sync(&ar->set_coverage_class_work);
cancel_delayed_work_sync(&ar->scan.timeout);
cancel_work_sync(&ar->restart_work);
- cancel_work_sync(&ar->recovery_check_work);
+ cancel_delayed_work_sync(&ar->recovery_check_work);
}
static int ath10k_config_ps(struct ath10k *ar)
@@ -8173,6 +8173,9 @@ static void ath10k_reconfig_complete(struct ieee80211_hw *hw,
ar->state = ATH10K_STATE_ON;
ieee80211_wake_queues(ar->hw);
+ /* Cancel the monitor first so it cannot bump the count after reset. */
+ cancel_delayed_work_sync(&ar->recovery_check_work);
+
/* Clear recovery state. */
complete(&ar->driver_recovery);
atomic_set(&ar->fail_cont_count, 0);
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
` (4 preceding siblings ...)
2026-10-21 9:09 ` [PATCH ath-next v2 5/8] wifi: ath10k: check the recovery deadline on its own timeline Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 13:54 ` Loic Poulain
2026-10-21 9:09 ` [PATCH ath-next v2 7/8] wifi: ath10k: remove now-unused driver_recovery completion Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 8/8] wifi: ath10k: trigger recovery on PCIe MMIO wake-up failures Kang Yang
7 siblings, 1 reply; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Once ath10k reaches ATH10K_STATE_WEDGED, the device can no
longer recover and remains unusable until the driver is
reloaded.
Userspace daemons may continue issuing scan and connect
requests, which the driver can only reject individually.
Mark the radio as hardware-blocked through rfkill when
entering the WEDGED state so userspace stops retrying
operations against a device that can no longer operate.
Both WEDGED transitions already route through
ath10k_core_enter_wedged(), so adding the call there is
enough to cover them consistently.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 6b6b80242cd1..66585cfa6d96 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2521,6 +2521,14 @@ static void ath10k_core_enter_wedged(struct ath10k *ar)
ieee80211_stop_queues(ar->hw);
ath10k_core_wake_all_waiters(ar);
+
+ /*
+ * Signal the radio as hard-blocked so userspace (e.g.
+ * wpa_supplicant, NetworkManager) stops looping scan and connect
+ * requests at a device the driver has already given up on. The
+ * state is cleared only when the driver is reloaded.
+ */
+ wiphy_rfkill_set_hw_state(ar->hw->wiphy, true);
}
static void ath10k_core_recovery_check_work(struct work_struct *work)
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* Re: [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state
2026-10-21 9:09 ` [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state Kang Yang
@ 2026-10-21 13:54 ` Loic Poulain
2026-10-22 1:55 ` Kang Yang
0 siblings, 1 reply; 14+ messages in thread
From: Loic Poulain @ 2026-10-21 13:54 UTC (permalink / raw)
To: Kang Yang
Cc: Jeff Johnson, Baochen Qiang, Jeff Johnson, linux-wireless,
ath10k, linux-kernel
On Wed, Oct 21, 2026 at 11:10 AM Kang Yang <kang.yang@oss.qualcomm.com> wrote:
>
> Once ath10k reaches ATH10K_STATE_WEDGED, the device can no
> longer recover and remains unusable until the driver is
> reloaded.
>
> Userspace daemons may continue issuing scan and connect
> requests, which the driver can only reject individually.
>
> Mark the radio as hardware-blocked through rfkill when
> entering the WEDGED state so userspace stops retrying
> operations against a device that can no longer operate.
>
> Both WEDGED transitions already route through
> ath10k_core_enter_wedged(), so adding the call there is
> enough to cover them consistently.
>
> Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
> Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
>
> Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
> ---
> drivers/net/wireless/ath/ath10k/core.c | 8 ++++++++
> 1 file changed, 8 insertions(+)
>
> diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
> index 6b6b80242cd1..66585cfa6d96 100644
> --- a/drivers/net/wireless/ath/ath10k/core.c
> +++ b/drivers/net/wireless/ath/ath10k/core.c
> @@ -2521,6 +2521,14 @@ static void ath10k_core_enter_wedged(struct ath10k *ar)
>
> ieee80211_stop_queues(ar->hw);
> ath10k_core_wake_all_waiters(ar);
> +
> + /*
> + * Signal the radio as hard-blocked so userspace (e.g.
> + * wpa_supplicant, NetworkManager) stops looping scan and connect
> + * requests at a device the driver has already given up on. The
> + * state is cleared only when the driver is reloaded.
> + */
> + wiphy_rfkill_set_hw_state(ar->hw->wiphy, true);
Using rfkill to signal the wedged state feels like an abuse of the
interface. Have you seen similar handling in other wireless drivers?
Wouldn't wiphy_unregister() be more appropriate here?
> }
>
> static void ath10k_core_recovery_check_work(struct work_struct *work)
>
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state
2026-10-21 13:54 ` Loic Poulain
@ 2026-10-22 1:55 ` Kang Yang
0 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-22 1:55 UTC (permalink / raw)
To: Loic Poulain
Cc: Jeff Johnson, Baochen Qiang, Jeff Johnson, linux-wireless,
ath10k, linux-kernel
On 10/21/2026 9:54 PM, Loic Poulain wrote:
> On Wed, Oct 21, 2026 at 11:10 AM Kang Yang <kang.yang@oss.qualcomm.com> wrote:
>>
>> Once ath10k reaches ATH10K_STATE_WEDGED, the device can no
>> longer recover and remains unusable until the driver is
>> reloaded.
>>
>> Userspace daemons may continue issuing scan and connect
>> requests, which the driver can only reject individually.
>>
>> Mark the radio as hardware-blocked through rfkill when
>> entering the WEDGED state so userspace stops retrying
>> operations against a device that can no longer operate.
>>
>> Both WEDGED transitions already route through
>> ath10k_core_enter_wedged(), so adding the call there is
>> enough to cover them consistently.
>>
>> Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
>> Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
>>
>> Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
>> ---
>> drivers/net/wireless/ath/ath10k/core.c | 8 ++++++++
>> 1 file changed, 8 insertions(+)
>>
>> diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
>> index 6b6b80242cd1..66585cfa6d96 100644
>> --- a/drivers/net/wireless/ath/ath10k/core.c
>> +++ b/drivers/net/wireless/ath/ath10k/core.c
>> @@ -2521,6 +2521,14 @@ static void ath10k_core_enter_wedged(struct ath10k *ar)
>>
>> ieee80211_stop_queues(ar->hw);
>> ath10k_core_wake_all_waiters(ar);
>> +
>> + /*
>> + * Signal the radio as hard-blocked so userspace (e.g.
>> + * wpa_supplicant, NetworkManager) stops looping scan and connect
>> + * requests at a device the driver has already given up on. The
>> + * state is cleared only when the driver is reloaded.
>> + */
>> + wiphy_rfkill_set_hw_state(ar->hw->wiphy, true);
>
> Using rfkill to signal the wedged state feels like an abuse of the
> interface. Have you seen similar handling in other wireless drivers?
> Wouldn't wiphy_unregister() be more appropriate here?
You can grep with 'wiphy_rfkill_set_hw_state' under
'drivers/net/wireless' and you will see dozens of related calls.
We have tried wiphy_unregister() during internal review, which will
generate a lot of call stacks(more than 20). wiphy_unregister() will
call into ath10k to perform many operations, but at this point, the
ath10k driver is no longer able to perform any behavior properly.
>
>> }
>>
>> static void ath10k_core_recovery_check_work(struct work_struct *work)
>>
>> --
>> 2.34.1
>>
^ permalink raw reply [flat|nested] 14+ messages in thread
* [PATCH ath-next v2 7/8] wifi: ath10k: remove now-unused driver_recovery completion
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
` (5 preceding siblings ...)
2026-10-21 9:09 ` [PATCH ath-next v2 6/8] wifi: ath10k: hard-block the radio when entering WEDGED state Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
2026-10-21 9:09 ` [PATCH ath-next v2 8/8] wifi: ath10k: trigger recovery on PCIe MMIO wake-up failures Kang Yang
7 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Previous patch removes the recovery monitor's dependency on
driver_recovery, leaving SNOC removal as the only
remaining user.
Replace the SNOC teardown synchronization with
cancel_work_sync() and cancel_delayed_work_sync(),
which already provide the required guarantee that
recovery work has stopped before resources are freed.
Remove the now-unused driver_recovery completion and
associated initialization and cleanup code.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/core.c | 3 ---
drivers/net/wireless/ath/ath10k/core.h | 1 -
drivers/net/wireless/ath/ath10k/mac.c | 1 -
drivers/net/wireless/ath/ath10k/snoc.c | 8 ++++----
4 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/drivers/net/wireless/ath/ath10k/core.c b/drivers/net/wireless/ath/ath10k/core.c
index 66585cfa6d96..7692e1a8b2e6 100644
--- a/drivers/net/wireless/ath/ath10k/core.c
+++ b/drivers/net/wireless/ath/ath10k/core.c
@@ -2617,8 +2617,6 @@ static void ath10k_core_restart(struct work_struct *work)
struct ath10k *ar = container_of(work, struct ath10k, restart_work);
int ret;
- reinit_completion(&ar->driver_recovery);
-
set_bit(ATH10K_FLAG_CRASH_FLUSH, &ar->dev_flags);
/* Place a barrier to make sure the compiler doesn't reorder
@@ -3716,7 +3714,6 @@ struct ath10k *ath10k_core_create(size_t priv_size, struct device *dev,
init_completion(&ar->scan.completed);
init_completion(&ar->scan.on_channel);
init_completion(&ar->target_suspend);
- init_completion(&ar->driver_recovery);
init_completion(&ar->wow.wakeup_completed);
init_completion(&ar->install_key_done);
diff --git a/drivers/net/wireless/ath/ath10k/core.h b/drivers/net/wireless/ath/ath10k/core.h
index a1ded2cba103..7c961308d7b9 100644
--- a/drivers/net/wireless/ath/ath10k/core.h
+++ b/drivers/net/wireless/ath/ath10k/core.h
@@ -1067,7 +1067,6 @@ struct ath10k {
} hif;
struct completion target_suspend;
- struct completion driver_recovery;
const struct ath10k_hw_regs *regs;
const struct ath10k_hw_ce_regs *hw_ce_regs;
diff --git a/drivers/net/wireless/ath/ath10k/mac.c b/drivers/net/wireless/ath/ath10k/mac.c
index 693b935536c2..5f828704f8ef 100644
--- a/drivers/net/wireless/ath/ath10k/mac.c
+++ b/drivers/net/wireless/ath/ath10k/mac.c
@@ -8177,7 +8177,6 @@ static void ath10k_reconfig_complete(struct ieee80211_hw *hw,
cancel_delayed_work_sync(&ar->recovery_check_work);
/* Clear recovery state. */
- complete(&ar->driver_recovery);
atomic_set(&ar->fail_cont_count, 0);
atomic_set(&ar->pending_recovery, 0);
diff --git a/drivers/net/wireless/ath/ath10k/snoc.c b/drivers/net/wireless/ath/ath10k/snoc.c
index 8f171ab6f4de..1ec58ec1029b 100644
--- a/drivers/net/wireless/ath/ath10k/snoc.c
+++ b/drivers/net/wireless/ath/ath10k/snoc.c
@@ -1916,10 +1916,10 @@ static void ath10k_snoc_remove(struct platform_device *pdev)
ath10k_dbg(ar, ATH10K_DBG_SNOC, "snoc remove\n");
- reinit_completion(&ar->driver_recovery);
-
- if (test_bit(ATH10K_SNOC_FLAG_RECOVERY, &ar_snoc->flags))
- wait_for_completion_timeout(&ar->driver_recovery, 3 * HZ);
+ if (test_bit(ATH10K_SNOC_FLAG_RECOVERY, &ar_snoc->flags)) {
+ cancel_work_sync(&ar->restart_work);
+ cancel_delayed_work_sync(&ar->recovery_check_work);
+ }
ath10k_snoc_free_resources(ar);
}
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread* [PATCH ath-next v2 8/8] wifi: ath10k: trigger recovery on PCIe MMIO wake-up failures
2026-10-21 9:09 [PATCH ath-next v2 0/8] wifi: ath10k: fix recovery and WEDGED failure handling Kang Yang
` (6 preceding siblings ...)
2026-10-21 9:09 ` [PATCH ath-next v2 7/8] wifi: ath10k: remove now-unused driver_recovery completion Kang Yang
@ 2026-10-21 9:09 ` Kang Yang
7 siblings, 0 replies; 14+ messages in thread
From: Kang Yang @ 2026-10-21 9:09 UTC (permalink / raw)
To: Kang Yang, Jeff Johnson, Baochen Qiang, Loic Poulain
Cc: Jeff Johnson, linux-wireless, ath10k, linux-kernel
Commit 77258d409ce4 ("ath10k: enable pci soc powersaving")
introduced MMIO wake-up handling through ath10k_pci_wake().
Current code only logs MMIO wake-up failures and returns.
This becomes a problem when a PCIe link-down leaves MMIO
wake-up failures as the earliest indication that the device
has stopped responding, because the existing recovery
mechanism is not triggered until higher-layer timeout paths
expire, for example:
failed to wake target for read32 at 0x00036044: -110
failed to wake target for write32 of 0xfffff81f at 0x00034834: -110
Improve this by triggering recovery immediately on MMIO
wake-up failures and by aborting MMIO-driven waits once the
device enters ATH10K_STATE_WEDGED, because no further forward
progress is possible after either condition.
This shortens known-failed recovery attempts and helps avoid
suspend watchdog timeouts on systems where recovery time is
tightly constrained.
Tested-on: QCA6174 hw3.2 PCI WLAN.RM.4.4.1-00288-QCARMSWPZ-1
Tested-on: QCA6174 hw3.2 SDIO WLAN.RMH.4.4.1-00189
Fixes: 77258d409ce4 ("ath10k: enable pci soc powersaving")
Signed-off-by: Kang Yang <kang.yang@oss.qualcomm.com>
---
drivers/net/wireless/ath/ath10k/pci.c | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/drivers/net/wireless/ath/ath10k/pci.c b/drivers/net/wireless/ath/ath10k/pci.c
index 335bc7c488e4..0c671630cfc9 100644
--- a/drivers/net/wireless/ath/ath10k/pci.c
+++ b/drivers/net/wireless/ath/ath10k/pci.c
@@ -478,6 +478,10 @@ static int ath10k_pci_wake_wait(struct ath10k *ar)
return 0;
}
+ /* The device cannot wake once it is wedged. */
+ if (ar->state == ATH10K_STATE_WEDGED)
+ return -ESHUTDOWN;
+
udelay(curr_delay);
tot_delay += curr_delay;
@@ -642,6 +646,16 @@ static void ath10k_bus_pci_write32(struct ath10k *ar, u32 offset, u32 value)
if (ret) {
ath10k_warn(ar, "failed to wake target for write32 of 0x%08x at 0x%08x: %d\n",
value, offset, ret);
+ /*
+ * wake_wait timed out; the device is unresponsive at the
+ * MMIO level. write32 returns void so the write is
+ * silently dropped -- kick recovery here instead of
+ * letting an upper WMI wait notice it seconds later.
+ * Guards confine this to runtime; cmpxchg dedups.
+ */
+ if (test_bit(ATH10K_FLAG_CORE_REGISTERED, &ar->dev_flags) &&
+ ar->state == ATH10K_STATE_ON)
+ ath10k_core_start_recovery(ar);
return;
}
@@ -665,6 +679,9 @@ static u32 ath10k_bus_pci_read32(struct ath10k *ar, u32 offset)
if (ret) {
ath10k_warn(ar, "failed to wake target for read32 at 0x%08x: %d\n",
offset, ret);
+ if (test_bit(ATH10K_FLAG_CORE_REGISTERED, &ar->dev_flags) &&
+ ar->state == ATH10K_STATE_ON)
+ ath10k_core_start_recovery(ar);
return 0xffffffff;
}
@@ -2248,6 +2265,12 @@ static int ath10k_pci_bmi_wait(struct ath10k *ar,
goto out;
}
+ /* Stop waiting once the device is wedged. */
+ if (ar->state == ATH10K_STATE_WEDGED) {
+ ret = -ESHUTDOWN;
+ goto out;
+ }
+
schedule();
}
@@ -3308,6 +3331,10 @@ int ath10k_pci_wait_for_target_init(struct ath10k *ar)
if (val & FW_IND_INITIALIZED)
break;
+ /* Stop waiting once the device is wedged. */
+ if (ar->state == ATH10K_STATE_WEDGED)
+ break;
+
if (ar_pci->oper_irq_mode == ATH10K_PCI_IRQ_INTX)
/* Fix potential race by repeating CORE_BASE writes */
ath10k_pci_enable_intx_irq(ar);
--
2.34.1
^ permalink raw reply [flat|nested] 14+ messages in thread