[PATCH v2 1/3] wifi: ath12k: fix MLO station firmware crash recovery

Jose Ignacio Tornos Martinez <[email protected]> Tue, 4 Aug 2026 19:49:59 +0200
Newsgroups org.kernel.vger.linux-wireless,org.infradead.lists.ath11k,org.infradead.lists.ath12k,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
ATH12K_FLAG_RECOVERY is cleared too early in
ath12k_core_reconfigure_on_crash(), before mac80211 runs
ieee80211_reconfig(). By the time mac80211 calls back into the driver
(sta_state, change_vif_links, set_key), the RECOVERY flag is already false,
so the driver treats recovery callbacks as normal operations.

This causes several problems during MLO recovery:

- ath12k_mac_op_sta_state() tries to activate MLO links during the
  AUTH->ASSOC transition, calling ieee80211_set_active_links()
  recursively, which triggers a WARNING at net/mac80211/link.c.

- ath12k_mac_op_change_vif_links() processes link removal during
  reconfig, causing inconsistent state.

- ath12k_mac_set_key() fails with "cannot install key for non-existent
  peer" because peers do not exist yet during reconfig. Keys will be
  re-established during normal reconnection after
  ieee80211_hw_restart_disconnect() triggers a fresh association.

- ath12k_mac_flush() waits for pending TX to complete, but after a
  firmware crash the TX will never complete, causing a 20 second timeout.

- ath12k_mac_station_remove() calls ath12k_bss_disassoc() and
  ath12k_mac_vdev_stop() which send WMI commands to dead firmware,
  causing timeouts that delay recovery.

- ath12k_clear_peer_keys() tries to look up and clear peer keys, but
  peers are already gone after firmware crash.

- ath12k_dp_rx_ampdu_stop() dereferences per-link station state that
  may not be valid during crash teardown.

- ath12k_peer_mlo_link_peers_delete() sends WMI peer delete commands
  for each MLO link peer. With dead firmware these time out and can
  trigger cascading resets.

These issues were observed during sporadic firmware crashes in MLO
operation. To allow systematic testing and reproduction, the debugfs
simulate_fw_crash interface was used to trigger controlled firmware
crashes during active MLO connections with traffic.

Fix by moving clear_bit(ATH12K_FLAG_RECOVERY) from
ath12k_core_reconfigure_on_crash() to ath12k_mac_op_reconfig_complete(),
so the flag stays set through the entire mac80211 reconfig phase. Add
ATH12K_FLAG_RECOVERY checks in change_vif_links, set_key, and sta_state
to skip operations that are invalid during recovery. Add
ATH12K_FLAG_CRASH_FLUSH checks in mac_flush, station_remove,
clear_peer_keys, dp_rx_ampdu_stop, and peer_mlo_link_peers_delete to
return immediately when the firmware is dead.

Tested on WCN7850 with MLO (Wi-Fi 7).

Signed-off-by: Jose Ignacio Tornos Martinez <[email protected]>
---
v2: Rebase and address comments from Baochen Qiang:
    - Remove CRASH_FLUSH guards from HAL srng source ring helpers
    (ath12k_hal_srng_src_num_free, ath12k_hal_srng_src_get_next_entry,
    ath12k_hal_srng_access_end). They were a layering violation — the
    HAL should not know about device crash semantics. They were added
    as a precautionary measure to abort as soon as possible, but
    ath12k_wifi7_dp_tx() already has a CRASH_FLUSH check before any
    HAL call.
    - Fix radio[0] usage in change_vif_links, sta_state, and
    peer_mlo_link_peers_delete to use per-radio ab.
v1: https://lore.kernel.org/all/[email protected]/

 drivers/net/wireless/ath/ath12k/core.c  |  2 --
 drivers/net/wireless/ath/ath12k/dp_rx.c |  3 +++
 drivers/net/wireless/ath/ath12k/mac.c   | 27 ++++++++++++++++++++++++-
 drivers/net/wireless/ath/ath12k/peer.c  |  6 ++++++
 4 files changed, 35 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/ath/ath12k/core.c b/drivers/net/wireless/ath/ath12k/core.c
index 262a2045309b..5c0ee74808cd 100644
--- a/drivers/net/wireless/ath/ath12k/core.c
+++ b/drivers/net/wireless/ath/ath12k/core.c
@@ -1391,8 +1391,6 @@ static int ath12k_core_reconfigure_on_crash(struct ath12k_base *ab)
 	if (ret)
 		goto err_hal_srng_deinit;
 
-	clear_bit(ATH12K_FLAG_RECOVERY, &ab->dev_flags);
-
 	return 0;
 
 err_hal_srng_deinit:
diff --git a/drivers/net/wireless/ath/ath12k/dp_rx.c b/drivers/net/wireless/ath/ath12k/dp_rx.c
index 8fa0e90b4531..473855ded8a7 100644
--- a/drivers/net/wireless/ath/ath12k/dp_rx.c
+++ b/drivers/net/wireless/ath/ath12k/dp_rx.c
@@ -751,6 +751,9 @@ int ath12k_dp_rx_ampdu_stop(struct ath12k *ar,
 
 	lockdep_assert_wiphy(ath12k_ar_to_hw(ar)->wiphy);
 
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, &ab->dev_flags))
+		return 0;
+
 	arsta = wiphy_dereference(ath12k_ar_to_hw(ar)->wiphy,
 				  ahsta->link[link_id]);
 	if (!arsta)
diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c
index 99bf5cf79d10..7b7110ca6428 100644
--- a/drivers/net/wireless/ath/ath12k/mac.c
+++ b/drivers/net/wireless/ath/ath12k/mac.c
@@ -4341,12 +4341,22 @@ ath12k_mac_op_change_vif_links(struct ieee80211_hw *hw,
 	struct ath12k_vif *ahvif = ath12k_vif_to_ahvif(vif);
 	unsigned long to_remove = old_links & ~new_links;
 	unsigned long to_add = ~old_links & new_links;
+	unsigned long existing = old_links;
 	struct ath12k_hw *ah = ath12k_hw_to_ah(hw);
 	struct ath12k_link_vif *arvif;
 	u8 link_id;
 
 	lockdep_assert_wiphy(hw->wiphy);
 
+	if (old_links) {
+		for_each_set_bit(link_id, &existing, IEEE80211_MLD_MAX_NUM_LINKS) {
+			arvif = wiphy_dereference(hw->wiphy, ahvif->link[link_id]);
+			if (arvif && arvif->ar &&
+			    test_bit(ATH12K_FLAG_RECOVERY, &arvif->ar->ab->dev_flags))
+				return -EINVAL;
+		}
+	}
+
 	ath12k_generic_dbg(ATH12K_DBG_MAC,
 			   "mac vif link changed for MLD %pM old_links 0x%x new_links 0x%x\n",
 			   vif->addr, old_links, new_links);
@@ -6025,6 +6035,9 @@ static int ath12k_clear_peer_keys(struct ath12k_link_vif *arvif,
 
 	lockdep_assert_wiphy(ath12k_ar_to_hw(ar)->wiphy);
 
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, &ab->dev_flags))
+		return 0;
+
 	spin_lock_bh(&dp->dp_lock);
 	peer = ath12k_dp_link_peer_find_by_vdev_and_addr(dp, arvif->vdev_id, addr);
 	if (!peer || !peer->dp_peer) {
@@ -6100,6 +6113,8 @@ static int ath12k_mac_set_key(struct ath12k *ar, enum set_key_cmd cmd,
 		spin_unlock_bh(&dp->dp_lock);
 
 		if (cmd == SET_KEY) {
+			if (test_bit(ATH12K_FLAG_RECOVERY, &ab->dev_flags))
+				return 0;
 			ath12k_warn(ab, "cannot install key for non-existent peer %pM\n",
 				    peer_addr);
 			return -EOPNOTSUPP;
@@ -7100,7 +7115,8 @@ static int ath12k_mac_station_remove(struct ath12k *ar,
 
 	wiphy_work_cancel(ar->ah->hw->wiphy, &arsta->update_wk);
 
-	if (ahvif->vdev_type == WMI_VDEV_TYPE_STA) {
+	if (ahvif->vdev_type == WMI_VDEV_TYPE_STA &&
+	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, &ar->ab->dev_flags)) {
 		ath12k_bss_disassoc(ar, arvif);
 		ret = ath12k_mac_vdev_stop(arvif);
 		if (ret)
@@ -7861,6 +7877,10 @@ int ath12k_mac_op_sta_state(struct ieee80211_hw *hw,
 		 * MLO case. Other cases would be handled in the future.
 		 */
 		ab = ah->radio[0].ab;
+		if (test_bit(ATH12K_FLAG_RECOVERY, &ab->dev_flags)) {
+			ret = 0;
+			goto exit;
+		}
 		if (ab->ag->num_devices == 1) {
 			ret = ath12k_mac_select_links(ab, vif, hw, &selected_links);
 			if (ret) {
@@ -12661,6 +12681,9 @@ static int ath12k_mac_flush(struct ath12k *ar)
 	long time_left;
 	int ret = 0;
 
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, &ar->ab->dev_flags))
+		return -ESHUTDOWN;
+
 	time_left = wait_event_timeout(ar->dp.tx_empty_waitq,
 				       (atomic_read(&ar->dp.num_tx_pending) == 0),
 				       ATH12K_FLUSH_TIMEOUT);
@@ -13559,6 +13582,8 @@ ath12k_mac_op_reconfig_complete(struct ieee80211_hw *hw,
 	for_each_ar(ah, ar, i) {
 		ab = ar->ab;
 
+		clear_bit(ATH12K_FLAG_RECOVERY, &ab->dev_flags);
+
 		ath12k_warn(ar->ab, "pdev %d successfully recovered\n",
 			    ar->pdev->pdev_id);
 
diff --git a/drivers/net/wireless/ath/ath12k/peer.c b/drivers/net/wireless/ath/ath12k/peer.c
index 80edebf0e364..d64e62192cd7 100644
--- a/drivers/net/wireless/ath/ath12k/peer.c
+++ b/drivers/net/wireless/ath/ath12k/peer.c
@@ -376,12 +376,18 @@ int ath12k_peer_mlo_link_peers_delete(struct ath12k_vif *ahvif, struct ath12k_st
 	unsigned long links;
 	struct ath12k *ar;
 	u8 link_id;
+	int i;
 
 	lockdep_assert_wiphy(ah->hw->wiphy);
 
 	if (!sta->mlo)
 		return -EINVAL;
 
+	for_each_ar(ah, ar, i) {
+		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, &ar->ab->dev_flags))
+			return 0;
+	}
+
 	struct ath12k_peer_delete_wait *waits __free(kfree) =
 				kzalloc_objs(*waits, IEEE80211_MLD_MAX_NUM_LINKS);
 	if (!waits)
-- 
2.54.0