[PATCH v2] AP MLD: Keep replacement driver STA during stale cleanup
Nicholas Sielicki
opensource at nslick.com
Fri Oct 30 19:05:12 PDT 2026
An AP MLD uses one driver station per address, even when hostapd retains
separate station entries on its affiliated links. If a station associates
on another link while cleanup of the previous association is pending,
the old entry can delete the new driver station by address two seconds
after the new association completes its four-way handshake. Its later
cleanup can also clear the new station's authorized flag.
This affects both non-AP MLD stations moving their association link and
non-MLD stations moving between affiliated links of an AP MLD. Before
removing a driver station or updating its flags, check whether an
associated entry on another link has replaced the old entry. Do not
treat an entry that is still associated, or one that added an
unassociated driver station for authentication, as replaced. Preserve
the link-aware removal path for partner link stations.
On a three-link AP MLD, a non-AP MLD client moving between the AP MLD
and a standalone BSS showed this sequence (STA MAC redacted):
00:49:15 mld0: Prune association for <STA-MAC>
00:49:15 mld0: AP-STA-CONNECTED <STA-MAC>
00:49:15 mld0: EAPOL-4WAY-HS-COMPLETED <STA-MAC>
00:49:17 mld0: Disassociation callback for STA <STA-MAC>
00:49:17 nl80211: sta_remove -> DEL_STATION mld0 <STA-MAC> --> 0 (Success)
Goran reproduced the non-MLD between-link case with mac80211_hwsim:
without the additional checks the driver station disappeared two
seconds after each move; with them it stayed present and authorized
through eight moves, while a normal disconnect still removed it.
Co-developed-by: Goran Sustek <gsustek at gmail.com>
Signed-off-by: Goran Sustek <gsustek at gmail.com>
Signed-off-by: Nicholas Sielicki <opensource at nslick.com>
---
Changes since v1:
- Also protect non-MLD stations moving between affiliated AP links.
- Do not clear authorization on a replacement when the stale entry is freed.
- Exclude still-associated and added_unassoc entries from the replacement check.
Goran's follow-up and hwsim results:
https://lists.infradead.org/pipermail/hostap/2026-October/045857.html
src/ap/ap_drv_ops.c | 12 +++++++++
src/ap/sta_info.c | 65 +++++++++++++++++++++++++++++++++++++++++----
src/ap/sta_info.h | 2 ++
3 files changed, 74 insertions(+), 5 deletions(-)
diff --git a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index dd2f5c542..c184cb6e3 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -323,6 +323,12 @@ int hostapd_set_authorized(struct hostapd_data *hapd,
return 0;
}
+#ifdef CONFIG_IEEE80211BE
+ /* The driver STA belongs to a newer association on another link */
+ if (ap_sta_replaced_on_partner_link(hapd, sta))
+ return 0;
+#endif /* CONFIG_IEEE80211BE */
+
if (authorized) {
return hostapd_sta_set_flags(hapd, sta->addr,
hostapd_sta_flags_to_drv(
@@ -358,6 +364,12 @@ int hostapd_set_sta_flags(struct hostapd_data *hapd, struct sta_info *sta)
return 0;
}
+#ifdef CONFIG_IEEE80211BE
+ /* The driver STA belongs to a newer association on another link */
+ if (ap_sta_replaced_on_partner_link(hapd, sta))
+ return 0;
+#endif /* CONFIG_IEEE80211BE */
+
flags_or = total_flags & set_flags;
flags_and = total_flags | ~set_flags;
return hostapd_sta_set_flags(hapd, sta->addr, total_flags,
diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
index 1e8ef1b4f..0df68a706 100644
--- a/src/ap/sta_info.c
+++ b/src/ap/sta_info.c
@@ -207,16 +207,71 @@ void ap_free_sta_pasn(struct hostapd_data *hapd, struct sta_info *sta)
#endif /* CONFIG_PASN */
-static void __ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
+#ifdef CONFIG_IEEE80211BE
+/**
+ * ap_sta_replaced_on_partner_link - Check for a newer association on a link
+ * @hapd: BSS of the station entry
+ * @sta: Old station entry
+ * Returns: true if another link of the AP MLD has an associated entry that
+ * replaces @sta
+ *
+ * A new association on another link can use the same address while the old
+ * station's disassociation callback or inactivity timer is still pending: the
+ * MLD address of a non-AP MLD, or the address of a non-MLD STA that moved from
+ * one affiliated link to another. Both associations use the same MLD netdev,
+ * so a driver operation by address for the old entry would apply to the new
+ * one. A non-MLD STA has its entry only on the link it associated to, so any
+ * associated entry on another link is a replacement. Only an entry that is no
+ * longer associated can have been replaced; this keeps the check one-sided
+ * while both entries are still marked associated before the old one is
+ * pruned. An entry that added the driver STA for an ongoing authentication
+ * (added_unassoc) owns that driver STA.
+ */
+bool ap_sta_replaced_on_partner_link(struct hostapd_data *hapd,
+ struct sta_info *sta)
+{
+ struct hostapd_data *lhapd;
+
+ if (!hapd->mld || (sta->flags & WLAN_STA_ASSOC) || sta->added_unassoc)
+ return false;
+
+ for_each_mld_link(lhapd, hapd) {
+ struct sta_info *lsta;
+
+ if (lhapd == hapd)
+ continue;
+ lsta = ap_get_sta(lhapd, sta->addr);
+ if (lsta &&
+ (!ap_sta_is_mld(hapd, sta) ||
+ (lsta->mld_assoc_link_id != sta->mld_assoc_link_id &&
+ lsta->mld_assoc_link_id == lhapd->mld_link_id)) &&
+ (lsta->flags & WLAN_STA_ASSOC)) {
+ wpa_printf(MSG_DEBUG,
+ "%s: Keep replacement STA " MACSTR
+ " on link %u",
+ hapd->conf->iface, MAC2STR(sta->addr),
+ lhapd->mld_link_id);
+ return true;
+ }
+ }
+
+ return false;
+}
+#endif /* CONFIG_IEEE80211BE */
+
+
+static int __ap_free_sta(struct hostapd_data *hapd, struct sta_info *sta)
{
#ifdef CONFIG_IEEE80211BE
+ if (ap_sta_replaced_on_partner_link(hapd, sta))
+ return 0;
+
if (hostapd_sta_is_link_sta(hapd, sta)) {
- hostapd_drv_link_sta_remove(hapd, sta->addr);
- return;
+ return hostapd_drv_link_sta_remove(hapd, sta->addr);
}
#endif /* CONFIG_IEEE80211BE */
- hostapd_drv_sta_remove(hapd, sta->addr);
+ return hostapd_drv_sta_remove(hapd, sta->addr);
}
@@ -1011,7 +1066,7 @@ static int ap_sta_remove(struct hostapd_data *hapd, struct sta_info *sta)
wpa_printf(MSG_DEBUG, "%s: Removing STA " MACSTR " from kernel driver",
hapd->conf->iface, MAC2STR(sta->addr));
- if (hostapd_drv_sta_remove(hapd, sta->addr) &&
+ if (__ap_free_sta(hapd, sta) &&
sta->flags & WLAN_STA_ASSOC) {
wpa_printf(MSG_DEBUG, "%s: Could not remove station " MACSTR
" from kernel driver",
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index c05c8a77e..fd81740a1 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -462,6 +462,8 @@ void ap_sta_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
int ap_sta_pending_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
struct sta_info *sta);
int ap_sta_re_add(struct hostapd_data *hapd, struct sta_info *sta);
+bool ap_sta_replaced_on_partner_link(struct hostapd_data *hapd,
+ struct sta_info *sta);
void ap_free_sta_pasn(struct hostapd_data *hapd, struct sta_info *sta);
--
2.56.0
More information about the Hostap
mailing list