summaryrefslogtreecommitdiffstats
path: root/net/mac80211/link.c
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-08-23 13:28:57 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-08-23 13:28:57 -0700
commit4352b8aee98005853aa63f57d6377282de17a33f (patch)
tree27614d7a9ef05c32d089004e104364ce42324837 /net/mac80211/link.c
downloadlinux-stable-4352b8aee98005853aa63f57d6377282de17a33f.tar.gz
linux-stable-4352b8aee98005853aa63f57d6377282de17a33f.zip
Merge tag 'i3c/for-7.3' of git://git.kernel.org/pub/scm/linux/kernel/git/i3c/linuxgrafted
Pull I3C updates from Alexandre Belloni: "A new driver this cycle, for the AMD AXI I3C controller. There are also new features such as IBI wakeup support, SETAASA device discovery and ACPI support for the the DAA methods, meaning we can now communicate with the SPD devices on DDR5 modules. New driver: - AMD AXI I3C master controller Subsystem: - I3C Common Command Code (CCC) handling improvements, especially around GET CCCs - SETAASA device discovery support - ACPI support for all existing DAA methods like SETDASA, SETNEWDA as well as I2C devices on I3C bus - IBI-based wakeup support Drivers: - dw: SETAASA support - mipi-i3c-hci: advertise IBI wakeup capability, AMD_PT I3C controller support, PIO queue management support for HCI v1.2 - renesas: improve suspend to RAM support, add runtime PM support" * tag 'i3c/for-7.3' of git://git.kernel.org/pub/scm/linux/kernel/git/i3c/linux: (70 commits) i3c: dw: reduce do_daa time if there's no client i3c: mipi-i3c-hci: Add support for AMD_PT I3C controller i3c: mipi-i3c-hci: Add PIO queue management support for HCI v1.2 i3c: mipi-i3c-hci: Fix missing STAT_IBI_STATUS_THLD in PIO mode i3c: dw: rename "pclk" to "apb" to match dt-binding i3c: dw: make struct dw_i3c_cmd smaller i3c: dw: use COMMAND_PORT_TRANSFER_ARG instead of hardcoding i3c: renesas: Don't register devices when ENTDAA times out i3c: master: dw-i3c-master: fix OD timing for first broadcast i3c: mipi-i3c-hci: Advertise IBI wakeup capability i3c: mipi-i3c-hci: Factor out i3c_hci_sysdev() i3c: mipi-i3c-hci-pci: Propagate I3C wakeup requirements to PCI i3c: master: Reject IBI requests from non-IBI-capable devices i3c: master: Add helper to query bus wakeup requirements i3c: master: Report wakeup events for IBIs i3c: master: Support IBI-based wakeup capability i3c: master: Fix potential UAF in i3c_device_match() i3c: master: Fix potential UAF in i3c_device_uevent() i3c: Make dev->desc locking assumptions explicit i3c: master: Fix use-after-free of master->this ...
Diffstat (limited to 'net/mac80211/link.c')
-rw-r--r--net/mac80211/link.c698
1 files changed, 698 insertions, 0 deletions
diff --git a/net/mac80211/link.c b/net/mac80211/link.c
new file mode 100644
index 000000000..931950a10
--- /dev/null
+++ b/net/mac80211/link.c
@@ -0,0 +1,698 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * MLO link handling
+ *
+ * Copyright (C) 2022-2026 Intel Corporation
+ */
+#include <linux/slab.h>
+#include <linux/kernel.h>
+#include <net/mac80211.h>
+#include "ieee80211_i.h"
+#include "driver-ops.h"
+#include "key.h"
+#include "debugfs_netdev.h"
+
+static void ieee80211_update_apvlan_links(struct ieee80211_sub_if_data *sdata)
+{
+ unsigned long rem = ~sdata->vif.valid_links &
+ GENMASK(IEEE80211_MLD_MAX_NUM_LINKS - 1, 0);
+ struct ieee80211_local *local = sdata->local;
+ unsigned long add = sdata->vif.valid_links;
+ struct wiphy *wiphy = local->hw.wiphy;
+ struct ieee80211_sub_if_data *vlan;
+ struct ieee80211_link_data *link;
+ struct sta_info *sta;
+
+ list_for_each_entry(vlan, &sdata->u.ap.vlans, u.vlan.list) {
+ int link_id;
+
+ if (vlan->wdev.use_4addr) {
+ sta = wiphy_dereference(wiphy,
+ vlan->u.vlan.sta);
+ if (sta)
+ add = add & sta->sta.valid_links;
+ }
+
+ if (add == vlan->vif.valid_links)
+ continue;
+
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ vlan->wdev.valid_links |= BIT(link_id);
+ ether_addr_copy(vlan->wdev.links[link_id].addr,
+ sdata->wdev.links[link_id].addr);
+ }
+
+ for_each_set_bit(link_id, &rem, IEEE80211_MLD_MAX_NUM_LINKS) {
+ vlan->wdev.valid_links &= ~BIT(link_id);
+ eth_zero_addr(vlan->wdev.links[link_id].addr);
+ }
+
+ ieee80211_vif_set_links(vlan, add, 0);
+
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ link = sdata_dereference(vlan->link[link_id], vlan);
+ ieee80211_link_vlan_copy_chanctx(link);
+ }
+ }
+}
+
+void ieee80211_apvlan_link_setup(struct ieee80211_sub_if_data *sdata)
+{
+ struct ieee80211_sub_if_data *ap_bss = container_of(sdata->bss,
+ struct ieee80211_sub_if_data, u.ap);
+ u16 new_links = ap_bss->vif.valid_links;
+ unsigned long add;
+ int link_id;
+
+ if (!ap_bss->vif.valid_links)
+ return;
+
+ add = new_links;
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ sdata->wdev.valid_links |= BIT(link_id);
+ ether_addr_copy(sdata->wdev.links[link_id].addr,
+ ap_bss->wdev.links[link_id].addr);
+ }
+
+ ieee80211_vif_set_links(sdata, new_links, 0);
+}
+
+void ieee80211_apvlan_link_clear(struct ieee80211_sub_if_data *sdata)
+{
+ if (!sdata->wdev.valid_links)
+ return;
+
+ sdata->wdev.valid_links = 0;
+ ieee80211_vif_clear_links(sdata);
+}
+
+void ieee80211_link_setup(struct ieee80211_link_data *link)
+{
+ if (link->sdata->vif.type == NL80211_IFTYPE_STATION)
+ ieee80211_mgd_setup_link(link);
+}
+
+void ieee80211_link_init(struct ieee80211_sub_if_data *sdata,
+ int link_id,
+ struct ieee80211_link_data *link,
+ struct ieee80211_bss_conf *link_conf)
+{
+ bool deflink = link_id < 0;
+
+ if (link_id < 0)
+ link_id = 0;
+
+ if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN) {
+ struct ieee80211_sub_if_data *ap_bss;
+ struct ieee80211_bss_conf *ap_bss_conf;
+
+ ap_bss = container_of(sdata->bss,
+ struct ieee80211_sub_if_data, u.ap);
+
+ if (deflink)
+ ap_bss_conf = &ap_bss->vif.bss_conf;
+ else
+ ap_bss_conf = sdata_dereference(ap_bss->vif.link_conf[link_id],
+ ap_bss);
+
+ memcpy(link_conf, ap_bss_conf, sizeof(*link_conf));
+ }
+
+ link->sdata = sdata;
+ link->link_id = link_id;
+ link->conf = link_conf;
+ link_conf->link_id = link_id;
+ link_conf->vif = &sdata->vif;
+ link->ap_power_level = IEEE80211_UNSET_POWER_LEVEL;
+ link->user_power_level = sdata->local->user_power_level;
+ link_conf->txpower = INT_MIN;
+
+ wiphy_work_init(&link->csa.finalize_work,
+ ieee80211_csa_finalize_work);
+ wiphy_work_init(&link->color_change_finalize_work,
+ ieee80211_color_change_finalize_work);
+ wiphy_delayed_work_init(&link->color_collision_detect_work,
+ ieee80211_color_collision_detection_work);
+ wiphy_hrtimer_work_init(&link->dfs_cac_timer_work,
+ ieee80211_dfs_cac_timer_work);
+
+ if (!deflink) {
+ switch (sdata->vif.type) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_AP_VLAN:
+ ether_addr_copy(link_conf->addr,
+ sdata->wdev.links[link_id].addr);
+ link_conf->bssid = link_conf->addr;
+ WARN_ON(!(sdata->wdev.valid_links & BIT(link_id)));
+ break;
+ case NL80211_IFTYPE_STATION:
+ /* station sets the bssid in ieee80211_mgd_setup_link */
+ break;
+ default:
+ WARN_ON(1);
+ }
+
+ ieee80211_link_debugfs_add(link);
+ }
+
+ rcu_assign_pointer(sdata->vif.link_conf[link_id], link_conf);
+ rcu_assign_pointer(sdata->link[link_id], link);
+}
+
+void ieee80211_link_stop(struct ieee80211_link_data *link)
+{
+ if (link->sdata->vif.type == NL80211_IFTYPE_STATION)
+ ieee80211_mgd_stop_link(link);
+
+ wiphy_delayed_work_cancel(link->sdata->local->hw.wiphy,
+ &link->color_collision_detect_work);
+ wiphy_work_cancel(link->sdata->local->hw.wiphy,
+ &link->color_change_finalize_work);
+ wiphy_work_cancel(link->sdata->local->hw.wiphy,
+ &link->csa.finalize_work);
+
+ if (link->sdata->wdev.links[link->link_id].cac_started) {
+ wiphy_hrtimer_work_cancel(link->sdata->local->hw.wiphy,
+ &link->dfs_cac_timer_work);
+ cfg80211_cac_event(link->sdata->dev,
+ &link->conf->chanreq.oper,
+ NL80211_RADAR_CAC_ABORTED,
+ GFP_KERNEL, link->link_id);
+ }
+
+ ieee80211_link_release_channel(link);
+}
+
+struct link_container {
+ struct ieee80211_link_data data;
+ struct ieee80211_bss_conf conf;
+};
+
+static void ieee80211_tear_down_links(struct ieee80211_sub_if_data *sdata,
+ struct link_container **links, u16 mask)
+{
+ struct ieee80211_link_data *link;
+ LIST_HEAD(keys);
+ unsigned int link_id;
+
+ for (link_id = 0; link_id < IEEE80211_MLD_MAX_NUM_LINKS; link_id++) {
+ if (!(mask & BIT(link_id)))
+ continue;
+ link = &links[link_id]->data;
+ if (link_id == 0 && !link)
+ link = &sdata->deflink;
+ if (WARN_ON(!link))
+ continue;
+ ieee80211_remove_link_keys(link, &keys);
+ ieee80211_link_debugfs_remove(link);
+ ieee80211_link_stop(link);
+ }
+
+ synchronize_rcu();
+
+ ieee80211_free_key_list(sdata->local, &keys);
+}
+
+static void ieee80211_free_links(struct ieee80211_sub_if_data *sdata,
+ struct link_container **links)
+{
+ unsigned int link_id;
+
+ for (link_id = 0; link_id < IEEE80211_MLD_MAX_NUM_LINKS; link_id++)
+ kfree(links[link_id]);
+}
+
+static int ieee80211_check_dup_link_addrs(struct ieee80211_sub_if_data *sdata)
+{
+ unsigned int i, j;
+
+ for (i = 0; i < IEEE80211_MLD_MAX_NUM_LINKS; i++) {
+ struct ieee80211_link_data *link1;
+
+ link1 = sdata_dereference(sdata->link[i], sdata);
+ if (!link1)
+ continue;
+ for (j = i + 1; j < IEEE80211_MLD_MAX_NUM_LINKS; j++) {
+ struct ieee80211_link_data *link2;
+
+ link2 = sdata_dereference(sdata->link[j], sdata);
+ if (!link2)
+ continue;
+
+ if (ether_addr_equal(link1->conf->addr,
+ link2->conf->addr))
+ return -EALREADY;
+ }
+ }
+
+ return 0;
+}
+
+static void ieee80211_set_vif_links_bitmaps(struct ieee80211_sub_if_data *sdata,
+ u16 valid_links, u16 dormant_links)
+{
+ sdata->vif.valid_links = valid_links;
+ sdata->vif.dormant_links = dormant_links;
+
+ if (!valid_links ||
+ WARN((~valid_links & dormant_links) ||
+ !(valid_links & ~dormant_links),
+ "Invalid links: valid=0x%x, dormant=0x%x",
+ valid_links, dormant_links)) {
+ sdata->vif.active_links = 0;
+ sdata->vif.dormant_links = 0;
+ return;
+ }
+
+ switch (sdata->vif.type) {
+ case NL80211_IFTYPE_AP:
+ case NL80211_IFTYPE_AP_VLAN:
+ /* in an AP all links are always active */
+ sdata->vif.active_links = valid_links;
+
+ /* AP links are not expected to be disabled */
+ WARN_ON(dormant_links);
+ break;
+ case NL80211_IFTYPE_STATION:
+ if (sdata->vif.active_links)
+ break;
+ sdata->vif.active_links = valid_links & ~dormant_links;
+ WARN_ON(hweight16(sdata->vif.active_links) > 1);
+ break;
+ default:
+ WARN_ON(1);
+ }
+}
+
+static int ieee80211_vif_update_links(struct ieee80211_sub_if_data *sdata,
+ struct link_container **to_free,
+ u16 new_links, u16 dormant_links)
+{
+ u16 old_links = sdata->vif.valid_links;
+ u16 old_active = sdata->vif.active_links;
+ unsigned long add = new_links & ~old_links;
+ unsigned long rem = old_links & ~new_links;
+ unsigned long sta_rem = rem;
+ unsigned int link_id;
+ int ret;
+ struct link_container *links[IEEE80211_MLD_MAX_NUM_LINKS] = {}, *link;
+ struct ieee80211_bss_conf *old[IEEE80211_MLD_MAX_NUM_LINKS];
+ struct ieee80211_link_data *old_data[IEEE80211_MLD_MAX_NUM_LINKS];
+ bool use_deflink = old_links == 0; /* set for error case */
+ bool non_sta = sdata->vif.type != NL80211_IFTYPE_STATION;
+ struct sta_info *sta;
+
+ lockdep_assert_wiphy(sdata->local->hw.wiphy);
+
+ memset(to_free, 0, sizeof(links));
+
+ if (old_links == new_links && dormant_links == sdata->vif.dormant_links)
+ return 0;
+
+ if (!old_links || !new_links)
+ WARN_ON(sta_info_flush(sdata, -1) > 0);
+
+ /* if there were no old links, need to clear the pointers to deflink */
+ if (!old_links)
+ rem |= BIT(0);
+
+ /* allocate new link structures first */
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ link = kzalloc_obj(*link);
+ if (!link) {
+ ret = -ENOMEM;
+ goto free;
+ }
+ links[link_id] = link;
+ }
+
+ /* keep track of the old pointers for the driver */
+ BUILD_BUG_ON(sizeof(old) != sizeof(sdata->vif.link_conf));
+ memcpy(old, sdata->vif.link_conf, sizeof(old));
+ /* and for us in error cases */
+ BUILD_BUG_ON(sizeof(old_data) != sizeof(sdata->link));
+ memcpy(old_data, sdata->link, sizeof(old_data));
+
+ /* grab old links to free later */
+ for_each_set_bit(link_id, &rem, IEEE80211_MLD_MAX_NUM_LINKS) {
+ if (rcu_access_pointer(sdata->link[link_id]) != &sdata->deflink) {
+ /*
+ * we must have allocated the data through this path so
+ * we know we can free both at the same time
+ */
+ to_free[link_id] = container_of(rcu_access_pointer(sdata->link[link_id]),
+ typeof(*links[link_id]),
+ data);
+ }
+
+ RCU_INIT_POINTER(sdata->link[link_id], NULL);
+ RCU_INIT_POINTER(sdata->vif.link_conf[link_id], NULL);
+ }
+
+ if (!old_links)
+ ieee80211_debugfs_recreate_netdev(sdata, true);
+
+ /* link them into data structures */
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ WARN_ON(!use_deflink &&
+ rcu_access_pointer(sdata->link[link_id]) == &sdata->deflink);
+
+ link = links[link_id];
+ ieee80211_link_init(sdata, link_id, &link->data, &link->conf);
+ ieee80211_link_setup(&link->data);
+ if (sdata->vif.type != NL80211_IFTYPE_AP_VLAN)
+ ieee80211_set_wmm_default(&link->data, true, non_sta);
+ }
+
+ if (new_links == 0)
+ ieee80211_link_init(sdata, -1, &sdata->deflink,
+ &sdata->vif.bss_conf);
+
+ ret = ieee80211_check_dup_link_addrs(sdata);
+ if (!ret) {
+ /* for keys we will not be able to undo this */
+ ieee80211_tear_down_links(sdata, to_free, rem);
+
+ ieee80211_set_vif_links_bitmaps(sdata, new_links, dormant_links);
+
+ /* tell the driver */
+ if (sdata->vif.type != NL80211_IFTYPE_AP_VLAN)
+ ret = drv_change_vif_links(sdata->local, sdata,
+ old_links & old_active,
+ new_links & sdata->vif.active_links,
+ old);
+ if (!new_links)
+ ieee80211_debugfs_recreate_netdev(sdata, false);
+
+ if (sdata->vif.type == NL80211_IFTYPE_AP)
+ ieee80211_update_apvlan_links(sdata);
+ }
+
+ /*
+ * Ignore errors if we are only removing links as removal should
+ * always succeed
+ */
+ if (!new_links)
+ ret = 0;
+
+ if (ret) {
+ /* restore config */
+ memcpy(sdata->link, old_data, sizeof(old_data));
+ memcpy(sdata->vif.link_conf, old, sizeof(old));
+ ieee80211_set_vif_links_bitmaps(sdata, old_links, dormant_links);
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ ieee80211_link_debugfs_remove(&links[link_id]->data);
+ ieee80211_link_stop(&links[link_id]->data);
+ }
+ /* and free (only) the newly allocated links */
+ memset(to_free, 0, sizeof(links));
+ goto free;
+ }
+
+ /* try to remove links that are now invalid from (MLO) stations */
+ list_for_each_entry(sta, &sdata->local->sta_list, list) {
+ unsigned long rem_links = sta->sta.valid_links & sta_rem;
+
+ if (sta->sdata != sdata)
+ continue;
+
+ /*
+ * skip stations that would have no links left,
+ * those will be removed completely later
+ */
+ if (sta->sta.valid_links == rem_links)
+ continue;
+
+ for_each_set_bit(link_id, &rem_links,
+ IEEE80211_MLD_MAX_NUM_LINKS)
+ ieee80211_sta_remove_link(sta, link_id);
+ }
+
+ /*
+ * Remove stations using any removed links. Note that due
+ * to the above station link removal, this only removes
+ * stations that were skipped above because they'd have no
+ * links left after link removal.
+ */
+ for_each_set_bit(link_id, &sta_rem, IEEE80211_MLD_MAX_NUM_LINKS)
+ sta_info_flush(sdata, link_id);
+
+ /* use deflink/bss_conf again if and only if there are no more links */
+ use_deflink = new_links == 0;
+
+ goto deinit;
+free:
+ /* if we failed during allocation, only free all */
+ for (link_id = 0; link_id < IEEE80211_MLD_MAX_NUM_LINKS; link_id++) {
+ kfree(links[link_id]);
+ links[link_id] = NULL;
+ }
+deinit:
+ if (use_deflink)
+ ieee80211_link_init(sdata, -1, &sdata->deflink,
+ &sdata->vif.bss_conf);
+ return ret;
+}
+
+int ieee80211_vif_set_links(struct ieee80211_sub_if_data *sdata,
+ u16 new_links, u16 dormant_links)
+{
+ struct link_container *links[IEEE80211_MLD_MAX_NUM_LINKS];
+ int ret;
+
+ ret = ieee80211_vif_update_links(sdata, links, new_links,
+ dormant_links);
+ ieee80211_free_links(sdata, links);
+
+ return ret;
+}
+
+static int _ieee80211_set_active_links(struct ieee80211_sub_if_data *sdata,
+ u16 active_links)
+{
+ struct ieee80211_bss_conf *link_confs[IEEE80211_MLD_MAX_NUM_LINKS];
+ struct ieee80211_local *local = sdata->local;
+ u16 old_active = sdata->vif.active_links;
+ unsigned long rem = old_active & ~active_links;
+ unsigned long add = active_links & ~old_active;
+ struct sta_info *sta;
+ unsigned int link_id;
+ int ret, i;
+
+ if (!ieee80211_sdata_running(sdata))
+ return -ENETDOWN;
+
+ if (sdata->vif.type != NL80211_IFTYPE_STATION)
+ return -EINVAL;
+
+ if (active_links & ~ieee80211_vif_usable_links(&sdata->vif))
+ return -EINVAL;
+
+ /* nothing to do */
+ if (old_active == active_links)
+ return 0;
+
+ for (i = 0; i < IEEE80211_MLD_MAX_NUM_LINKS; i++)
+ link_confs[i] = sdata_dereference(sdata->vif.link_conf[i],
+ sdata);
+
+ if (add) {
+ sdata->vif.active_links |= active_links;
+ ret = drv_change_vif_links(local, sdata,
+ old_active,
+ sdata->vif.active_links,
+ link_confs);
+ if (ret) {
+ sdata->vif.active_links = old_active;
+ return ret;
+ }
+ }
+
+ for_each_set_bit(link_id, &rem, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_link_data *link;
+
+ link = sdata_dereference(sdata->link[link_id], sdata);
+
+ ieee80211_teardown_tdls_peers(link);
+
+ __ieee80211_link_release_channel(link, true);
+
+ /*
+ * If CSA is (still) active while the link is deactivated,
+ * just schedule the channel switch work for the time we
+ * had previously calculated, and we'll take the process
+ * from there.
+ */
+ if (link->conf->csa_active)
+ wiphy_hrtimer_work_queue(local->hw.wiphy,
+ &link->u.mgd.csa.switch_work,
+ link->u.mgd.csa.time -
+ ktime_get_boottime());
+ }
+
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_link_data *link;
+
+ link = sdata_dereference(sdata->link[link_id], sdata);
+
+ /*
+ * This call really should not fail. Unfortunately, it appears
+ * that this may happen occasionally with some drivers. Should
+ * it happen, we are stuck in a bad place as going backwards is
+ * not really feasible.
+ *
+ * So lets just tell link_use_channel that it must not fail to
+ * assign the channel context (from mac80211's perspective) and
+ * assume the driver is going to trigger a recovery flow if it
+ * had a failure.
+ * That really is not great nor guaranteed to work. But at least
+ * the internal mac80211 state remains consistent and there is
+ * a chance that we can recover.
+ */
+ ret = _ieee80211_link_use_channel(link,
+ &link->conf->chanreq,
+ IEEE80211_CHANCTX_SHARED,
+ true);
+ WARN_ON_ONCE(ret);
+
+ /*
+ * inform about the link info changed parameters after all
+ * stations are also added
+ */
+ }
+
+ list_for_each_entry(sta, &local->sta_list, list) {
+ if (sdata != sta->sdata)
+ continue;
+
+ /* this is very temporary, but do it anyway */
+ __ieee80211_sta_recalc_aggregates(sta,
+ old_active | active_links);
+
+ ret = drv_change_sta_links(local, sdata, &sta->sta,
+ old_active,
+ old_active | active_links);
+ WARN_ON_ONCE(ret);
+ }
+
+ ret = ieee80211_key_switch_links(sdata, rem, add);
+ WARN_ON_ONCE(ret);
+
+ list_for_each_entry(sta, &local->sta_list, list) {
+ if (sdata != sta->sdata)
+ continue;
+
+ __ieee80211_sta_recalc_aggregates(sta, active_links);
+
+ ret = drv_change_sta_links(local, sdata, &sta->sta,
+ old_active | active_links,
+ active_links);
+ WARN_ON_ONCE(ret);
+
+ /*
+ * Do it again, just in case - the driver might very
+ * well have called ieee80211_sta_recalc_aggregates()
+ * from there when filling in the new links, which
+ * would set it wrong since the vif's active links are
+ * not switched yet...
+ */
+ __ieee80211_sta_recalc_aggregates(sta, active_links);
+ }
+
+ for_each_set_bit(link_id, &add, IEEE80211_MLD_MAX_NUM_LINKS) {
+ struct ieee80211_link_data *link;
+
+ link = sdata_dereference(sdata->link[link_id], sdata);
+
+ ieee80211_mgd_set_link_qos_params(link);
+ ieee80211_link_info_change_notify(sdata, link,
+ BSS_CHANGED_ERP_CTS_PROT |
+ BSS_CHANGED_ERP_PREAMBLE |
+ BSS_CHANGED_ERP_SLOT |
+ BSS_CHANGED_HT |
+ BSS_CHANGED_BASIC_RATES |
+ BSS_CHANGED_BSSID |
+ BSS_CHANGED_CQM |
+ BSS_CHANGED_QOS |
+ BSS_CHANGED_TXPOWER |
+ BSS_CHANGED_BANDWIDTH |
+ BSS_CHANGED_TWT |
+ BSS_CHANGED_HE_OBSS_PD |
+ BSS_CHANGED_HE_BSS_COLOR);
+ }
+
+ old_active = sdata->vif.active_links;
+ sdata->vif.active_links = active_links;
+
+ if (rem) {
+ ret = drv_change_vif_links(local, sdata, old_active,
+ active_links, link_confs);
+ WARN_ON_ONCE(ret);
+ }
+
+ return 0;
+}
+
+int ieee80211_set_active_links(struct ieee80211_vif *vif, u16 active_links)
+{
+ struct ieee80211_sub_if_data *sdata = vif_to_sdata(vif);
+ struct ieee80211_local *local = sdata->local;
+ u16 old_active;
+ int ret;
+
+ lockdep_assert_wiphy(local->hw.wiphy);
+
+ if (WARN_ON(!active_links))
+ return -EINVAL;
+
+ old_active = sdata->vif.active_links;
+ if (old_active == active_links)
+ return 0;
+
+ if (!drv_can_activate_links(local, sdata, active_links))
+ return -EINVAL;
+
+ if (old_active & active_links) {
+ /*
+ * if there's at least one link that stays active across
+ * the change then switch to it (to those) first, and
+ * then enable the additional links
+ */
+ ret = _ieee80211_set_active_links(sdata,
+ old_active & active_links);
+ if (!ret)
+ ret = _ieee80211_set_active_links(sdata, active_links);
+ } else {
+ /* otherwise switch directly */
+ ret = _ieee80211_set_active_links(sdata, active_links);
+ }
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(ieee80211_set_active_links);
+
+void ieee80211_set_active_links_async(struct ieee80211_vif *vif,
+ u16 active_links)
+{
+ struct ieee80211_sub_if_data *sdata = vif_to_sdata(vif);
+
+ if (WARN_ON(!active_links))
+ return;
+
+ if (!ieee80211_sdata_running(sdata))
+ return;
+
+ if (sdata->vif.type != NL80211_IFTYPE_STATION)
+ return;
+
+ if (active_links & ~ieee80211_vif_usable_links(&sdata->vif))
+ return;
+
+ /* nothing to do */
+ if (sdata->vif.active_links == active_links)
+ return;
+
+ sdata->desired_active_links = active_links;
+ wiphy_work_queue(sdata->local->hw.wiphy, &sdata->activate_links_work);
+}
+EXPORT_SYMBOL_GPL(ieee80211_set_active_links_async);