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 --- 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/20260727162748.963275-2-jtornosm@redhat.com/ 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 When firmware crashes while wpa_supplicant has an active connection, wpa_supplicant may immediately attempt a scan via nl80211 while the firmware is still powering on through MHI. ath12k_mac_op_hw_scan() proceeds without checking the recovery state, accessing partially initialized radio structures which leads to a NULL pointer dereference: BUG: unable to handle page fault for address: 0000000000001508 RIP: ath12k_mac_op_hw_scan+0x148/0x2b0 [ath12k] Call Trace: drv_hw_scan+0x88/0x140 [mac80211] __ieee80211_start_scan+0x2bc/0x6b0 [mac80211] nl80211_trigger_scan+0x54a/0x9f0 [cfg80211] Fix by checking ATH12K_FLAG_RECOVERY at the start of ath12k_mac_op_hw_scan() and returning -EBUSY. mac80211 will retry the scan after the recovery completes. Tested on WCN7850 with MLO (Wi-Fi 7). Signed-off-by: Jose Ignacio Tornos Martinez --- v2: Fix radio[0] usage v1: https://lore.kernel.org/all/20260727162748.963275-3-jtornosm@redhat.com/ drivers/net/wireless/ath/ath12k/mac.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c index 7b7110ca6428..a57c2e91be84 100644 --- a/drivers/net/wireless/ath/ath12k/mac.c +++ b/drivers/net/wireless/ath/ath12k/mac.c @@ -5783,6 +5783,11 @@ int ath12k_mac_op_hw_scan(struct ieee80211_hw *hw, lockdep_assert_wiphy(hw->wiphy); + for_each_ar(ah, ar, i) { + if (test_bit(ATH12K_FLAG_RECOVERY, &ar->ab->dev_flags)) + return -EBUSY; + } + chan_list = kzalloc_objs(*chan_list, hw_req->req.n_channels); if (!chan_list) return -ENOMEM; -- 2.54.0 When firmware reports a beacon miss, ath12k queues connection_loss_work as a 3-second backup timer that forces a full VIF disconnect via ieee80211_connection_loss(). This work can only be cancelled when a beacon frame arrives through WMI in ath12k_mac_handle_beacon_iter(). In MLO, this mechanism causes periodic spurious disconnections because the firmware does not reliably forward beacon frames to the host via WMI, making the cancellation path ineffective. The result is a full VIF disconnect every ~16 seconds during normal MLO operation, easily reproducible under heavy traffic. Skip queueing connection_loss_work for MLO VIFs. The mac80211 AP probe triggered by ieee80211_beacon_loss() is sufficient to detect real AP unreachability. Other MLO-capable drivers such as mt76 and rtw89 similarly rely on ieee80211_beacon_loss() without adding a backup disconnect timer. Tested on WCN7850 with MLO (Wi-Fi 7). Signed-off-by: Jose Ignacio Tornos Martinez --- v2: No modification v1: https://lore.kernel.org/all/20260727162748.963275-6-jtornosm@redhat.com/ drivers/net/wireless/ath/ath12k/mac.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/drivers/net/wireless/ath/ath12k/mac.c b/drivers/net/wireless/ath/ath12k/mac.c index af09153ad9d9..dfd69822c5ce 100644 --- a/drivers/net/wireless/ath/ath12k/mac.c +++ b/drivers/net/wireless/ath/ath12k/mac.c @@ -2008,9 +2008,16 @@ void ath12k_mac_handle_beacon_miss(struct ath12k *ar, * (done by mac80211) succeeds but beacons do not resume then it * doesn't make sense to continue operation. Queue connection loss work * which can be cancelled when beacon is received. + * + * Skip for MLO because connection_loss_work disconnects the entire + * VIF based on a single link's beacon miss, and its cancellation + * mechanism (beacon reception via WMI) is unreliable. The mac80211 + * probe triggered by ieee80211_beacon_loss() above is sufficient + * to detect real AP unreachability. */ - ieee80211_queue_delayed_work(hw, &arvif->connection_loss_work, - ATH12K_CONNECTION_LOSS_HZ); + if (!ieee80211_vif_is_mld(vif)) + ieee80211_queue_delayed_work(hw, &arvif->connection_loss_work, + ATH12K_CONNECTION_LOSS_HZ); } static void ath12k_mac_vif_sta_connection_loss_work(struct work_struct *work) -- 2.54.0