[PATCH] wifi: mt76: mt7925: stabilize STA_REC_MLD link selection

From: Andrei Rusu de Castro

Date: Wed Sep 02 2026 - 08:36:49 EST


mt7925_mcu_sta_mld_tlv() encodes the primary link and then, as the
secondary entry, whichever link the current station update happens to
be for.

STA_REC_MLD is a complete record rather than an incremental update:
every call rewrites primary_id, secondary_id, link_num and link[]. Since
mt7925_mcu_sta_update() is issued per link, the record left in firmware
depends on which link was updated last. For a two-link station an update
for the secondary link leaves both links, while a later update for the
primary leaves only the primary.

Fill the remaining firmware entries from the station's valid links.
Keep the primary in entry zero, walk secondary links in link ID order,
skip links without both station and BSS state, stop at the firmware
array size, and set link_num from the number of entries written.

A link added through .change_sta_links needs one ordering exception.
mt7925_mac_link_sta_add() sends the MCU command before publishing the
new link in msta->link[] and msta->valid_links. Fold that in-flight
caller link into the local valid-link snapshot and use its passed
station object when the published slot is still empty. The VIF link
state is already published and remains the BSS authority.

Commit ff643b81bc38 ("wifi: mt76: mt7925: pass mlink and mconf to
sta_mld_tlv()") removed the complete link lookups because the caller
already knew one link. Reintroduce them because that caller context
cannot otherwise describe the complete two-entry firmware record.

A source and call-graph audit identified the
trigger-dependent record and the link-add publication window. A
source-level harness issues the encoder across seven settled layouts
plus the in-flight add ordering, reproduces the old trigger dependence,
and passes 90 checks. The exact changed object builds with W=1.

Fixes: ff643b81bc38 ("wifi: mt76: mt7925: pass mlink and mconf to sta_mld_tlv()")

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
.../net/wireless/mediatek/mt76/mt7925/mcu.c | 49 +++++++++++++++++--
1 file changed, 44 insertions(+), 5 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
index fa29c486a455..d3e1ae4143cb 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mcu.c
@@ -2068,9 +2068,12 @@ mt7925_mcu_sta_mld_tlv(struct sk_buff *skb,
struct mt792x_vif *mvif = (struct mt792x_vif *)vif->drv_priv;
struct mt792x_sta *msta = (struct mt792x_sta *)sta->drv_priv;
struct mt792x_dev *dev = mvif->phy->dev;
+ unsigned long valid = msta->valid_links;
struct mt792x_bss_conf *mconf_pri;
struct sta_rec_mld *mld;
+ unsigned int link_id;
struct tlv *tlv;
+ u8 max_links;
u8 cnt = 0;

/* Primary link always uses driver's deflink WCID. */
@@ -2099,11 +2102,47 @@ mt7925_mcu_sta_mld_tlv(struct sk_buff *skb,
mld->link[cnt].wlan_id = cpu_to_le16(msta->deflink.wcid.idx);
mld->link[cnt++].bss_idx = mconf_pri->mt76.idx;

- /* Optionally encode the currently-updated secondary link. */
- if (mlink && mlink != &msta->deflink && mconf) {
- mld->secondary_id = cpu_to_le16(mlink->wcid.idx);
- mld->link[cnt].wlan_id = cpu_to_le16(mlink->wcid.idx);
- mld->link[cnt++].bss_idx = mconf->mt76.idx;
+ /* Encode the remaining valid links of this station, not just the one
+ * this command happens to be updating. STA_REC_MLD is a complete
+ * record: every update rewrites link_num and link[], so selecting the
+ * secondary entry from the caller's link made the record firmware
+ * ends up holding depend on which link was updated last.
+ */
+ max_links = ARRAY_SIZE(mld->link);
+
+ /* mt7925_mac_link_sta_add() issues this command before publishing
+ * a new link in msta->link[] and msta->valid_links. Include that
+ * in-flight caller link in this record so it is not lost.
+ */
+ if (mlink && mconf && mlink != &msta->deflink)
+ valid |= BIT(mlink->wcid.link_id);
+
+ for_each_set_bit(link_id, &valid, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct mt792x_link_sta *mlink_sec;
+ struct mt792x_bss_conf *mconf_sec;
+
+ if (cnt == max_links)
+ break;
+
+ if (link_id == msta->deflink_id)
+ continue;
+
+ mlink_sec = mt792x_sta_to_link(msta, link_id);
+ if (!mlink_sec && mlink && link_id == mlink->wcid.link_id)
+ mlink_sec = mlink;
+ if (!mlink_sec || mlink_sec == &msta->deflink)
+ continue;
+
+ mconf_sec = rcu_dereference_protected(mvif->link_conf[link_id],
+ lockdep_is_held(&dev->mt76.mutex));
+ if (!mconf_sec)
+ continue;
+
+ if (cnt == 1)
+ mld->secondary_id = cpu_to_le16(mlink_sec->wcid.idx);
+
+ mld->link[cnt].wlan_id = cpu_to_le16(mlink_sec->wcid.idx);
+ mld->link[cnt++].bss_idx = mconf_sec->mt76.idx;
}

mld->link_num = cnt;