* [PATCH mt76-next 02/25] wifi: mt76: mt7996: do not update beacons while CSA is active
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 03/25] wifi: mt76: mt7996: only remove a BMC STA_REC that was added Felix Fietkau
` (22 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless; +Cc: StanleyYP Wang
Each upload of the CSA beacon makes the firmware restart the countdown
from the template, but the driver uploads it more than once. At the
start of a CSA, mt7996_link_info_changed() and
mt7996_channel_switch_beacon() both upload it. mt7996_set_channel()
uploads it once more for the channel switch that
__ieee80211_csa_finalize() performs while csa_active is still set.
Upload the CSA beacon of a link only while the firmware does not hold
it: at the start of the CSA, after offchannel operation and after a
hardware restart. Do this, and the beacon reset for a CSA during CAC,
in mt7996_link_info_changed(), which knows the link of the CSA. Drop
mt7996_channel_switch_beacon(), which selects the link by the band of
the new channel; that band can belong to another radio.
In mt7996_set_channel(), restore only the beacons that were disabled
for offchannel operation. Other beacon updates during CSA are dropped
as well, since mac80211 rejects beacon changes from user space during
CSA and sets the post-CSA beacon right after the switch. A beacon
disable during CSA still goes to the firmware.
Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices")
Fixes: f30906c55a40 ("wifi: mt76: mt7996: disable beacons when going offchannel")
Co-developed-by: StanleyYP Wang <StanleyYP.Wang@mediatek.com>
Signed-off-by: StanleyYP Wang <StanleyYP.Wang@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/mac.c | 1 +
.../net/wireless/mediatek/mt76/mt7996/main.c | 68 ++++++++-----------
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 13 +++-
.../wireless/mediatek/mt76/mt7996/mt7996.h | 2 +
4 files changed, 40 insertions(+), 44 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index edff4e5a5dff..2e1b67ac2d11 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -2251,6 +2251,7 @@ void mt7996_mac_update_beacons(struct mt7996_phy *phy)
ieee80211_iterate_active_interfaces(phy->mt76->hw,
IEEE80211_IFACE_ITER_RESUME_ALL,
mt7996_update_vif_beacon, phy);
+ phy->offchan_bcn_stopped = phy->mt76->offchannel;
}
static void
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 9f1927cad5c4..a2655784a13d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -349,6 +349,7 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
return -ENOSPC;
link->mld_idx = mld_idx;
+ link->csa_bcn_uploaded = false;
eth_zero_addr(link->bmc_bssid);
mlink->omac_idx = idx;
mlink->band_idx = band_idx;
@@ -703,7 +704,7 @@ int mt7996_set_channel(struct mt76_phy *mphy)
mt7996_mac_reset_counters(phy);
phy->noise = 0;
- if (!mphy->offchannel)
+ if (!mphy->offchannel && phy->offchan_bcn_stopped)
mt7996_mac_update_beacons(phy);
out:
@@ -995,6 +996,30 @@ mt7996_vif_cfg_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mutex_unlock(&dev->mt76.mutex);
}
+static void
+mt7996_link_beacon_update(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
+ struct ieee80211_bss_conf *link_conf,
+ struct mt7996_vif_link *link, struct mt7996_phy *phy)
+{
+ if (!link_conf->enable_beacon || !link_conf->csa_active) {
+ mt7996_mcu_add_beacon(hw, vif, link_conf,
+ link_conf->enable_beacon);
+ return;
+ }
+
+ /* each upload of the CSA beacon restarts the firmware countdown */
+ if (link->csa_bcn_uploaded)
+ return;
+
+ /* Reset beacon when channel switch triggered during CAC to let
+ * FW correctly perform CSA countdown
+ */
+ if (!cfg80211_reg_can_beacon(hw->wiphy, &phy->mt76->chandef, vif->type))
+ mt7996_mcu_add_beacon(hw, vif, link_conf, false);
+
+ mt7996_mcu_add_beacon(hw, vif, link_conf, true);
+}
+
static void
mt7996_link_info_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_bss_conf *info, u64 changed)
@@ -1067,7 +1092,7 @@ mt7996_link_info_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
link->mt76.beacon_rates_idx =
mt7996_get_rates_table(phy, info, true, false);
- mt7996_mcu_add_beacon(hw, vif, info, info->enable_beacon);
+ mt7996_link_beacon_update(hw, vif, info, link, phy);
}
if (changed & (BSS_CHANGED_UNSOL_BCAST_PROBE_RESP |
@@ -1087,44 +1112,6 @@ mt7996_link_info_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mutex_unlock(&dev->mt76.mutex);
}
-static void
-mt7996_channel_switch_beacon(struct ieee80211_hw *hw,
- struct ieee80211_vif *vif,
- struct cfg80211_chan_def *chandef)
-{
- struct mt7996_dev *dev = mt7996_hw_dev(hw);
- struct mt7996_phy *phy = mt7996_band_phy(dev, chandef->chan->band);
- struct ieee80211_bss_conf *link_conf;
- unsigned int link_id;
-
- mutex_lock(&dev->mt76.mutex);
-
- for_each_vif_active_link(vif, link_conf, link_id) {
- struct mt7996_vif_link *link;
- struct mt7996_phy *link_phy;
-
- link = mt7996_vif_link(dev, vif, link_id);
- if (!link)
- continue;
-
- link_phy = mt7996_vif_link_phy(link);
- if (link_phy != phy)
- continue;
-
- /* Reset beacon when channel switch triggered during CAC to let
- * FW correctly perform CSA countdown
- */
- if (!cfg80211_reg_can_beacon(hw->wiphy, &phy->mt76->chandef,
- vif->type))
- mt7996_mcu_add_beacon(hw, vif, link_conf, false);
-
- mt7996_mcu_add_beacon(hw, vif, link_conf, true);
- break;
- }
-
- mutex_unlock(&dev->mt76.mutex);
-}
-
static int
mt7996_post_channel_switch(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_bss_conf *link_conf)
@@ -3093,7 +3080,6 @@ const struct ieee80211_ops mt7996_ops = {
.cancel_remain_on_channel = mt76_cancel_remain_on_channel,
.release_buffered_frames = mt76_release_buffered_frames,
.get_txpower = mt7996_get_txpower,
- .channel_switch_beacon = mt7996_channel_switch_beacon,
.post_channel_switch = mt7996_post_channel_switch,
.get_stats = mt7996_get_stats,
.get_et_sset_count = mt7996_get_et_sset_count,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index e3420ac523ab..7e72174bd6b1 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -3254,7 +3254,7 @@ int mt7996_mcu_add_beacon(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct sk_buff *skb, *rskb;
struct tlv *tlv;
struct bss_bcn_content_tlv *bcn;
- int len, extra_len = 0;
+ int len, extra_len = 0, ret;
if (link_conf->nontransmitted)
return 0;
@@ -3262,6 +3262,10 @@ int mt7996_mcu_add_beacon(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
if (!mlink)
return -EINVAL;
+ /* a later CSA needs a new template, also after a failed upload */
+ if (!link_conf->csa_active)
+ link->csa_bcn_uploaded = false;
+
if (mphy && mphy->offchannel)
enabled = false;
@@ -3303,8 +3307,11 @@ int mt7996_mcu_add_beacon(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mt7996_mcu_beacon_cntdwn(rskb, skb, &offs, link_conf->csa_active);
out:
dev_kfree_skb(skb);
- return mt76_mcu_skb_send_msg(&dev->mt76, rskb,
- MCU_WMWA_UNI_CMD(BSS_INFO_UPDATE), true);
+ ret = mt76_mcu_skb_send_msg(&dev->mt76, rskb,
+ MCU_WMWA_UNI_CMD(BSS_INFO_UPDATE), true);
+ link->csa_bcn_uploaded = !ret && enabled && link_conf->csa_active;
+
+ return ret;
}
int mt7996_mcu_beacon_inband_discov(struct mt7996_dev *dev,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index da9958647b54..84edb905fe06 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -295,6 +295,7 @@ struct mt7996_vif_link {
u8 mld_idx;
u8 bmc_bssid[ETH_ALEN];
+ bool csa_bcn_uploaded;
};
struct mt7996_vif_link_info {
@@ -416,6 +417,7 @@ struct mt7996_phy {
bool has_aux_rx;
bool counter_reset;
bool rdd_tx_paused;
+ bool offchan_bcn_stopped;
};
struct mt7996_dev {
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 03/25] wifi: mt76: mt7996: only remove a BMC STA_REC that was added
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 02/25] wifi: mt76: mt7996: do not update beacons while CSA is active Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 04/25] wifi: mt76: mt7996: pass the puncturing bitmap to the firmware Felix Fietkau
` (21 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_add() defers the BMC STA_REC of a station interface to
BSS_CHANGED_BSSID or BSS_CHANGED_ASSOC. A link that
mt76_get_vif_phy_link() creates for a scan or ROC receives neither,
because it is not part of the vif links. mt7996_vif_link_destroy() still
sends a STA_REC removal for it at the end of each scan, and the firmware
rejects it:
staRecFreeStaRec-1606 Err: u2ClientSupportCnt is already 0. (wcid 1087)
staRecUniCmdUpdate, u2Tag 0x0 ret = 0xc0000001 not success
Record per link whether the firmware accepted its BMC STA_REC, and only
send the removal for such a link. This also covers a station link that
is torn down before its first BSSID or association update.
Fixes: 51b795d68cad ("wifi: mt76: connac: use peer address for station BMC entry")
Fixes: 69d54ce7491d ("wifi: mt76: mt7996: switch to single multi-radio wiphy")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 18 +++++++++++-------
.../net/wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index a2655784a13d..e8faa826e037 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -349,6 +349,7 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
return -ENOSPC;
link->mld_idx = mld_idx;
+ link->bmc_sta_added = false;
link->csa_bcn_uploaded = false;
eth_zero_addr(link->bmc_bssid);
mlink->omac_idx = idx;
@@ -392,9 +393,10 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
/* defer the first STA_REC of BMC entry to BSS_CHANGED_BSSID for STA
* interface, since firmware only records BSSID when the entry is new
*/
- if (vif->type != NL80211_IFTYPE_STATION)
- mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
- CONN_STATE_PORT_SECURE, true);
+ if (vif->type != NL80211_IFTYPE_STATION &&
+ !mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
+ CONN_STATE_PORT_SECURE, true))
+ link->bmc_sta_added = true;
rcu_assign_pointer(dev->mt76.wcid[idx], &msta_link->wcid);
if (!mlink->wcid->offchannel)
@@ -441,8 +443,9 @@ static void mt7996_vif_link_destroy(struct mt7996_phy *phy,
if (!link_conf)
link_conf = &vif->bss_conf;
- mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
- CONN_STATE_DISCONNECT, false);
+ if (link->bmc_sta_added)
+ mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
+ CONN_STATE_DISCONNECT, false);
mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink, msta_link, false);
mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, false);
@@ -939,8 +942,9 @@ mt7996_vif_link_bss_add(struct mt7996_phy *phy, struct ieee80211_vif *vif,
mt7996_mcu_add_bss_info(phy, vif, link_conf, &link->mt76,
&link->msta_link, true);
- mt7996_mcu_add_sta(phy->dev, link_conf, NULL, link, NULL,
- CONN_STATE_PORT_SECURE, newly);
+ if (!mt7996_mcu_add_sta(phy->dev, link_conf, NULL, link, NULL,
+ CONN_STATE_PORT_SECURE, newly))
+ link->bmc_sta_added = true;
}
static void
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 84edb905fe06..d30b4d689bac 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -295,6 +295,7 @@ struct mt7996_vif_link {
u8 mld_idx;
u8 bmc_bssid[ETH_ALEN];
+ bool bmc_sta_added;
bool csa_bcn_uploaded;
};
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 04/25] wifi: mt76: mt7996: pass the puncturing bitmap to the firmware
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 02/25] wifi: mt76: mt7996: do not update beacons while CSA is active Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 03/25] wifi: mt76: mt7996: only remove a BMC STA_REC that was added Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 05/25] wifi: mt76: abort a scan when the chanctx changes Felix Fietkau
` (20 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Allen Ye
mt7996_set_channel() programs the channel and the width of a chanctx,
but not chandef->punctured, so the firmware does not know which
subchannels of the channel are punctured.
Send the bitmap of the chandef with the PP_EN_CTRL command on every
channel set, in user mode with the firmware's own puncturing detection
off. The firmware holds one bitmap per band, so stations and APs on the
band use the same command. Do not wait for a response, like the
MediaTek vendor driver, which sends this command with the same
firmware. Skip 2.4 GHz, where an EHT channel is at most 40 MHz wide and
cannot be punctured.
Co-developed-by: Allen Ye <allen.ye@mediatek.com>
Signed-off-by: Allen Ye <allen.ye@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../wireless/mediatek/mt76/mt76_connac_mcu.h | 1 +
.../net/wireless/mediatek/mt76/mt7996/main.c | 4 +++
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 32 +++++++++++++++++++
.../net/wireless/mediatek/mt76/mt7996/mcu.h | 4 +++
.../wireless/mediatek/mt76/mt7996/mt7996.h | 1 +
5 files changed, 42 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
index 88281733a3df..e9a7542c07dc 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
@@ -1349,6 +1349,7 @@ enum {
MCU_UNI_CMD_CHANNEL_SWITCH = 0x34,
MCU_UNI_CMD_THERMAL = 0x35,
MCU_UNI_CMD_VOW = 0x37,
+ MCU_UNI_CMD_PP = 0x38,
MCU_UNI_CMD_FIXED_RATE_TABLE = 0x40,
MCU_UNI_CMD_RSSI_MONITOR = 0x41,
MCU_UNI_CMD_TESTMODE_CTRL = 0x46,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e8faa826e037..3a3441951254 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -690,6 +690,10 @@ int mt7996_set_channel(struct mt76_phy *mphy)
if (mphy->offchannel)
mt7996_mac_update_beacons(phy);
+ ret = mt7996_mcu_set_punct(phy);
+ if (ret)
+ goto out;
+
ret = mt7996_mcu_set_chan_info(phy, UNI_CHANNEL_SWITCH);
if (ret)
goto out;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index 7e72174bd6b1..f35250430383 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -4255,6 +4255,38 @@ int mt7996_mcu_set_chan_info(struct mt7996_phy *phy, u16 tag)
&req, sizeof(req), true);
}
+int mt7996_mcu_set_punct(struct mt7996_phy *phy)
+{
+ struct cfg80211_chan_def *chandef = &phy->mt76->chandef;
+ struct mt7996_dev *dev = phy->dev;
+ struct {
+ u8 __rsv[4];
+
+ __le16 tag;
+ __le16 len;
+ u8 mgmt_mode;
+ u8 band_idx;
+ u8 force_bitmap_ctrl;
+ u8 auto_mode;
+ __le16 bitmap;
+ u8 csa_enable;
+ u8 __rsv2;
+ } __packed req = {
+ .tag = cpu_to_le16(UNI_CMD_PP_EN_CTRL),
+ .len = cpu_to_le16(sizeof(req) - 4),
+ .mgmt_mode = 1,
+ .band_idx = phy->mt76->band_idx,
+ .force_bitmap_ctrl = 2,
+ .bitmap = cpu_to_le16(chandef->punctured),
+ };
+
+ if (chandef->chan->band == NL80211_BAND_2GHZ)
+ return 0;
+
+ return mt76_mcu_send_msg(&dev->mt76, MCU_WM_UNI_CMD(PP),
+ &req, sizeof(req), false);
+}
+
static int
mt7996_mcu_get_cal_free_data(struct mt7996_dev *dev)
{
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 6078f8811b1e..70ed57d9bf9a 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -1033,6 +1033,10 @@ enum{
UNI_CMD_SR_SET_SIGA = 0xd0,
};
+enum {
+ UNI_CMD_PP_EN_CTRL,
+};
+
enum {
UNI_CMD_THERMAL_PROTECT_ENABLE = 0x6,
UNI_CMD_THERMAL_PROTECT_DISABLE,
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index d30b4d689bac..1f1dff9f6610 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -786,6 +786,7 @@ int mt7996_mcu_add_rate_ctrl(struct mt7996_dev *dev, struct mt7996_sta *msta,
bool changed);
int mt7996_set_channel(struct mt76_phy *mphy);
int mt7996_mcu_set_chan_info(struct mt7996_phy *phy, u16 tag);
+int mt7996_mcu_set_punct(struct mt7996_phy *phy);
int mt7996_mcu_set_tx(struct mt7996_dev *dev, struct ieee80211_vif *vif,
struct ieee80211_bss_conf *link_conf);
int mt7996_mcu_set_fixed_rate_ctrl(struct mt7996_dev *dev,
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 05/25] wifi: mt76: abort a scan when the chanctx changes
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (2 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 04/25] wifi: mt76: mt7996: pass the puncturing bitmap to the firmware Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 06/25] wifi: mt76: set the channel again when the chanctx puncturing changes Felix Fietkau
` (19 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt76_change_chanctx() sets the channel of the chanctx again while a scan
can still run on the phy. During the dwell on an offchannel scan
channel, the phy thus returns to the operating channel without the
mt76_offchannel_notify() that ends the power save period towards the
AP.
Abort the scan before the channel is set again, like
mt76_add_chanctx() and mt76_remove_chanctx() do.
Fixes: 82334623af0c ("wifi: mt76: add chanctx functions for multi-channel phy support")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/channel.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/channel.c b/drivers/net/wireless/mediatek/mt76/channel.c
index 192276cf19fd..115117a19b5c 100644
--- a/drivers/net/wireless/mediatek/mt76/channel.c
+++ b/drivers/net/wireless/mediatek/mt76/channel.c
@@ -88,6 +88,9 @@ void mt76_change_chanctx(struct ieee80211_hw *hw,
IEEE80211_CHANCTX_CHANGE_RADAR)))
return;
+ if (mt76_phy_scanning(phy))
+ mt76_abort_scan(dev);
+
if (phy->roc_vif)
mt76_abort_roc(phy);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 06/25] wifi: mt76: set the channel again when the chanctx puncturing changes
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (3 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 05/25] wifi: mt76: abort a scan when the chanctx changes Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 07/25] wifi: mt76: mt7996: advertise preamble puncturing support Felix Fietkau
` (18 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt76_change_chanctx() ignores a change that only sets
IEEE80211_CHANCTX_CHANGE_PUNCTURING, so the driver keeps the old
puncturing bitmap. mac80211 sends such a change from
ieee80211_link_change_chanreq(), for example when a station follows a
new Disabled Subchannel Bitmap of its AP without a channel switch.
Set the channel again on a puncturing change, as for a width change, so
that the driver programs the new bitmap. Only mt7996 uses
mt76_change_chanctx(), and mt7996_set_channel() sends the bitmap to the
firmware.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/channel.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/channel.c b/drivers/net/wireless/mediatek/mt76/channel.c
index 115117a19b5c..39a5dca4ca7d 100644
--- a/drivers/net/wireless/mediatek/mt76/channel.c
+++ b/drivers/net/wireless/mediatek/mt76/channel.c
@@ -85,7 +85,8 @@ void mt76_change_chanctx(struct ieee80211_hw *hw,
struct mt76_dev *dev = phy->dev;
if (!(changed & (IEEE80211_CHANCTX_CHANGE_WIDTH |
- IEEE80211_CHANCTX_CHANGE_RADAR)))
+ IEEE80211_CHANCTX_CHANGE_RADAR |
+ IEEE80211_CHANCTX_CHANGE_PUNCTURING)))
return;
if (mt76_phy_scanning(phy))
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 07/25] wifi: mt76: mt7996: advertise preamble puncturing support
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (4 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 06/25] wifi: mt76: set the channel again when the chanctx puncturing changes Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 08/25] wifi: mt76: mt7996: keep scan and ROC links out of the MLD group Felix Fietkau
` (17 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Shayne Chen
nl80211 refuses a non-zero NL80211_ATTR_PUNCT_BITMAP for a driver
without NL80211_EXT_FEATURE_PUNCT, so an mt7996 AP cannot start or
switch to a punctured channel.
Advertise the feature. mt7996_set_channel() sends the puncturing bitmap
of every chanctx to the firmware, including after a puncturing-only
chanctx change.
Co-developed-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/init.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/init.c b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
index cc94eade9950..f44a3a8588d3 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/init.c
@@ -526,6 +526,7 @@ mt7996_init_wiphy(struct ieee80211_hw *hw, struct mtk_wed_device *wed)
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_CAN_REPLACE_PTK0);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_MU_MIMO_AIR_SNIFFER);
wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);
+ wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_PUNCT);
if (mt7996_eeprom_has_background_radar(dev) &&
(!mdev->dev->of_node ||
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 08/25] wifi: mt76: mt7996: keep scan and ROC links out of the MLD group
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (5 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 07/25] wifi: mt76: mt7996: advertise preamble puncturing support Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 09/25] wifi: mt76: mt7996: factor the vif link teardown out of remove_interface Felix Fietkau
` (16 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
When an MLD vif scans or does ROC on a radio where it has no link,
mt76_get_vif_phy_link() adds an offchannel link with &vif->bss_conf.
mt7996_mcu_bss_mld_tlv() registers that link in the firmware as a
member of the vif's MLD group, with the group and remap index, the MLD
address and link_id 0. The group then holds link 0 twice if the MLD has
a real link 0, and once more for each other radio the scan visits. The
removal of the offchannel link sends a BSS_INFO without an MLD TLV, so
the firmware gets no command that takes it out of the group again.
Register an offchannel link like the link of a non-MLD vif. It only
carries scan and ROC management frames, which need no MLD address
translation.
Fixes: ed01c310eca9 ("wifi: mt76: mt7996: Fix mt7996_mcu_bss_mld_tlv routine")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/mcu.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index f35250430383..ce37b6b65918 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -1172,7 +1172,7 @@ mt7996_mcu_bss_mld_tlv(struct sk_buff *skb,
mld->own_mld_id = link->mld_idx;
mld->link_id = link_conf->link_id;
- if (ieee80211_vif_is_mld(vif)) {
+ if (ieee80211_vif_is_mld(vif) && !link->mt76.offchannel) {
mld->group_mld_id = mvif->mld_group_idx;
mld->remap_idx = mvif->mld_remap_idx;
memcpy(mld->mac_addr, vif->addr, ETH_ALEN);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 09/25] wifi: mt76: mt7996: factor the vif link teardown out of remove_interface
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (6 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 08/25] wifi: mt76: mt7996: keep scan and ROC links out of the MLD group Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 10/25] wifi: mt76: mt7996: destroy station links before the MLD group is freed Felix Fietkau
` (15 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
Move the loop that destroys the links of a vif out of
mt7996_remove_interface() into a helper that takes the links to destroy
and, optionally, their link confs. A following change destroys station
links when the vif leaves MLO, where mac80211 still passes the old link
confs.
No functional change.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 47 +++++++++++--------
1 file changed, 28 insertions(+), 19 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 3a3441951254..cc8ce83f8b2d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -471,6 +471,33 @@ static void mt7996_vif_link_destroy(struct mt7996_phy *phy,
}
}
+static void
+mt7996_vif_links_destroy(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ unsigned long links,
+ struct ieee80211_bss_conf **link_confs)
+{
+ unsigned int link_id;
+
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_bss_conf *link_conf = NULL;
+ struct mt7996_vif_link *link;
+ struct mt7996_phy *phy;
+
+ link = mt7996_vif_link(dev, vif, link_id);
+ if (!link)
+ continue;
+
+ phy = __mt7996_phy(dev, link->msta_link.wcid.phy_idx);
+ if (!phy)
+ continue;
+
+ if (link_confs)
+ link_conf = link_confs[link_id];
+
+ mt7996_vif_link_destroy(phy, link, vif, link_conf);
+ }
+}
+
void mt7996_vif_link_remove(struct mt76_phy *mphy, struct ieee80211_vif *vif,
struct ieee80211_bss_conf *link_conf,
struct mt76_vif_link *mlink)
@@ -640,30 +667,12 @@ static void mt7996_remove_interface(struct ieee80211_hw *hw,
struct ieee80211_vif *vif)
{
struct mt7996_vif *mvif = (struct mt7996_vif *)vif->drv_priv;
- unsigned long rem_links = mvif->mt76.valid_links;
struct mt7996_dev *dev = mt7996_hw_dev(hw);
struct mt7996_radio_data rdata = {};
- unsigned int link_id;
int i;
mutex_lock(&dev->mt76.mutex);
-
- /* Remove all active links */
- for_each_set_bit(link_id, &rem_links, IEEE80211_MLD_MAX_NUM_LINKS) {
- struct mt7996_vif_link *link;
- struct mt7996_phy *phy;
-
- link = mt7996_vif_link(dev, vif, link_id);
- if (!link)
- continue;
-
- phy = __mt7996_phy(dev, link->msta_link.wcid.phy_idx);
- if (!phy)
- continue;
-
- mt7996_vif_link_destroy(phy, link, vif, NULL);
- }
-
+ mt7996_vif_links_destroy(dev, vif, mvif->mt76.valid_links, NULL);
mutex_unlock(&dev->mt76.mutex);
ieee80211_iterate_active_interfaces_mtx(hw, 0, mt7996_remove_iter,
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 10/25] wifi: mt76: mt7996: destroy station links before the MLD group is freed
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (7 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 09/25] wifi: mt76: mt7996: factor the vif link teardown out of remove_interface Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 11/25] wifi: mt76: mt7996: send MLD link operations for an AP MLD only Felix Fietkau
` (14 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Shayne Chen
Since the link teardown moved to mt7996_remove_interface(), a station
link stays in the firmware after its chanctx is unassigned. When the
links of the vif go to 0, mt7996_change_vif_links() frees the MLD group
and remap index in the driver, while the firmware BSS of each link
still names them. The firmware has no MLD link removal for a station
link, so the next vif that takes the same index shares it with a stale
BSS until the station vif is removed. That removal then uses
&vif->bss_conf instead of the link conf.
Destroy the station links when the vif leaves MLO, with the link confs
that mac80211 passes, and before the indexes are freed. mac80211
releases the chanctx of each removed link before it calls the driver,
so all links of the vif are unassigned at this point. AP links are not
affected: they leave their group at unassign through the MLD link
operation. First abort a remain-on-channel that holds one of the
destroyed links, since mt76_unassign_vif_chanctx() aborts only a scan
and mac80211 ends a remain-on-channel only when the interface stops.
Fixes: 08813703ac41 ("wifi: mt76: mt7996: Destroy vif active links in mt7996_remove_interface()")
Co-developed-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index cc8ce83f8b2d..1ca4d9e099fe 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -2995,6 +2995,26 @@ mt7996_net_setup_tc(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
}
#endif
+static void
+mt7996_vif_link_roc_abort(struct mt7996_dev *dev, struct ieee80211_vif *vif)
+{
+ struct mt7996_vif *mvif = (struct mt7996_vif *)vif->drv_priv;
+ struct mt76_phy *phy = mvif->mt76.roc_phy;
+ struct mt76_vif_link *mlink;
+ bool abort;
+
+ if (!phy)
+ return;
+
+ mutex_lock(&dev->mt76.mutex);
+ mlink = phy->roc_link;
+ abort = phy->roc_vif == vif && mlink && !mlink->offchannel;
+ mutex_unlock(&dev->mt76.mutex);
+
+ if (abort)
+ mt76_abort_roc(phy);
+}
+
static int
mt7996_change_vif_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
u16 old_links, u16 new_links,
@@ -3002,8 +3022,15 @@ mt7996_change_vif_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
{
struct mt7996_dev *dev = mt7996_hw_dev(hw);
struct mt7996_vif *mvif = (struct mt7996_vif *)vif->drv_priv;
+ bool sta_links_destroy;
int ret = 0;
+ sta_links_destroy = !vif->valid_links &&
+ vif->type == NL80211_IFTYPE_STATION;
+ /* a ROC can hold a station link that is destroyed below */
+ if (sta_links_destroy)
+ mt7996_vif_link_roc_abort(dev, vif);
+
mutex_lock(&dev->mt76.mutex);
if (!old_links) {
@@ -3030,6 +3057,13 @@ mt7996_change_vif_links(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
if (new_links)
goto out;
+ /* the firmware supports no MLD link removal for a station link, so
+ * remove its BSS before the group and remap index can be reused
+ */
+ if (sta_links_destroy)
+ mt7996_vif_links_destroy(dev, vif, mvif->mt76.valid_links,
+ old);
+
dev->mld_idx_mask &= ~BIT_ULL(mvif->mld_group_idx);
dev->mld_remap_idx_mask &= ~BIT_ULL(mvif->mld_remap_idx);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 11/25] wifi: mt76: mt7996: send MLD link operations for an AP MLD only
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (8 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 10/25] wifi: mt76: mt7996: destroy station links before the MLD group is freed Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 12/25] wifi: mt76: mt7996: set up a vif link again on another radio Felix Fietkau
` (13 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_remove() sends the MLD reconfiguration stop and the MLD
link removal for every AP link, and mt7996_vif_link_add() sends the MLD
link add for every AP link it holds already. A non-MLD AP has no MLD
group in the firmware: its BSS carries group_mld_id 0xff. The firmware
refuses both removal commands at each teardown of a non-MLD AP:
mt7996e 0000:01:00.0: Message 00120082 (seq 1) cid 0082 failed: firmware status 0xc0000001
mt7996e 0000:01:00.0: Message 001a0002 (seq 2) cid 0002 failed: firmware status 0xc0000001
The add sends the group_mld_id and remap_idx of the vif, which a vif
that never was an MLD holds at 0, the first index that an AP MLD gets.
Send the MLD link operations only for an AP MLD, matching the MLD tag
of mt7996_mcu_add_bss_info(). A non-MLD AP link that is added again
programs its BSS as a station link does.
Fixes: e7ec71d9f8fa ("wifi: mt76: mt7996: Add mcu APIs to enable/disable vif links.")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 1ca4d9e099fe..d22abbd543b7 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -328,7 +328,7 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
if (ret)
return ret;
- if (vif->type == NL80211_IFTYPE_AP)
+ if (vif->type == NL80211_IFTYPE_AP && ieee80211_vif_is_mld(vif))
return mt7996_mcu_mld_link_oper(dev, link_conf, link,
true);
@@ -514,7 +514,7 @@ void mt7996_vif_link_remove(struct mt76_phy *mphy, struct ieee80211_vif *vif,
if (mlink->wcid->offchannel) {
mt7996_vif_link_destroy(phy, link, vif, link_conf);
} else {
- if (vif->type == NL80211_IFTYPE_AP) {
+ if (vif->type == NL80211_IFTYPE_AP && ieee80211_vif_is_mld(vif)) {
mt7996_mcu_mld_reconf_stop_link(phy->dev, vif,
BIT(link_id));
mt7996_mcu_mld_link_oper(phy->dev, link_conf, link,
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 12/25] wifi: mt76: mt7996: set up a vif link again on another radio
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (9 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 11/25] wifi: mt76: mt7996: send MLD link operations for an AP MLD only Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 13/25] wifi: mt76: mt7996: describe an MLD peer with the PEER_MLD command Felix Fietkau
` (12 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
Since the link teardown moved to mt7996_remove_interface(), a link stays
in mvif->mt76.valid_links after its chanctx is unassigned. When the next
chanctx assignment puts the link on another radio, mt7996_vif_link_add()
takes the reuse branch and programs DEV_INFO and BSS_INFO on the new
radio with the OMAC index and the band of the old radio. The firmware
then holds the BSS of the link on the wrong band, and the STA_REC of a
station on that link is created under it. After the station
disconnects, the firmware keeps that record, so every later station
that gets the same WTBL index is refused:
mt7996e 0000:01:00.0: Message 001a0003 (seq 1) cid 0003 failed: firmware status 0x10003
hostapd then fails with "Could not set STA to kernel driver" for each
new station on that index until the firmware restarts. Examples are a
non-AP interface that used link 0 on 6 GHz and associates as an MLD
with link 0 on 2.4 GHz, and an AP that is stopped and started on
another band, both without an interface down.
Tear such a link down on its old radio and add it again on the new one.
If the add on the new radio fails, free the link, since it is no longer
a valid link and mt7996_remove_interface() destroys only valid links.
Keep the reuse branch for a link that stays on its band, and for
mt76_switch_vif_chanctx(), which moves a link to another radio during a
channel switch with its stations in place.
Fixes: 08813703ac41 ("wifi: mt76: mt7996: Destroy vif active links in mt7996_remove_interface()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../net/wireless/mediatek/mt76/mt7996/main.c | 145 ++++++++++++------
1 file changed, 96 insertions(+), 49 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index d22abbd543b7..3384e46e7b05 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -305,6 +305,76 @@ mt7996_key_iter(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
WARN_ON(mt7996_set_hw_key(hw, it->cmd, vif, sta, it->link_id, key));
}
+static void mt7996_vif_link_teardown(struct mt7996_phy *phy,
+ struct mt7996_vif_link *link,
+ struct ieee80211_vif *vif,
+ struct ieee80211_bss_conf *link_conf)
+{
+ struct mt7996_vif *mvif = (struct mt7996_vif *)vif->drv_priv;
+ struct mt7996_sta_link *msta_link = &link->msta_link;
+ unsigned int link_id = msta_link->wcid.link_id;
+ struct mt76_vif_link *mlink = &link->mt76;
+ struct mt7996_dev *dev = phy->dev;
+ int idx = msta_link->wcid.idx;
+
+ if (!link_conf)
+ link_conf = &vif->bss_conf;
+
+ if (link->bmc_sta_added)
+ mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
+ CONN_STATE_DISCONNECT, false);
+ mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink, msta_link, false);
+ mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, false);
+
+ rcu_assign_pointer(dev->mt76.wcid[idx], NULL);
+
+ dev->mt76.vif_mask &= ~BIT_ULL(mlink->idx);
+ dev->mld_idx_mask &= ~BIT_ULL(link->mld_idx);
+ phy->omac_mask &= ~BIT_ULL(mlink->omac_idx);
+ if (!mlink->wcid->offchannel)
+ mvif->mt76.valid_links &= ~BIT(link_id);
+
+ spin_lock_bh(&dev->mt76.sta_poll_lock);
+ if (!list_empty(&msta_link->wcid.poll_list))
+ list_del_init(&msta_link->wcid.poll_list);
+ spin_unlock_bh(&dev->mt76.sta_poll_lock);
+
+ mt76_wcid_cleanup(&dev->mt76, &msta_link->wcid);
+}
+
+static void mt7996_vif_link_free(struct mt7996_vif_link *link,
+ struct ieee80211_vif *vif)
+{
+ unsigned int link_id = link->msta_link.wcid.link_id;
+ struct mt76_vif_link *mlink = &link->mt76;
+
+ if (mlink == (struct mt76_vif_link *)vif->drv_priv ||
+ mlink->wcid->offchannel)
+ return;
+
+ rcu_assign_pointer(mlink->mvif->link[link_id], NULL);
+ kfree_rcu(mlink, rcu_head);
+}
+
+static int mt7996_vif_link_reuse(struct mt7996_phy *phy,
+ struct ieee80211_vif *vif,
+ struct ieee80211_bss_conf *link_conf,
+ struct mt7996_vif_link *link)
+{
+ struct mt76_vif_link *mlink = &link->mt76;
+ int ret;
+
+ ret = mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, true);
+ if (ret)
+ return ret;
+
+ if (vif->type == NL80211_IFTYPE_AP && ieee80211_vif_is_mld(vif))
+ return mt7996_mcu_mld_link_oper(phy->dev, link_conf, link, true);
+
+ return mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink,
+ &link->msta_link, true);
+}
+
int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
struct ieee80211_bss_conf *link_conf,
struct mt76_vif_link *mlink)
@@ -320,33 +390,38 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
.link_id = link_conf->link_id,
};
int mld_idx, idx, ret;
+ bool moved = false;
if ((mvif->mt76.valid_links & BIT(link_conf->link_id)) &&
!mlink->offchannel) {
- /* update the link address */
- ret = mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, true);
- if (ret)
- return ret;
+ struct mt7996_phy *old_phy;
- if (vif->type == NL80211_IFTYPE_AP && ieee80211_vif_is_mld(vif))
- return mt7996_mcu_mld_link_oper(dev, link_conf, link,
- true);
+ /* mt76_switch_vif_chanctx() sets the new chanctx only after
+ * the link moved
+ */
+ if (mlink->band_idx == band_idx ||
+ mt76_vif_link_phy(mlink) != mphy)
+ return mt7996_vif_link_reuse(phy, vif, link_conf, link);
- return mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink,
- msta_link, true);
+ old_phy = __mt7996_phy(dev, msta_link->wcid.phy_idx);
+ if (old_phy) {
+ mt7996_vif_link_teardown(old_phy, link, vif, link_conf);
+ moved = true;
+ }
}
+ ret = -ENOSPC;
mlink->idx = __ffs64(~dev->mt76.vif_mask);
if (mlink->idx >= mt7996_max_interface_num(dev))
- return -ENOSPC;
+ goto error;
idx = get_omac_idx(vif->type, phy->omac_mask);
if (idx < 0)
- return -ENOSPC;
+ goto error;
mld_idx = get_own_mld_idx(dev->mld_idx_mask, false);
if (mld_idx < 0)
- return -ENOSPC;
+ goto error;
link->mld_idx = mld_idx;
link->bmc_sta_added = false;
@@ -360,7 +435,7 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
ret = mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, true);
if (ret)
- return ret;
+ goto error;
dev->mt76.vif_mask |= BIT_ULL(mlink->idx);
dev->mld_idx_mask |= BIT_ULL(link->mld_idx);
@@ -426,6 +501,12 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
}
return 0;
+
+error:
+ if (moved)
+ mt7996_vif_link_free(link, vif);
+
+ return ret;
}
static void mt7996_vif_link_destroy(struct mt7996_phy *phy,
@@ -433,42 +514,8 @@ static void mt7996_vif_link_destroy(struct mt7996_phy *phy,
struct ieee80211_vif *vif,
struct ieee80211_bss_conf *link_conf)
{
- struct mt7996_vif *mvif = (struct mt7996_vif *)vif->drv_priv;
- struct mt7996_sta_link *msta_link = &link->msta_link;
- unsigned int link_id = msta_link->wcid.link_id;
- struct mt76_vif_link *mlink = &link->mt76;
- struct mt7996_dev *dev = phy->dev;
- int idx = msta_link->wcid.idx;
-
- if (!link_conf)
- link_conf = &vif->bss_conf;
-
- if (link->bmc_sta_added)
- mt7996_mcu_add_sta(dev, link_conf, NULL, link, NULL,
- CONN_STATE_DISCONNECT, false);
- mt7996_mcu_add_bss_info(phy, vif, link_conf, mlink, msta_link, false);
- mt7996_mcu_add_dev_info(phy, vif, link_conf, mlink, false);
-
- rcu_assign_pointer(dev->mt76.wcid[idx], NULL);
-
- dev->mt76.vif_mask &= ~BIT_ULL(mlink->idx);
- dev->mld_idx_mask &= ~BIT_ULL(link->mld_idx);
- phy->omac_mask &= ~BIT_ULL(mlink->omac_idx);
- if (!mlink->wcid->offchannel)
- mvif->mt76.valid_links &= ~BIT(link_id);
-
- spin_lock_bh(&dev->mt76.sta_poll_lock);
- if (!list_empty(&msta_link->wcid.poll_list))
- list_del_init(&msta_link->wcid.poll_list);
- spin_unlock_bh(&dev->mt76.sta_poll_lock);
-
- mt76_wcid_cleanup(&dev->mt76, &msta_link->wcid);
-
- if (mlink != (struct mt76_vif_link *)vif->drv_priv &&
- !mlink->wcid->offchannel) {
- rcu_assign_pointer(mlink->mvif->link[link_id], NULL);
- kfree_rcu(mlink, rcu_head);
- }
+ mt7996_vif_link_teardown(phy, link, vif, link_conf);
+ mt7996_vif_link_free(link, vif);
}
static void
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 13/25] wifi: mt76: mt7996: describe an MLD peer with the PEER_MLD command
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (10 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 12/25] wifi: mt76: mt7996: set up a vif link again on another radio Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 14/25] wifi: mt76: mt7915: honour the valid flag in efuse block reads Felix Fietkau
` (11 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Shayne Chen
The firmware learns about an MLD peer only from the MLD_SETUP TLV of
STA_REC_UPDATE. The vendor driver for the same firmware describes the
peer with the PEER_MLD command instead: a PEER_MLD_INFO TLV with the
links of the peer, its primary, secondary and setup WCID, and a
PEER_MLD_CAP TLV with the A-MSDU limits. It flags every MLD_SETUP TLV
with EXTRA_INFO_MLD_SKIP, so that the firmware takes the MLD setup from
PEER_MLD.
Send PEER_MLD at the association of an MLO peer, before the STA_REC of
its first link. The firmware MLD index of the peer is the WCID of its
primary link. The teardown stays with STA_REC_MLD_OFF.
Flag an MLD_SETUP TLV only while its links are the links that PEER_MLD
described. mac80211 activates the partner links of a station after the
association, so PEER_MLD of a station covers only the assoc link. The
firmware refuses a flagged MLD_SETUP for the full link set, and
change_sta_links then fails with a warning in
_ieee80211_set_active_links(). An unflagged MLD_SETUP sets up the links
that join or leave later, as before.
While the PEER_MLD description is valid, keep its MLD index as the
setup and primary WCID of each MLD_SETUP, also after mt7996 makes
another link the primary link when the first one goes away. With the
WCID of the new primary link, the next association of the peer gets no
MLD record from its PEER_MLD, and the firmware refuses the MLD_SETUP of
that association:
NULL MLDREC of WCID 1
A link keeps its WCID until the station is removed, also after its link
station goes. TX still moves to another link; only the MLD description
in the firmware keeps the primary of the association.
Co-developed-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Shayne Chen <shayne.chen@mediatek.com>
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
.../wireless/mediatek/mt76/mt76_connac_mcu.h | 1 +
.../net/wireless/mediatek/mt76/mt7996/main.c | 8 ++
.../net/wireless/mediatek/mt76/mt7996/mcu.c | 110 +++++++++++++++++-
.../net/wireless/mediatek/mt76/mt7996/mcu.h | 59 +++++++++-
.../wireless/mediatek/mt76/mt7996/mt7996.h | 4 +
5 files changed, 180 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
index e9a7542c07dc..bb98a6eacac4 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mcu.h
@@ -1363,6 +1363,7 @@ enum {
MCU_UNI_CMD_FACT_CAL = 0x7c,
MCU_UNI_CMD_RADIO_STATUS = 0x80,
MCU_UNI_CMD_MLD = 0x82,
+ MCU_UNI_CMD_PEER_MLD = 0x83,
MCU_UNI_CMD_SDO = 0x88,
MCU_UNI_CMD_AXIDMA = 0xa2,
};
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 3384e46e7b05..e97af1828444 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1531,6 +1531,7 @@ mt7996_mac_sta_add(struct mt7996_dev *dev, struct ieee80211_vif *vif,
msta->deflink_id = IEEE80211_LINK_UNSPECIFIED;
msta->seclink_id = IEEE80211_LINK_UNSPECIFIED;
msta->conn_state = CONN_STATE_DISCONNECT;
+ msta->peer_mld_links = 0;
msta->ba_tx_mask = 0;
msta->ba_rx_mask = 0;
msta->vif = mvif;
@@ -1649,6 +1650,12 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
mutex_lock(&dev->mt76.mutex);
+ if (ev == MT76_STA_EVENT_ASSOC && sta->mlo) {
+ err = mt7996_mcu_set_peer_mld(dev, vif, sta);
+ if (err)
+ goto out;
+ }
+
err = mt7996_mac_sta_links_event(dev, vif, sta,
mt7996_mac_sta_links(dev, vif, sta),
ev);
@@ -1664,6 +1671,7 @@ mt7996_mac_sta_event(struct mt7996_dev *dev, struct ieee80211_vif *vif,
break;
case MT76_STA_EVENT_DISASSOC:
msta->conn_state = CONN_STATE_DISCONNECT;
+ msta->peer_mld_links = 0;
mt7996_mac_sta_clear_connected(dev, msta);
break;
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index ce37b6b65918..34ac25c22b01 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -2803,13 +2803,19 @@ mt7996_mcu_sta_mld_setup_tlv(struct mt7996_dev *dev, struct sk_buff *skb,
mld_setup = (struct sta_rec_mld_setup *)tlv;
memcpy(mld_setup->mld_addr, sta->addr, ETH_ALEN);
mld_setup->setup_wcid = cpu_to_le16(msta_link->wcid.idx);
- mld_setup->primary_id = cpu_to_le16(msta_link->wcid.idx);
+ /* the firmware keeps the MLD record of PEER_MLD under fw_mld_idx */
+ if (msta->peer_mld_links)
+ mld_setup->setup_wcid = cpu_to_le16(msta->fw_mld_idx);
+ mld_setup->primary_id = mld_setup->setup_wcid;
if (links & BIT(msta->seclink_id))
seclink_id = msta->seclink_id;
msta_link = mt7996_sta_link_protected(dev, msta, seclink_id);
mld_setup->seconed_id = cpu_to_le16(msta_link->wcid.idx);
mld_setup->link_num = nlinks;
+ /* PEER_MLD_INFO holds only the links of the association */
+ if (links == msta->peer_mld_links)
+ mld_setup->extra_info = EXTRA_INFO_MLD_SKIP;
mld_setup_link = (struct mld_setup_link *)mld_setup->link_info;
for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
@@ -2824,6 +2830,108 @@ mt7996_mcu_sta_mld_setup_tlv(struct mt7996_dev *dev, struct sk_buff *skb,
}
}
+static u32 mt7996_mcu_peer_mld_amsdu_len(u16 max_amsdu_len)
+{
+ switch (max_amsdu_len) {
+ case IEEE80211_MAX_MPDU_LEN_VHT_11454:
+ return IEEE80211_VHT_CAP_MAX_MPDU_LENGTH_11454;
+ case IEEE80211_MAX_MPDU_LEN_HT_7935:
+ case IEEE80211_MAX_MPDU_LEN_VHT_7991:
+ return IEEE80211_VHT_CAP_MAX_MPDU_LENGTH_7991;
+ default:
+ return IEEE80211_VHT_CAP_MAX_MPDU_LENGTH_3895;
+ }
+}
+
+int mt7996_mcu_set_peer_mld(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta)
+{
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+ unsigned long links = mt7996_mac_sta_links(dev, vif, sta);
+ unsigned int nlinks = hweight16(links);
+ struct peer_mld_cap_entry *cap_entry;
+ struct peer_mld_link_info *link_info;
+ struct mt7996_sta_link *msta_link;
+ u8 seclink_id = msta->deflink_id;
+ struct peer_mld_req_hdr *hdr;
+ u16 max_amsdu_len = U16_MAX;
+ struct peer_mld_info *info;
+ struct peer_mld_cap *cap;
+ unsigned int link_id;
+ struct sk_buff *skb;
+ struct tlv *tlv;
+ u16 fw_mld_idx;
+ int len, ret;
+
+ if (!(links & BIT(msta->deflink_id)))
+ return 0;
+
+ len = sizeof(*hdr) + sizeof(*info) + nlinks * sizeof(*link_info) +
+ sizeof(*cap) + 2 * sizeof(*cap_entry);
+ skb = mt76_mcu_msg_alloc(&dev->mt76, NULL, len);
+ if (!skb)
+ return -ENOMEM;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, msta->deflink_id);
+
+ fw_mld_idx = msta_link->wcid.idx;
+ hdr = skb_put_zero(skb, sizeof(*hdr));
+ hdr->fw_mld_idx = cpu_to_le16(fw_mld_idx);
+ memcpy(hdr->peer_mld_addr, sta->addr, ETH_ALEN);
+
+ tlv = mt7996_mcu_add_uni_tlv(skb, UNI_CMD_PEER_MLD_INFO,
+ sizeof(*info) +
+ nlinks * sizeof(*link_info));
+ info = (struct peer_mld_info *)tlv;
+ info->link_num = nlinks;
+ info->primary_id = cpu_to_le16(msta_link->wcid.idx);
+ info->setup_wcid = cpu_to_le16(msta_link->wcid.idx);
+
+ if (links & BIT(msta->seclink_id))
+ seclink_id = msta->seclink_id;
+ msta_link = mt7996_sta_link_protected(dev, msta, seclink_id);
+ info->second_id = cpu_to_le16(msta_link->wcid.idx);
+
+ link_info = info->link_info;
+ for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_link_sta *link_sta;
+ struct mt7996_vif_link *link;
+
+ msta_link = mt7996_sta_link_protected(dev, msta, link_id);
+ link = mt7996_vif_link(dev, vif, link_id);
+ link_sta = link_sta_dereference_protected(sta, link_id);
+
+ max_amsdu_len = min(max_amsdu_len, link_sta->agg.max_amsdu_len);
+
+ link_info->fw_link_id = cpu_to_le16(msta_link->wcid.idx);
+ link_info->link_id = link_id;
+ link_info->bss_idx = link->mt76.idx;
+ link_info->wlan_idx = cpu_to_le16(msta_link->wcid.idx);
+ link_info->band_idx = link->mt76.band_idx;
+ link_info++;
+ }
+
+ tlv = mt7996_mcu_add_uni_tlv(skb, UNI_CMD_PEER_MLD_CAP,
+ sizeof(*cap) + 2 * sizeof(*cap_entry));
+ cap = (struct peer_mld_cap *)tlv;
+
+ cap_entry = cap->entry;
+ cap_entry->type = cpu_to_le32(UNI_CMD_PEER_MLD_CAP_TYPE_AMSDU_MAX_LEN);
+ cap_entry->val = cpu_to_le32(mt7996_mcu_peer_mld_amsdu_len(max_amsdu_len));
+ cap_entry++;
+ cap_entry->type = cpu_to_le32(UNI_CMD_PEER_MLD_CAP_TYPE_AMSDU_MAX_CNT);
+ cap_entry->val = cpu_to_le32(31);
+
+ ret = mt76_mcu_skb_send_msg(&dev->mt76, skb,
+ MCU_WMWA_UNI_CMD(PEER_MLD), true);
+ if (!ret) {
+ msta->peer_mld_links = links;
+ msta->fw_mld_idx = fw_mld_idx;
+ }
+
+ return ret;
+}
+
static void
mt7996_mcu_sta_eht_mld_tlv(struct mt7996_dev *dev, struct sk_buff *skb,
struct ieee80211_sta *sta)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
index 70ed57d9bf9a..2fdf82394355 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.h
@@ -730,7 +730,8 @@ struct sta_rec_mld_setup {
__le16 seconed_id;
__le16 setup_wcid;
u8 link_num;
- u8 info;
+#define EXTRA_INFO_MLD_SKIP BIT(2)
+ u8 extra_info;
u8 __rsv[2];
u8 link_info[];
} __packed;
@@ -773,6 +774,62 @@ enum {
UNI_CMD_MLD_RECONF_STOP_LINK = 0x04,
};
+struct peer_mld_req_hdr {
+ u8 ver;
+ u8 peer_mld_addr[ETH_ALEN];
+ u8 rsv;
+ __le16 fw_mld_idx;
+ __le16 flag;
+ u8 buf[];
+} __packed;
+
+struct peer_mld_link_info {
+ __le16 fw_link_id;
+ u8 link_id;
+ u8 bss_idx;
+ __le16 wlan_idx;
+ u8 band_idx;
+ u8 rsv;
+} __packed;
+
+struct peer_mld_info {
+ __le16 tag;
+ __le16 len;
+ u8 ver;
+ u8 link_num;
+ u8 rsv[2];
+ __le16 primary_id;
+ __le16 second_id;
+ __le16 setup_wcid;
+ u8 rsv2[2];
+ struct peer_mld_link_info link_info[];
+} __packed;
+
+struct peer_mld_cap_entry {
+ __le32 type;
+ __le32 val;
+} __packed;
+
+struct peer_mld_cap {
+ __le16 tag;
+ __le16 len;
+ u8 ver;
+ u8 rsv;
+ __le16 bitmap;
+ u8 rsv2[8];
+ struct peer_mld_cap_entry entry[];
+} __packed;
+
+enum {
+ UNI_CMD_PEER_MLD_INFO = 0x01,
+ UNI_CMD_PEER_MLD_CAP = 0x02,
+};
+
+enum {
+ UNI_CMD_PEER_MLD_CAP_TYPE_AMSDU_MAX_LEN = 0x01,
+ UNI_CMD_PEER_MLD_CAP_TYPE_AMSDU_MAX_CNT = 0x02,
+};
+
struct hdr_trans_en {
__le16 tag;
__le16 len;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
index 1f1dff9f6610..0c6a3dba2e03 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mt7996.h
@@ -276,6 +276,8 @@ struct mt7996_sta {
u8 deflink_id;
u8 seclink_id;
u8 conn_state;
+ u16 peer_mld_links;
+ u16 fw_mld_idx;
unsigned long wcid_flags;
@@ -753,6 +755,8 @@ int mt7996_mcu_add_sta(struct mt7996_dev *dev,
struct mt7996_vif_link *link,
struct mt7996_sta_link *msta_link,
int conn_state, bool newly);
+int mt7996_mcu_set_peer_mld(struct mt7996_dev *dev, struct ieee80211_vif *vif,
+ struct ieee80211_sta *sta);
int mt7996_mcu_teardown_mld_sta(struct mt7996_dev *dev,
struct mt7996_vif_link *link,
struct mt7996_sta_link *msta_link);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 14/25] wifi: mt76: mt7915: honour the valid flag in efuse block reads
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (11 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 13/25] wifi: mt76: mt7996: describe an MLD peer with the PEER_MLD command Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 15/25] wifi: mt76: mt7996: skip MLD address translation for encap offload frames Felix Fietkau
` (10 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7915_mcu_get_eeprom() ignores the valid flag of the EFUSE_ACCESS
response, and copies undefined data for an unwritten efuse block.
Return -ENODATA for an invalid block. mt7915_eeprom_load() skips the
block and leaves its eeprom range untouched.
mt7915_eeprom_has_background_radar() then reports no background radar
on MT7906.
Fixes: e57b7901469f ("mt76: add mac80211 driver for MT7915 PCIe-based chipsets")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7915/eeprom.c | 2 +-
drivers/net/wireless/mediatek/mt76/mt7915/mcu.c | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/eeprom.c b/drivers/net/wireless/mediatek/mt76/mt7915/eeprom.c
index 145d14a1012d..0ab02580423f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/eeprom.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/eeprom.c
@@ -154,7 +154,7 @@ static int mt7915_eeprom_load(struct mt7915_dev *dev)
block_num = DIV_ROUND_UP(eeprom_size, eeprom_blk_size);
for (i = 0; i < block_num; i++) {
ret = mt7915_mcu_get_eeprom(dev, i * eeprom_blk_size, NULL);
- if (ret < 0)
+ if (ret < 0 && ret != -ENODATA)
return ret;
}
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
index 67b470f4bd9b..e66968b819da 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
@@ -2952,6 +2952,11 @@ int mt7915_mcu_get_eeprom(struct mt7915_dev *dev, u32 offset, u8 *read_buf)
}
res = (struct mt7915_mcu_eeprom_info *)skb->data;
+ if (!le32_to_cpu(res->valid)) {
+ dev_kfree_skb(skb);
+ return -ENODATA;
+ }
+
if (!buf) {
u32 addr = le32_to_cpu(res->addr);
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 15/25] wifi: mt76: mt7996: skip MLD address translation for encap offload frames
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (12 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 14/25] wifi: mt76: mt7915: honour the valid flag in efuse block reads Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 16/25] wifi: mt76: mt7921: honour the valid flag in efuse block reads Felix Fietkau
` (9 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7996_tx() translates the MLD addresses of a frame into link addresses
for an MLD interface. It casts skb->data to struct ieee80211_hdr, but a
frame with IEEE80211_TX_CTL_HW_80211_ENCAP set carries an Ethernet
header, so the writes to addr1 and addr2 corrupt the Ethernet header
and the start of the payload.
Skip the translation for such a frame.
This is currently not reachable, because mac80211 queues encap offload
frames to a TXQ and mt7996_tx() receives them only when they bypass the
TXQ. Found by inspection.
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index e97af1828444..f715817243a6 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -2089,7 +2089,8 @@ static void mt7996_tx(struct ieee80211_hw *hw,
if (link_id == IEEE80211_LINK_UNSPECIFIED)
link_id = deflink_id;
- if (vif && ieee80211_vif_is_mld(vif)) {
+ if (vif && ieee80211_vif_is_mld(vif) &&
+ !(info->flags & IEEE80211_TX_CTL_HW_80211_ENCAP)) {
struct ieee80211_bss_conf *link_conf;
if (msta) {
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 16/25] wifi: mt76: mt7921: honour the valid flag in efuse block reads
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (13 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 15/25] wifi: mt76: mt7996: skip MLD address translation for encap offload frames Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 17/25] wifi: mt76: mt7915: upload the CSA beacon only once Felix Fietkau
` (8 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7921_mcu_read_eeprom() ignores the valid flag of the EFUSE_ACCESS
response. For an unwritten efuse block, mt7921_load_clc() thus selects
the CLC records by an undefined hardware type.
Return -ENODATA for an invalid block, like mt7915_mcu_get_eeprom().
mt7921_load_clc() then keeps the hardware type 0, which it also uses on
USB and SDIO, where it does not read the field.
Fixes: 23bdc5d8cadf ("wifi: mt76: mt7921: introduce Country Location Control support")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7921/mcu.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7921/mcu.c
index a0caa77339ee..242a4415539e 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7921/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7921/mcu.c
@@ -111,6 +111,11 @@ static int mt7921_mcu_read_eeprom(struct mt792x_dev *dev, u32 offset, u8 *val)
}
res = (struct mt7921_mcu_eeprom_info *)skb->data;
+ if (!le32_to_cpu(res->valid)) {
+ dev_kfree_skb(skb);
+ return -ENODATA;
+ }
+
*val = res->data[offset % MT7921_EEPROM_BLOCK_SIZE];
dev_kfree_skb(skb);
@@ -453,7 +458,7 @@ static int mt7921_load_clc(struct mt792x_dev *dev, const char *fw_name)
if (mt76_is_mmio(&dev->mt76)) {
ret = mt7921_mcu_read_eeprom(dev, MT_EE_HW_TYPE, &hw_encap);
- if (ret)
+ if (ret && ret != -ENODATA)
return ret;
hw_encap = u8_get_bits(hw_encap, MT_EE_HW_TYPE_ENCAP);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 17/25] wifi: mt76: mt7915: upload the CSA beacon only once
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (14 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 16/25] wifi: mt76: mt7921: honour the valid flag in efuse block reads Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 18/25] wifi: mt76: read the chanctx of a link once Felix Fietkau
` (7 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
At the start of a CSA, __ieee80211_channel_switch() reports
BSS_CHANGED_BEACON with csa_active set and then calls
.channel_switch_beacon. mt7915_bss_info_changed() and
mt7915_channel_switch_beacon() thus both upload the same CSA beacon,
and each upload passes the initial count of the template to the
firmware.
Drop mt7915_channel_switch_beacon(). mac80211 calls it only after a
BSS_CHANGED_BEACON report, so mt7915_bss_info_changed() already uploads
the CSA beacon, like mt7996 does.
Fixes: e57b7901469f ("mt76: add mac80211 driver for MT7915 PCIe-based chipsets")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7915/main.c | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/main.c b/drivers/net/wireless/mediatek/mt76/mt7915/main.c
index 556f8948abac..8cf7fde1a197 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/main.c
@@ -737,18 +737,6 @@ mt7915_stop_ap(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
mutex_unlock(&dev->mt76.mutex);
}
-static void
-mt7915_channel_switch_beacon(struct ieee80211_hw *hw,
- struct ieee80211_vif *vif,
- struct cfg80211_chan_def *chandef)
-{
- struct mt7915_dev *dev = mt7915_hw_dev(hw);
-
- mutex_lock(&dev->mt76.mutex);
- mt7915_mcu_add_beacon(hw, vif, true, BSS_CHANGED_BEACON);
- mutex_unlock(&dev->mt76.mutex);
-}
-
int mt7915_mac_sta_add(struct mt76_dev *mdev, struct ieee80211_vif *vif,
struct ieee80211_sta *sta)
{
@@ -1803,7 +1791,6 @@ const struct ieee80211_ops mt7915_ops = {
.release_buffered_frames = mt76_release_buffered_frames,
.get_txpower = mt76_get_txpower,
.set_sar_specs = mt7915_set_sar_specs,
- .channel_switch_beacon = mt7915_channel_switch_beacon,
.get_stats = mt7915_get_stats,
.get_et_sset_count = mt7915_get_et_sset_count,
.get_et_stats = mt7915_get_et_stats,
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 18/25] wifi: mt76: read the chanctx of a link once
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (15 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 17/25] wifi: mt76: mt7915: upload the CSA beacon only once Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 19/25] wifi: mt76: restart the beacon monitor of a link that gets a chanctx again Felix Fietkau
` (6 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt76_vif_link_phy() and mt76_vif_phy() test mlink->ctx and then load it
again to reach the mt76_chanctx. mt7996_tx(), mt7996_mac_sta_poll() from
the TX free path, mt76_rx_beacon(), the beacon monitor and
mt76_offchannel_notify() call mt76_vif_link_phy() without dev->mutex,
while mt76_unassign_vif_chanctx() can set mlink->ctx to NULL in between.
The second load can then return NULL.
Load mlink->ctx once with acquire semantics, and publish it with
release semantics in the chanctx functions, so that a reader that sees
a chanctx also sees the link state written before it. Let
mt76_vif_phy() use mt76_vif_link_phy() instead of its own copy.
Fixes: 69d54ce7491d ("wifi: mt76: mt7996: switch to single multi-radio wiphy")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/channel.c | 6 +++---
drivers/net/wireless/mediatek/mt76/mac80211.c | 7 +------
drivers/net/wireless/mediatek/mt76/mt76.h | 15 +++++++++++++--
3 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/channel.c b/drivers/net/wireless/mediatek/mt76/channel.c
index 39a5dca4ca7d..a44773b9a49e 100644
--- a/drivers/net/wireless/mediatek/mt76/channel.c
+++ b/drivers/net/wireless/mediatek/mt76/channel.c
@@ -137,7 +137,7 @@ int mt76_assign_vif_chanctx(struct ieee80211_hw *hw,
mlink_alloc = true;
}
- mlink->ctx = conf;
+ mt76_vif_link_ctx_set(mlink, conf);
ret = dev->drv->vif_link_add(phy, vif, link_conf, mlink);
if (ret) {
if (mlink_alloc)
@@ -179,7 +179,7 @@ void mt76_unassign_vif_chanctx(struct ieee80211_hw *hw,
goto out;
dev->drv->vif_link_remove(phy, vif, link_conf, mlink);
- mlink->ctx = NULL;
+ mt76_vif_link_ctx_set(mlink, NULL);
out:
mutex_unlock(&dev->mutex);
}
@@ -257,7 +257,7 @@ int mt76_switch_vif_chanctx(struct ieee80211_hw *hw,
goto out;
}
- mlink->ctx = v->new_ctx;
+ mt76_vif_link_ctx_set(mlink, v->new_ctx);
if (mlink->beacon_mon_interval)
WRITE_ONCE(mlink->beacon_mon_last, jiffies);
}
diff --git a/drivers/net/wireless/mediatek/mt76/mac80211.c b/drivers/net/wireless/mediatek/mt76/mac80211.c
index 89f289ac57a9..b2702c9079d9 100644
--- a/drivers/net/wireless/mediatek/mt76/mac80211.c
+++ b/drivers/net/wireless/mediatek/mt76/mac80211.c
@@ -876,16 +876,11 @@ struct mt76_phy *mt76_vif_phy(struct ieee80211_hw *hw,
struct ieee80211_vif *vif)
{
struct mt76_vif_link *mlink = (struct mt76_vif_link *)vif->drv_priv;
- struct mt76_chanctx *ctx;
if (!hw->wiphy->n_radio)
return hw->priv;
- if (!mlink->ctx)
- return NULL;
-
- ctx = (struct mt76_chanctx *)mlink->ctx->drv_priv;
- return ctx->phy;
+ return mt76_vif_link_phy(mlink);
}
EXPORT_SYMBOL_GPL(mt76_vif_phy);
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 9e851dea71d9..42a1f84c5ca9 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -2167,15 +2167,26 @@ mt76_vif_conf_link(struct mt76_dev *dev, struct ieee80211_vif *vif,
return mt76_dereference(mvif->link[link_conf->link_id], dev);
}
+static inline void
+mt76_vif_link_ctx_set(struct mt76_vif_link *mlink,
+ struct ieee80211_chanctx_conf *conf)
+{
+ /* pairs with smp_load_acquire() in mt76_vif_link_phy() */
+ smp_store_release(&mlink->ctx, conf);
+}
+
static inline struct mt76_phy *
mt76_vif_link_phy(struct mt76_vif_link *mlink)
{
+ struct ieee80211_chanctx_conf *conf;
struct mt76_chanctx *ctx;
- if (!mlink->ctx)
+ /* pairs with smp_store_release() in mt76_vif_link_ctx_set() */
+ conf = smp_load_acquire(&mlink->ctx);
+ if (!conf)
return NULL;
- ctx = (struct mt76_chanctx *)mlink->ctx->drv_priv;
+ ctx = (struct mt76_chanctx *)conf->drv_priv;
return ctx->phy;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 19/25] wifi: mt76: restart the beacon monitor of a link that gets a chanctx again
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (16 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 18/25] wifi: mt76: read the chanctx of a link once Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 20/25] wifi: mt76: publish the beacon monitor interval after its timestamp Felix Fietkau
` (5 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_remove() keeps a station link when its chanctx is
unassigned, for example when mac80211 deactivates the link. The link
keeps its beacon_mon_interval and the beacon_mon_last of its last
beacon. When the link is activated again, mt7996_vif_link_add() takes
the reuse branch, which does not touch the beacon monitor. Unless a
beacon on that link arrives first, the next mt76_beacon_mon_check()
reports a beacon loss for the stale timestamp, and the station
disconnects:
sta-mld0: CTRL-EVENT-BEACON-LOSS
sta-mld0: CTRL-EVENT-DISCONNECTED bssid=a0:ad:9f:70:fb:4a reason=4 locally_generated=1
Reset beacon_mon_last whenever a link gets a chanctx, which
mt76_switch_vif_chanctx() already did after a channel switch. Do it
before the link points to the chanctx, since mt76_beacon_mon_check()
runs without dev->mutex and can see the link while vif_link_add() waits
for the firmware.
Fixes: 08813703ac41 ("wifi: mt76: mt7996: Destroy vif active links in mt7996_remove_interface()")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/channel.c | 2 --
drivers/net/wireless/mediatek/mt76/mt76.h | 3 +++
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/channel.c b/drivers/net/wireless/mediatek/mt76/channel.c
index a44773b9a49e..da111d37c4f5 100644
--- a/drivers/net/wireless/mediatek/mt76/channel.c
+++ b/drivers/net/wireless/mediatek/mt76/channel.c
@@ -258,8 +258,6 @@ int mt76_switch_vif_chanctx(struct ieee80211_hw *hw,
}
mt76_vif_link_ctx_set(mlink, v->new_ctx);
- if (mlink->beacon_mon_interval)
- WRITE_ONCE(mlink->beacon_mon_last, jiffies);
}
out:
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 42a1f84c5ca9..fb1b99f56e9c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -2171,6 +2171,9 @@ static inline void
mt76_vif_link_ctx_set(struct mt76_vif_link *mlink,
struct ieee80211_chanctx_conf *conf)
{
+ if (conf && mlink->beacon_mon_interval)
+ WRITE_ONCE(mlink->beacon_mon_last, jiffies);
+
/* pairs with smp_load_acquire() in mt76_vif_link_phy() */
smp_store_release(&mlink->ctx, conf);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 20/25] wifi: mt76: publish the beacon monitor interval after its timestamp
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (17 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 19/25] wifi: mt76: restart the beacon monitor of a link that gets a chanctx again Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:23 ` [PATCH mt76-next 21/25] wifi: mt76: mt7996: keep rx header translation off while a monitor runs Felix Fietkau
` (4 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
mt7996_vif_link_add() and mt7996_vif_cfg_changed() start the beacon
monitor of a station link with a plain store of beacon_mon_interval,
followed by the store of beacon_mon_last. mt76_beacon_mon_iter() runs
without dev->mutex and reads the interval first. It can thus see the
new interval with the old timestamp, for example 0 for a new link, and
report a beacon loss at once.
Add helpers that start and stop the monitor: the timestamp is written
first, and the interval is published with release semantics, which
mt76_beacon_mon_iter() pairs with an acquire load. Read the interval
with READ_ONCE() in the other lockless users. This also removes the
duplicated interval computation in mt7996.
Fixes: 4df75606ece5 ("wifi: mt76: add per-link beacon monitoring for MLO")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mac80211.c | 19 +++++++++++++-----
drivers/net/wireless/mediatek/mt76/mt76.h | 16 +++++++++++++++
.../net/wireless/mediatek/mt76/mt7996/main.c | 20 +++++++------------
3 files changed, 37 insertions(+), 18 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mac80211.c b/drivers/net/wireless/mediatek/mt76/mac80211.c
index b2702c9079d9..76908bd835f2 100644
--- a/drivers/net/wireless/mediatek/mt76/mac80211.c
+++ b/drivers/net/wireless/mediatek/mt76/mac80211.c
@@ -2243,7 +2243,7 @@ mt76_offchannel_notify_iter(void *_data, u8 *mac, struct ieee80211_vif *vif)
if (!ieee80211_vif_is_mld(vif)) {
if (mt76_vif_link_phy(mlink) == data->phy) {
- if (!data->offchannel && mlink->beacon_mon_interval)
+ if (!data->offchannel && READ_ONCE(mlink->beacon_mon_interval))
WRITE_ONCE(mlink->beacon_mon_last, jiffies);
mt76_offchannel_send_nullfunc(data, vif, -1);
}
@@ -2260,7 +2260,7 @@ mt76_offchannel_notify_iter(void *_data, u8 *mac, struct ieee80211_vif *vif)
if (mt76_vif_link_phy(mlink) != data->phy)
continue;
- if (!data->offchannel && mlink->beacon_mon_interval)
+ if (!data->offchannel && READ_ONCE(mlink->beacon_mon_interval))
WRITE_ONCE(mlink->beacon_mon_last, jiffies);
mt76_offchannel_send_nullfunc(data, vif, link_id);
@@ -2308,7 +2308,7 @@ static void mt76_rx_beacon_iter(void *_data, u8 *mac,
mlink = (struct mt76_vif_link *)vif->drv_priv;
else
mlink = rcu_dereference(mvif->link[link_id]);
- if (!mlink || !mlink->beacon_mon_interval)
+ if (!mlink || !READ_ONCE(mlink->beacon_mon_interval))
continue;
if (mt76_vif_link_phy(mlink) != data->phy)
@@ -2363,11 +2363,20 @@ static void mt76_beacon_mon_iter(void *data, u8 *mac,
return;
for (link_id = 0; link_id < IEEE80211_MLD_MAX_NUM_LINKS; link_id++) {
+ u16 interval;
+
if (link_id == mvif->deflink_id)
mlink = (struct mt76_vif_link *)vif->drv_priv;
else
mlink = rcu_dereference(mvif->link[link_id]);
- if (!mlink || !mlink->beacon_mon_interval)
+ if (!mlink)
+ continue;
+
+ /* pairs with smp_store_release() in
+ * mt76_vif_link_beacon_mon_start()
+ */
+ interval = smp_load_acquire(&mlink->beacon_mon_interval);
+ if (!interval)
continue;
if (mt76_vif_link_phy(mlink) != phy)
@@ -2375,7 +2384,7 @@ static void mt76_beacon_mon_iter(void *data, u8 *mac,
if (time_after(jiffies,
READ_ONCE(mlink->beacon_mon_last) +
- MT76_BEACON_MON_MAX_MISS * mlink->beacon_mon_interval))
+ MT76_BEACON_MON_MAX_MISS * interval))
ieee80211_beacon_loss(vif);
}
}
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index fb1b99f56e9c..fce7bf2f0333 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -2178,6 +2178,22 @@ mt76_vif_link_ctx_set(struct mt76_vif_link *mlink,
smp_store_release(&mlink->ctx, conf);
}
+static inline void
+mt76_vif_link_beacon_mon_start(struct mt76_vif_link *mlink, u16 beacon_int)
+{
+ u16 interval = msecs_to_jiffies(ieee80211_tu_to_usec(beacon_int) / 1000);
+
+ WRITE_ONCE(mlink->beacon_mon_last, jiffies);
+ /* pairs with smp_load_acquire() in mt76_beacon_mon_iter() */
+ smp_store_release(&mlink->beacon_mon_interval, interval);
+}
+
+static inline void
+mt76_vif_link_beacon_mon_stop(struct mt76_vif_link *mlink)
+{
+ WRITE_ONCE(mlink->beacon_mon_interval, 0);
+}
+
static inline struct mt76_phy *
mt76_vif_link_phy(struct mt76_vif_link *mlink)
{
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index f715817243a6..6815f5192116 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -492,12 +492,9 @@ int mt7996_vif_link_add(struct mt76_phy *mphy, struct ieee80211_vif *vif,
if (vif->type == NL80211_IFTYPE_STATION) {
vif->driver_flags |= IEEE80211_VIF_BEACON_FILTER;
- if (vif->cfg.assoc && link_conf->beacon_int) {
- mlink->beacon_mon_interval =
- msecs_to_jiffies(ieee80211_tu_to_usec(
- link_conf->beacon_int) / 1000);
- WRITE_ONCE(mlink->beacon_mon_last, jiffies);
- }
+ if (vif->cfg.assoc && link_conf->beacon_int)
+ mt76_vif_link_beacon_mon_start(mlink,
+ link_conf->beacon_int);
}
return 0;
@@ -1027,12 +1024,9 @@ mt7996_vif_cfg_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
if (!link)
continue;
- if (vif->type == NL80211_IFTYPE_STATION) {
- link->mt76.beacon_mon_interval =
- msecs_to_jiffies(ieee80211_tu_to_usec(
- link_conf->beacon_int) / 1000);
- WRITE_ONCE(link->mt76.beacon_mon_last, jiffies);
- }
+ if (vif->type == NL80211_IFTYPE_STATION)
+ mt76_vif_link_beacon_mon_start(&link->mt76,
+ link_conf->beacon_int);
phy = mt7996_vif_link_phy(link);
if (!phy)
@@ -1053,7 +1047,7 @@ mt7996_vif_cfg_changed(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
link = mt7996_vif_link(dev, vif, link_id);
if (link)
- link->mt76.beacon_mon_interval = 0;
+ mt76_vif_link_beacon_mon_stop(&link->mt76);
}
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 21/25] wifi: mt76: mt7996: keep rx header translation off while a monitor runs
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (18 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 20/25] wifi: mt76: publish the beacon monitor interval after its timestamp Felix Fietkau
@ 2026-10-28 12:23 ` Felix Fietkau
2026-10-28 12:24 ` [PATCH mt76-next 22/25] wifi: mt76: mt7996: apply monitor mode again after a full reset Felix Fietkau
` (3 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:23 UTC (permalink / raw)
To: linux-wireless
MT_MDP_DCR0_RX_HDR_TRANS_EN is one bit for the whole chip, but
mt7996_set_monitor() writes it from the state of one radio. When a
monitor interface goes away that covers a radio no other monitor
covers, mt7996_remove_interface() turns sniffer mode off on that radio,
and header translation comes back on while a monitor of another radio
still runs. Frames translated to 802.3 do not reach monitor interfaces,
so that monitor stops receiving the data frames addressed to the
device.
Turn header translation off while any radio of the device is in monitor
mode, like mt7915_config() does with dev->monitor_mask.
Fixes: 947d63d8cd3b ("wifi: mt76: mt7996: Disable Rx hdr_trans in monitor mode")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 6815f5192116..ec7c5e8f3fda 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -612,6 +612,17 @@ static void mt7996_phy_set_rxfilter(struct mt7996_phy *phy)
mt76_clear(dev, MT_WF_RFCR1(phy->mt76->band_idx), ctl_flags);
}
+static bool mt7996_monitor_active(struct mt7996_dev *dev)
+{
+ struct mt7996_phy *phy;
+
+ mt7996_for_each_phy(dev, phy)
+ if (!(phy->rxfilter & MT_WF_RFCR_DROP_OTHER_UC))
+ return true;
+
+ return false;
+}
+
static void mt7996_set_monitor(struct mt7996_phy *phy, bool enabled)
{
struct mt7996_dev *dev;
@@ -632,7 +643,7 @@ static void mt7996_set_monitor(struct mt7996_phy *phy, bool enabled)
mt76_rmw_field(dev, MT_DMA_DCR0(phy->mt76->band_idx),
MT_DMA_DCR0_RXD_G5_EN, enabled);
mt76_rmw_field(dev, MT_MDP_DCR0,
- MT_MDP_DCR0_RX_HDR_TRANS_EN, !enabled);
+ MT_MDP_DCR0_RX_HDR_TRANS_EN, !mt7996_monitor_active(dev));
mt7996_phy_set_rxfilter(phy);
mt7996_mcu_set_sniffer_mode(phy, enabled);
}
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 22/25] wifi: mt76: mt7996: apply monitor mode again after a full reset
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (19 preceding siblings ...)
2026-10-28 12:23 ` [PATCH mt76-next 21/25] wifi: mt76: mt7996: keep rx header translation off while a monitor runs Felix Fietkau
@ 2026-10-28 12:24 ` Felix Fietkau
2026-10-28 12:24 ` [PATCH mt76-next 23/25] wifi: mt76: check the RX PN of an MLD peer once per peer Felix Fietkau
` (2 subsequent siblings)
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:24 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_full_reset() loads the firmware again in mt7996_mac_restart()
and then lets ieee80211_restart_hw() add every interface back. The reset
also clears the RXD group 5 bit in MT_DMA_DCR0 and sets the header
translation bit in MT_MDP_DCR0. phy->rxfilter still has
MT_WF_RFCR_DROP_OTHER_UC cleared on the radios of a monitor interface,
so mt7996_set_monitor() returns early on the re-add. The new firmware
never gets the sniffer mode command, and the monitor runs on with
header translation on and without RXD group 5.
Mark every radio as out of monitor mode before the interfaces are added
again, so that the re-add programs the monitor state from scratch.
Fixes: 69d54ce7491d ("wifi: mt76: mt7996: switch to single multi-radio wiphy")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/mac.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index 2e1b67ac2d11..a92536180dbe 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -2491,8 +2491,10 @@ mt7996_mac_full_reset(struct mt7996_dev *dev)
if (i == 10)
dev_err(dev->mt76.dev, "chip full reset failed\n");
- mt7996_for_each_phy(dev, phy)
+ mt7996_for_each_phy(dev, phy) {
phy->omac_mask = 0;
+ phy->rxfilter |= MT_WF_RFCR_DROP_OTHER_UC;
+ }
dev->mld_idx_mask = 0;
dev->mld_remap_idx_mask = 0;
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 23/25] wifi: mt76: check the RX PN of an MLD peer once per peer
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (20 preceding siblings ...)
2026-10-28 12:24 ` [PATCH mt76-next 22/25] wifi: mt76: mt7996: apply monitor mode again after a full reset Felix Fietkau
@ 2026-10-28 12:24 ` Felix Fietkau
2026-10-28 12:24 ` [PATCH mt76-next 24/25] wifi: mt76: mt7996: send no MLD_SETUP when station links only go away Felix Fietkau
2026-10-28 12:24 ` [PATCH mt76-next 25/25] wifi: mt76: mt7996: keep a unicast management frame on its MLO link Felix Fietkau
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:24 UTC (permalink / raw)
To: linux-wireless
For a frame that the hardware decrypted, mt76_check_ccmp_pn() compares
the PN with the last PN of status->wcid. For an MLD peer, mt7996 reports
each frame on the wcid of the link it arrived on, so every link keeps
its own replay counter, although all links of the MLD share the PN space
of the key. With the IV stripped, the function also sets
RX_FLAG_PN_VALIDATED, so mac80211 skips its own check per key. A frame
received on one link and replayed on another one thus passes the replay
check whenever the second link has seen lower PNs.
Keep the replay counters of a peer on its default wcid (def_wcid), like
the reorder state in mt76_rx_aggr_reorder(). The key check still uses
the wcid of the link. This also applies to mt7925, which sets def_wcid
for the links of an MLD station.
A key install resets these counters to the RX sequence of the key in
mac80211, which frames marked RX_FLAG_PN_VALIDATED never advance. In
mt7996 and mt7925, the key of a station is set up link by link, and in
mt7996 a link that is added later gets the installed keys again. Each
of these setups would reset the counters of the whole peer, so that a
frame of the new key received on one link could be replayed after the
setup of the next one. Seed the counters on the default wcid once per
key install, before the key reaches any link of the station, and only
enable the check on each link.
Fixes: dd82a9e02c05 ("wifi: mt76: mt7996: Rely on mt7996_sta_link in sta_add/sta_remove callbacks")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mac80211.c | 18 ++++++++----------
drivers/net/wireless/mediatek/mt76/mt76.h | 7 +++++++
.../net/wireless/mediatek/mt76/mt7925/main.c | 13 +++++++++++--
.../net/wireless/mediatek/mt76/mt7996/main.c | 11 ++++++++++-
4 files changed, 36 insertions(+), 13 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mac80211.c b/drivers/net/wireless/mediatek/mt76/mac80211.c
index 76908bd835f2..08ed3fc6d386 100644
--- a/drivers/net/wireless/mediatek/mt76/mac80211.c
+++ b/drivers/net/wireless/mediatek/mt76/mac80211.c
@@ -1209,16 +1209,10 @@ void mt76_wcid_key_setup(struct mt76_dev *dev, struct mt76_wcid *wcid,
struct ieee80211_key_seq seq;
int i;
- wcid->rx_check_pn = false;
-
- if (!key)
+ mt76_wcid_key_link_setup(wcid, key);
+ if (!wcid->rx_check_pn)
return;
- if (key->cipher != WLAN_CIPHER_SUITE_CCMP)
- return;
-
- wcid->rx_check_pn = true;
-
/* data frame */
for (i = 0; i < IEEE80211_NUM_TIDS; i++) {
ieee80211_get_key_rx_seq(key, i, &seq);
@@ -1327,6 +1321,7 @@ mt76_check_ccmp_pn(struct sk_buff *skb)
struct mt76_rx_status *status = (struct mt76_rx_status *)skb->cb;
struct mt76_wcid *wcid = status->wcid;
struct ieee80211_hdr *hdr;
+ struct mt76_wcid *pn_wcid;
int security_idx;
int ret;
@@ -1339,6 +1334,9 @@ mt76_check_ccmp_pn(struct sk_buff *skb)
if (!wcid || !wcid->rx_check_pn)
return;
+ /* the links of an MLD peer share one PN space per key */
+ pn_wcid = mt76_wcid_primary(wcid);
+
security_idx = status->qos_ctl & IEEE80211_QOS_CTL_TID_MASK;
if (status->flag & RX_FLAG_8023)
goto skip_hdr_check;
@@ -1366,14 +1364,14 @@ mt76_check_ccmp_pn(struct sk_buff *skb)
skip_hdr_check:
BUILD_BUG_ON(sizeof(status->iv) != sizeof(wcid->rx_key_pn[0]));
- ret = memcmp(status->iv, wcid->rx_key_pn[security_idx],
+ ret = memcmp(status->iv, pn_wcid->rx_key_pn[security_idx],
sizeof(status->iv));
if (ret <= 0) {
status->flag |= RX_FLAG_ONLY_MONITOR;
return;
}
- memcpy(wcid->rx_key_pn[security_idx], status->iv, sizeof(status->iv));
+ memcpy(pn_wcid->rx_key_pn[security_idx], status->iv, sizeof(status->iv));
if (status->flag & RX_FLAG_IV_STRIPPED)
status->flag |= RX_FLAG_PN_VALIDATED;
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index fce7bf2f0333..477d33a0022d 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -1574,6 +1574,13 @@ int mt76_rx_aggr_start(struct mt76_dev *dev, struct mt76_wcid *wcid, u8 tid,
u16 ssn, u16 size);
void mt76_rx_aggr_stop(struct mt76_dev *dev, struct mt76_wcid *wcid, u8 tid);
+static inline void
+mt76_wcid_key_link_setup(struct mt76_wcid *wcid,
+ struct ieee80211_key_conf *key)
+{
+ wcid->rx_check_pn = key && key->cipher == WLAN_CIPHER_SUITE_CCMP;
+}
+
void mt76_wcid_key_setup(struct mt76_dev *dev, struct mt76_wcid *wcid,
struct ieee80211_key_conf *key);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/main.c b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
index 22cc5b49da0a..ffbc6b954632 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
@@ -707,8 +707,11 @@ static int mt7925_set_link_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
else
goto out;
- mt76_wcid_key_setup(&dev->mt76, wcid,
- cmd == SET_KEY ? key : NULL);
+ if (sta)
+ mt76_wcid_key_link_setup(wcid, cmd == SET_KEY ? key : NULL);
+ else
+ mt76_wcid_key_setup(&dev->mt76, wcid,
+ cmd == SET_KEY ? key : NULL);
err = mt7925_mcu_add_key(&dev->mt76, vif, &mlink->bip,
key, MCU_UNI_CMD(STA_REC_UPDATE),
@@ -749,6 +752,12 @@ static int mt7925_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
mt792x_mutex_acquire(dev);
+ /* the links of a station share one RX PN state, seeded before the
+ * key reaches any of them
+ */
+ if (sta && cmd == SET_KEY)
+ mt76_wcid_key_setup(&dev->mt76, &msta->deflink.wcid, key);
+
if (ieee80211_vif_is_mld(vif)) {
unsigned int link_id;
unsigned long add;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index ec7c5e8f3fda..82143ade1cc0 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -268,7 +268,7 @@ mt7996_set_hw_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
if (cmd != SET_KEY && !is_bigtk)
return 0;
- mt76_wcid_key_setup(&dev->mt76, &msta_link->wcid, key);
+ mt76_wcid_key_link_setup(&msta_link->wcid, key);
err = mt7996_mcu_add_key(&dev->mt76, link, key,
MCU_WMWA_UNI_CMD(STA_REC_UPDATE),
@@ -837,6 +837,15 @@ static int mt7996_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
else
links = BIT(0);
+ /* the links of a station share one RX PN state, seeded before the
+ * key reaches any of them
+ */
+ if (sta && cmd == SET_KEY) {
+ struct mt7996_sta *msta = (struct mt7996_sta *)sta->drv_priv;
+
+ mt76_wcid_key_setup(&dev->mt76, &msta->deflink.wcid, key);
+ }
+
for_each_set_bit(link_id, &links, IEEE80211_MLD_MAX_NUM_LINKS) {
err = mt7996_set_hw_key(hw, cmd, vif, sta, link_id, key);
if (err)
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 24/25] wifi: mt76: mt7996: send no MLD_SETUP when station links only go away
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (21 preceding siblings ...)
2026-10-28 12:24 ` [PATCH mt76-next 23/25] wifi: mt76: check the RX PN of an MLD peer once per peer Felix Fietkau
@ 2026-10-28 12:24 ` Felix Fietkau
2026-10-28 12:24 ` [PATCH mt76-next 25/25] wifi: mt76: mt7996: keep a unicast management frame on its MLO link Felix Fietkau
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:24 UTC (permalink / raw)
To: linux-wireless
Since links added after association are brought up in
mt7996_mac_sta_links_sync(), the remaining links of an MLO station also
get an MLD_SETUP when links are only removed, with a link list that no
longer holds the removed links. After that MLD_SETUP, the remaining
links pass no traffic in either direction, although TX completes
without failures. Traffic only resumes once a link is added again.
Send the MLD_SETUP to the other links only when a link joins. The
MediaTek driver also sends no firmware command when a link station
goes, and tears down all links with STA_REC_MLD_OFF at disassociation.
This also applies to the restore after a failed link add.
Fixes: 1aef01d3cbbe ("wifi: mt76: mt7996: bring up station links added after association")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/main.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/main.c b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
index 82143ade1cc0..5df5354beb50 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/main.c
@@ -1864,7 +1864,8 @@ mt7996_mac_sta_links_sync(struct mt7996_dev *dev, struct ieee80211_vif *vif,
mt7996_mac_sta_link_ps_leave(dev, link, msta_link);
}
- updated = sta->mlo ? links & ~updated : 0;
+ /* a link list without a removed link stalls the remaining links */
+ updated = sta->mlo && joined ? links & ~updated : 0;
for_each_set_bit(link_id, &updated, IEEE80211_MLD_MAX_NUM_LINKS) {
struct mt7996_sta_link *msta_link;
struct mt7996_vif_link *link;
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread* [PATCH mt76-next 25/25] wifi: mt76: mt7996: keep a unicast management frame on its MLO link
2026-10-28 12:23 [PATCH mt76-next 01/25] wifi: mt76: mt7996: take the link of an 802.3 frame from its station Felix Fietkau
` (22 preceding siblings ...)
2026-10-28 12:24 ` [PATCH mt76-next 24/25] wifi: mt76: mt7996: send no MLD_SETUP when station links only go away Felix Fietkau
@ 2026-10-28 12:24 ` Felix Fietkau
23 siblings, 0 replies; 25+ messages in thread
From: Felix Fietkau @ 2026-10-28 12:24 UTC (permalink / raw)
To: linux-wireless
mt7996_mac_write_txwi_80211() sets MT_TXD5_FL for an MLD interface only
on multicast, EAPOL and 4-address null frames. A unicast management
frame to an MLD peer goes out on a link that the hardware picks among
the links of the peer, not on the link of its TXD. ML reconfiguration
depends on that link: mac80211 removes a link before it sends the Link
Reconfiguration Request, so that the request leaves on a remaining
link, and the AP MLD sends the Response on the link where the request
was received. With hardware link selection, the request leaves on the
removed link, or the AP sends the Response there. The station then
drops the Response, and the link removal ends with a disconnection.
Set MT_TXD5_FL for unicast management frames to a station of an MLD
interface, so that they keep the link that mac80211 or the driver
selected.
Fixes: f0b0b239b8f3 ("wifi: mt76: mt7996: rework mt7996_mac_write_txwi() for MLO support")
Signed-off-by: Felix Fietkau <nbd@nbd.name>
---
drivers/net/wireless/mediatek/mt76/mt7996/mac.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index a92536180dbe..03670293a78f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -851,7 +851,8 @@ mt7996_mac_write_txwi_80211(struct mt7996_dev *dev, __le32 *txwi,
}
if (vif && ieee80211_vif_is_mld(vif) &&
- (multicast || unlikely(skb->protocol == cpu_to_be16(ETH_P_PAE))))
+ (multicast || unlikely(skb->protocol == cpu_to_be16(ETH_P_PAE)) ||
+ (ieee80211_is_mgmt(fc) && wcid->sta)))
txwi[5] |= cpu_to_le32(MT_TXD5_FL);
if (ieee80211_is_nullfunc(fc) && ieee80211_has_a4(fc) &&
--
2.47.3
^ permalink raw reply related [flat|nested] 25+ messages in thread