When the hardware returns a frame with ATH9K_TXERR_FILT, ath9k retries it internally only if it was sent as part of an A-MPDU. All other frames are completed with IEEE80211_TX_STAT_TX_FILTERED set and left to mac80211, which buffers them if the station is asleep and retries them once if it is awake (ieee80211_handle_filtered_frame()). Since commit d94a461d7a7d ("ath9k: use ieee80211_tx_status_noskb where possible") this no longer happens. Unless the frame requested a TX status, ath_tx_status() reports it with ieee80211_tx_status_noskb() and frees the skb, so mac80211 has nothing to buffer or retry and every filtered non-aggregated frame is lost. The filter does not only hit sleeping stations. On AR9561, programming the TX filter for one sleeping station (ath9k_hw_set_tx_filter()) also filters the next frame of every other station whose key cache index is in the same block of eight. An awake station without HT, whose frames are never aggregated, then loses that frame. On an AP with a few power-saving clients this caused 45% loss of pings sent once per second to an 802.11g-only client and TCP connections that stalled for minutes. HT clients did not suffer because ath9k retries their frames itself. Report filtered frames with the skb again. mac80211 re-queues such a frame, so also remove the header padding from it, as is already done for frames that request a TX status. Tested on an AR9561 AP (TP-Link Archer C7 v5, OpenWrt 25.12.6, wireless backports 6.18.55 on kernel 6.12.112) with a client that associates without HT. Programming the TX filter for an unused key cache index in the client's block before each ping lost 20 of 20 pings before this change and 0 of 50 with it, and mac80211's tx_filtered counter now increases once per filtered frame. Fixes: d94a461d7a7d ("ath9k: use ieee80211_tx_status_noskb where possible") Assisted-by: Claude:claude-fable-5-2 Signed-off-by: Dominik Engelhardt --- Notes (not part of the commit message): Test setup: TP-Link Archer C7 v5 as AP (QCA9563 SoC, AR9561 2.4 GHz radio), OpenWrt 25.12.6, i.e. kernel 6.12.112 with wireless backports 6.18.55. I have not run this on a mainline kernel. It is compile-tested on ath-next (x86_64, W=1, no new warnings). The OpenWrt SDK build with only this change reproduces the release ath9k_hw.ko and ath9k_common.ko bit for bit, so ath9k.ko differs from the release only by this patch. Reproducer, no second sleeping station needed: write the same values ath9k_hw_set_tx_filter() writes to AR_D_TXBLK_BASE (0x1038) through the debugfs regidx/regval files, for an unused key cache index that shares a block of eight with the target station's index (hw_key_idx in the mac80211 keys debugfs), then send one frame to the station. I can send the script. Results with the filter programmed before every ping: non-HT client, before: 0 of 20 pings answered, "TXERR Filtered" +20, mac80211 tx_filtered +0 non-HT client, after: 50 of 50 answered (56 and 1400 byte payload), "TXERR Filtered" +50, tx_filtered +50 HT client, before and after: 20 of 20 answered, handled by the existing A-MPDU software retry With a filter event every 0.5 s during 9000 pings at 10 ms intervals (1400 byte payload) to the awake non-HT client: no loss, no corrupted payload, no kernel warnings, memory stable. What this does not fix: with a client that really is in power save under sustained traffic (a ping every 20 ms), the filtered frames now reach mac80211's tx_filtered queue instead of being freed, but loss in that test was not clearly better (0 to 7.7% per run, against 1.5 to 2.5% before; the client's sleep pattern varied a lot between runs). I suspect the re-queued frames end up behind newer frames in the TXQ and are then dropped by the client's CCMP replay check. If you would rather have ath9k requeue filtered non-aggregated frames itself, as it does for A-MPDU subframes, I can look into that. The AR9561 filter granularity (a block of eight key cache indices instead of one) looks like a separate issue in ath9k_hw_set_tx_filter() or the hardware. After writing the filter for one index, reading 0x1038/0x1078/0x10b8/0x10f8 shows all eight bits of its block set. I have only this one chip to test with and no register documentation, so I have left it alone. An LLM (Claude) was used to analyse the problem, write the reproducer and draft this patch and text. I ran the tests on my own hardware and have reviewed the change. drivers/net/wireless/ath/ath9k/xmit.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/net/wireless/ath/ath9k/xmit.c b/drivers/net/wireless/ath/ath9k/xmit.c index 3413bb490..c8b9ef6dd 100644 --- a/drivers/net/wireless/ath/ath9k/xmit.c +++ b/drivers/net/wireless/ath/ath9k/xmit.c @@ -92,7 +92,8 @@ static void ath_tx_status(struct ieee80211_hw *hw, struct sk_buff *skb) struct ieee80211_sta *sta = info->status.status_driver_data[0]; if (info->flags & (IEEE80211_TX_CTL_REQ_TX_STATUS | - IEEE80211_TX_STATUS_EOSP)) { + IEEE80211_TX_STATUS_EOSP | + IEEE80211_TX_STAT_TX_FILTERED)) { ieee80211_tx_status_skb(hw, skb); return; } @@ -2474,7 +2475,8 @@ static void ath_tx_complete(struct ath_softc *sc, struct sk_buff *skb, tx_info->flags |= IEEE80211_TX_STAT_ACK; } - if (tx_info->flags & IEEE80211_TX_CTL_REQ_TX_STATUS) { + if (tx_info->flags & (IEEE80211_TX_CTL_REQ_TX_STATUS | + IEEE80211_TX_STAT_TX_FILTERED)) { padpos = ieee80211_hdrlen(hdr->frame_control); padsize = padpos & 3; if (padsize && skb->len>padpos+padsize) { base-commit: d0fbb39d65dc9dcb4175fa6b23e473dec3670f4d -- 2.47.3