[PATCH wireless-next 4/5] wifi: cfg80211: Fragment per-link station stats in nl80211_dump_station()
P Praneesh
praneesh.p at oss.qualcomm.com
Sun Jun 7 10:59:11 PDT 2026
In MLO scenarios, stations may have multiple links, each with distinct
statistics. When userspace tools like iw or hostapd request station dumps,
attempting to pack all per-link stats into a single netlink message can
easily exceed the default 4KB buffer limit, especially when more than two
links are active. This results in -EMSGSIZE errors and incomplete data
delivery.
To address this, fragment per-link station statistics across multiple
netlink messages to ensure reliable delivery of complete MLO station
information. Extend the stateful two-phase dump mechanism: the first
phase sends aggregated station-level statistics and the second phase
sends individual per-link statistics in separate fragments.
Add a new request flag attribute, NL80211_ATTR_STA_DUMP_LINK_STATS
(NLA_FLAG), for NL80211_CMD_GET_STATION dump. Userspace can set this
flag to request per-link station statistics for multi-link (MLO)
stations.
Extract this flag safely during the first dump invocation by passing an
attribute buffer to nl80211_prepare_wdev_dump(), caching the boolean
result in the dump context to avoid repeated parsing overhead.
Each per-link message now includes a nested NL80211_ATTR_STA_INFO
attribute to carry link-specific statistics, along with
NL80211_ATTR_MLO_LINKS for link context. Add a new helper function
nl80211_send_link_station() to construct these messages. The dump loop
iterates through the valid_links bitmask to process active links,
breaking out gracefully on EMSGSIZE while preserving the deeply allocated
station info for the next syscall iteration.
Even when no per-link statistics attributes are added, an empty
NL80211_ATTR_STA_INFO nest is still emitted. This is intentional, as
userspace expects NL80211_ATTR_STA_INFO to be present in
NL80211_CMD_NEW_STATION messages and may otherwise abort parsing or
drop the event if it is missing.
Backward compatibility is seamlessly preserved for non-MLO stations.
Signed-off-by: P Praneesh <praneesh.p at oss.qualcomm.com>
---
include/uapi/linux/nl80211.h | 32 +++-
net/wireless/nl80211.c | 332 +++++++++++++++++++++++++++++++++--
2 files changed, 351 insertions(+), 13 deletions(-)
diff --git a/include/uapi/linux/nl80211.h b/include/uapi/linux/nl80211.h
index 9998f6c0a665..f00a9cd8ab2c 100644
--- a/include/uapi/linux/nl80211.h
+++ b/include/uapi/linux/nl80211.h
@@ -1818,6 +1818,10 @@ enum nl80211_commands {
* @NL80211_ATTR_STA_INFO: information about a station, part of station info
* given for %NL80211_CMD_GET_STATION, nested attribute containing
* info as possible, see &enum nl80211_sta_info.
+ * In the per-link dump messages produced when %NL80211_ATTR_STA_DUMP_LINK_STATS
+ * is requested, the top-level occurrence of this attribute is empty.
+ * Per-link statistics are carried inside %NL80211_ATTR_MLO_LINKS;
+ * see %NL80211_ATTR_STA_DUMP_LINK_STATS for the full message layout.
*
* @NL80211_ATTR_WIPHY_BANDS: Information about an operating bands,
* consisting of a nested array.
@@ -2904,7 +2908,11 @@ enum nl80211_commands {
* @NL80211_ATTR_MLO_LINK_ID: A (u8) link ID for use with MLO, to be used with
* various commands that need a link ID to operate.
* @NL80211_ATTR_MLO_LINKS: A nested array of links, each containing some
- * per-link information and a link ID.
+ * per-link information and a link ID. In %NL80211_CMD_NEW_STATION
+ * responses produced by a %NL80211_ATTR_STA_DUMP_LINK_STATS dump,
+ * each link entry additionally carries %NL80211_ATTR_STA_INFO with
+ * per-link station statistics and %NL80211_ATTR_MAC with the
+ * link-specific MAC address.
* @NL80211_ATTR_MLD_ADDR: An MLD address, used with various commands such as
* authenticate/associate.
*
@@ -3165,6 +3173,26 @@ enum nl80211_commands {
* @NL80211_ATTR_NPCA_PRIMARY_FREQ: NPCA primary channel (u32)
* @NL80211_ATTR_NPCA_PUNCT_BITMAP: NPCA puncturing bitmap (u32)
*
+ * @NL80211_ATTR_STA_DUMP_LINK_STATS: Request flag for %NL80211_CMD_GET_STATION
+ * (dump mode only). When set on an MLD station, the dump produces two
+ * %NL80211_CMD_NEW_STATION messages per station per dump call:
+ *
+ * 1. An aggregated-stats message whose top-level %NL80211_ATTR_STA_INFO
+ * contains MLO-combined statistics (same content as a dump without
+ * this flag).
+ *
+ * 2. For each active link, a per-link message containing
+ * %NL80211_ATTR_MLO_LINKS with a single link entry. Each entry holds
+ * %NL80211_ATTR_MLO_LINK_ID, the link-specific %NL80211_ATTR_MAC,
+ * and %NL80211_ATTR_STA_INFO with per-link statistics (see
+ * &enum nl80211_sta_info). The top-level %NL80211_ATTR_STA_INFO in
+ * this message is intentionally empty; it is present solely for ABI
+ * compatibility with parsers that require %NL80211_ATTR_STA_INFO to
+ * be present in every %NL80211_CMD_NEW_STATION message.
+ *
+ * The aggregated message always precedes the per-link messages for the
+ * same station within a dump sequence.
+ *
* @NUM_NL80211_ATTR: total number of nl80211_attrs available
* @NL80211_ATTR_MAX: highest attribute number currently defined
* @__NL80211_ATTR_AFTER_LAST: internal use
@@ -3763,6 +3791,8 @@ enum nl80211_attrs {
NL80211_ATTR_NPCA_PRIMARY_FREQ,
NL80211_ATTR_NPCA_PUNCT_BITMAP,
+ NL80211_ATTR_STA_DUMP_LINK_STATS,
+
/* add attributes here, update the policy in nl80211.c */
__NL80211_ATTR_AFTER_LAST,
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index d146d6af6e48..3dba329c7eb2 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -54,6 +54,8 @@ enum nl80211_dump_station_phase {
struct nl80211_dump_station_ctx {
int sta_idx;
enum nl80211_dump_station_phase phase;
+ int link_idx;
+ bool dump_link_stats;
u8 mac_addr[ETH_ALEN];
struct station_info sinfo;
};
@@ -1126,6 +1128,7 @@ static const struct nla_policy nl80211_policy[NUM_NL80211_ATTR] = {
[NL80211_ATTR_NPCA_PRIMARY_FREQ] = { .type = NLA_U32 },
[NL80211_ATTR_NPCA_PUNCT_BITMAP] =
NLA_POLICY_FULL_RANGE(NLA_U32, &nl80211_punct_bitmap_range),
+ [NL80211_ATTR_STA_DUMP_LINK_STATS] = { .type = NLA_FLAG },
};
/* policy for the key attributes */
@@ -7874,6 +7877,185 @@ static bool nl80211_put_signal(struct sk_buff *msg, u8 mask, s8 *signal,
return true;
}
+static int nl80211_fill_link_station(struct sk_buff *msg,
+ struct cfg80211_registered_device *rdev,
+ struct link_station_info *link_sinfo)
+{
+ struct nlattr *bss_param, *link_sinfoattr;
+
+#define PUT_LINK_SINFO(attr, memb, type) do { \
+ BUILD_BUG_ON(sizeof(type) == sizeof(u64)); \
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_ ## attr) && \
+ nla_put_ ## type(msg, NL80211_STA_INFO_ ## attr, \
+ link_sinfo->memb)) \
+ goto nla_put_failure; \
+ } while (0)
+#define PUT_LINK_SINFO_U64(attr, memb) do { \
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_ ## attr) && \
+ nla_put_u64_64bit(msg, NL80211_STA_INFO_ ## attr, \
+ link_sinfo->memb, NL80211_STA_INFO_PAD)) \
+ goto nla_put_failure; \
+ } while (0)
+
+ link_sinfoattr = nla_nest_start_noflag(msg, NL80211_ATTR_STA_INFO);
+ if (!link_sinfoattr)
+ goto nla_put_failure;
+
+ PUT_LINK_SINFO(INACTIVE_TIME, inactive_time, u32);
+
+ if (link_sinfo->filled & (BIT_ULL(NL80211_STA_INFO_RX_BYTES) |
+ BIT_ULL(NL80211_STA_INFO_RX_BYTES64)) &&
+ nla_put_u32(msg, NL80211_STA_INFO_RX_BYTES,
+ (u32)link_sinfo->rx_bytes))
+ goto nla_put_failure;
+
+ if (link_sinfo->filled & (BIT_ULL(NL80211_STA_INFO_TX_BYTES) |
+ BIT_ULL(NL80211_STA_INFO_TX_BYTES64)) &&
+ nla_put_u32(msg, NL80211_STA_INFO_TX_BYTES,
+ (u32)link_sinfo->tx_bytes))
+ goto nla_put_failure;
+
+ PUT_LINK_SINFO_U64(RX_BYTES64, rx_bytes);
+ PUT_LINK_SINFO_U64(TX_BYTES64, tx_bytes);
+ PUT_LINK_SINFO_U64(RX_DURATION, rx_duration);
+ PUT_LINK_SINFO_U64(TX_DURATION, tx_duration);
+
+ if (wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_AIRTIME_FAIRNESS))
+ PUT_LINK_SINFO(AIRTIME_WEIGHT, airtime_weight, u16);
+
+ switch (rdev->wiphy.signal_type) {
+ case CFG80211_SIGNAL_TYPE_MBM:
+ PUT_LINK_SINFO(SIGNAL, signal, u8);
+ PUT_LINK_SINFO(SIGNAL_AVG, signal_avg, u8);
+ break;
+ default:
+ break;
+ }
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_CHAIN_SIGNAL)) {
+ if (!nl80211_put_signal(msg, link_sinfo->chains,
+ link_sinfo->chain_signal,
+ NL80211_STA_INFO_CHAIN_SIGNAL))
+ goto nla_put_failure;
+ }
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_CHAIN_SIGNAL_AVG)) {
+ if (!nl80211_put_signal(msg, link_sinfo->chains,
+ link_sinfo->chain_signal_avg,
+ NL80211_STA_INFO_CHAIN_SIGNAL_AVG))
+ goto nla_put_failure;
+ }
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_TX_BITRATE)) {
+ if (!nl80211_put_sta_rate(msg, &link_sinfo->txrate,
+ NL80211_STA_INFO_TX_BITRATE))
+ goto nla_put_failure;
+ }
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_RX_BITRATE)) {
+ if (!nl80211_put_sta_rate(msg, &link_sinfo->rxrate,
+ NL80211_STA_INFO_RX_BITRATE))
+ goto nla_put_failure;
+ }
+
+ PUT_LINK_SINFO(RX_PACKETS, rx_packets, u32);
+ PUT_LINK_SINFO(TX_PACKETS, tx_packets, u32);
+ PUT_LINK_SINFO(TX_RETRIES, tx_retries, u32);
+ PUT_LINK_SINFO(TX_FAILED, tx_failed, u32);
+ PUT_LINK_SINFO(EXPECTED_THROUGHPUT, expected_throughput, u32);
+ PUT_LINK_SINFO(BEACON_LOSS, beacon_loss_count, u32);
+
+ if (link_sinfo->filled & BIT_ULL(NL80211_STA_INFO_BSS_PARAM)) {
+ bss_param = nla_nest_start_noflag(msg,
+ NL80211_STA_INFO_BSS_PARAM);
+ if (!bss_param)
+ goto nla_put_failure;
+
+ if (((link_sinfo->bss_param.flags &
+ BSS_PARAM_FLAGS_CTS_PROT) &&
+ nla_put_flag(msg, NL80211_STA_BSS_PARAM_CTS_PROT)) ||
+ ((link_sinfo->bss_param.flags &
+ BSS_PARAM_FLAGS_SHORT_PREAMBLE) &&
+ nla_put_flag(msg,
+ NL80211_STA_BSS_PARAM_SHORT_PREAMBLE)) ||
+ ((link_sinfo->bss_param.flags &
+ BSS_PARAM_FLAGS_SHORT_SLOT_TIME) &&
+ nla_put_flag(msg,
+ NL80211_STA_BSS_PARAM_SHORT_SLOT_TIME)) ||
+ nla_put_u8(msg, NL80211_STA_BSS_PARAM_DTIM_PERIOD,
+ link_sinfo->bss_param.dtim_period) ||
+ nla_put_u16(msg, NL80211_STA_BSS_PARAM_BEACON_INTERVAL,
+ link_sinfo->bss_param.beacon_interval))
+ goto nla_put_failure;
+
+ nla_nest_end(msg, bss_param);
+ }
+
+ PUT_LINK_SINFO_U64(RX_DROP_MISC, rx_dropped_misc);
+ PUT_LINK_SINFO_U64(BEACON_RX, rx_beacon);
+ PUT_LINK_SINFO(BEACON_SIGNAL_AVG, rx_beacon_signal_avg, u8);
+ PUT_LINK_SINFO(RX_MPDUS, rx_mpdu_count, u32);
+ PUT_LINK_SINFO(FCS_ERROR_COUNT, fcs_err_count, u32);
+ if (wiphy_ext_feature_isset(&rdev->wiphy,
+ NL80211_EXT_FEATURE_ACK_SIGNAL_SUPPORT)) {
+ PUT_LINK_SINFO(ACK_SIGNAL, ack_signal, u8);
+ PUT_LINK_SINFO(ACK_SIGNAL_AVG, avg_ack_signal, s8);
+ }
+
+#undef PUT_LINK_SINFO
+#undef PUT_LINK_SINFO_U64
+
+ if (link_sinfo->pertid) {
+ struct nlattr *tidsattr;
+ int tid;
+
+ tidsattr = nla_nest_start_noflag(msg,
+ NL80211_STA_INFO_TID_STATS);
+ if (!tidsattr)
+ goto nla_put_failure;
+
+ for (tid = 0; tid < IEEE80211_NUM_TIDS + 1; tid++) {
+ struct cfg80211_tid_stats *tidstats;
+ struct nlattr *tidattr;
+
+ tidstats = &link_sinfo->pertid[tid];
+
+ if (!tidstats->filled)
+ continue;
+
+ tidattr = nla_nest_start_noflag(msg, tid + 1);
+ if (!tidattr)
+ goto nla_put_failure;
+
+#define PUT_TIDVAL_U64(attr, memb) do { \
+ if (tidstats->filled & BIT(NL80211_TID_STATS_ ## attr) && \
+ nla_put_u64_64bit(msg, NL80211_TID_STATS_ ## attr, \
+ tidstats->memb, NL80211_TID_STATS_PAD)) \
+ goto nla_put_failure; \
+ } while (0)
+
+ PUT_TIDVAL_U64(RX_MSDU, rx_msdu);
+ PUT_TIDVAL_U64(TX_MSDU, tx_msdu);
+ PUT_TIDVAL_U64(TX_MSDU_RETRIES, tx_msdu_retries);
+ PUT_TIDVAL_U64(TX_MSDU_FAILED, tx_msdu_failed);
+
+#undef PUT_TIDVAL_U64
+ if ((tidstats->filled &
+ BIT(NL80211_TID_STATS_TXQ_STATS)) &&
+ !nl80211_put_txq_stats(msg, &tidstats->txq_stats,
+ NL80211_TID_STATS_TXQ_STATS))
+ goto nla_put_failure;
+
+ nla_nest_end(msg, tidattr);
+ }
+
+ nla_nest_end(msg, tidsattr);
+ }
+
+ nla_nest_end(msg, link_sinfoattr);
+ return 0;
+
+nla_put_failure:
+ return -EMSGSIZE;
+}
+
static int nl80211_put_sta_info_common(struct sk_buff *msg,
struct cfg80211_registered_device *rdev,
struct station_info *sinfo)
@@ -8347,6 +8529,86 @@ nl80211_send_accumulated_station(struct sk_buff *msg,
return -EMSGSIZE;
}
+static int nl80211_send_link_station(struct sk_buff *msg,
+ struct netlink_callback *cb,
+ struct cfg80211_registered_device *rdev,
+ struct wireless_dev *wdev,
+ const u8 *mac_addr,
+ struct station_info *sinfo,
+ int link_idx)
+{
+ void *hdr;
+ struct nlattr *links, *link;
+ struct link_station_info *link_sinfo;
+ struct nlattr *sinfoattr;
+ int err;
+
+ hdr = nl80211hdr_put(msg, NETLINK_CB(cb->skb).portid,
+ cb->nlh->nlmsg_seq, NLM_F_MULTI,
+ NL80211_CMD_NEW_STATION);
+ if (!hdr)
+ return -EMSGSIZE;
+
+ if ((wdev->netdev &&
+ nla_put_u32(msg, NL80211_ATTR_IFINDEX, wdev->netdev->ifindex)) ||
+ nla_put_u64_64bit(msg, NL80211_ATTR_WDEV, wdev_id(wdev),
+ NL80211_ATTR_PAD) ||
+ nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, mac_addr) ||
+ nla_put_u32(msg, NL80211_ATTR_GENERATION, sinfo->generation)) {
+ err = -EMSGSIZE;
+ goto err_cancel;
+ }
+
+ sinfoattr = nla_nest_start_noflag(msg, NL80211_ATTR_STA_INFO);
+ if (!sinfoattr) {
+ err = -EMSGSIZE;
+ goto err_cancel;
+ }
+
+ link_sinfo = sinfo->links[link_idx];
+ if (!link_sinfo) {
+ err = -ENOENT;
+ goto err_cancel;
+ }
+
+ nla_nest_end(msg, sinfoattr);
+ if (!is_valid_ether_addr(link_sinfo->addr)) {
+ err = -EADDRNOTAVAIL;
+ goto err_cancel;
+ }
+
+ links = nla_nest_start(msg, NL80211_ATTR_MLO_LINKS);
+ if (!links) {
+ err = -EMSGSIZE;
+ goto err_cancel;
+ }
+
+ link = nla_nest_start(msg, link_idx + 1);
+ if (!link) {
+ err = -EMSGSIZE;
+ goto err_cancel;
+ }
+
+ if (nla_put_u8(msg, NL80211_ATTR_MLO_LINK_ID, link_idx) ||
+ nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, link_sinfo->addr)) {
+ err = -EMSGSIZE;
+ goto err_cancel;
+ }
+
+ err = nl80211_fill_link_station(msg, rdev, link_sinfo);
+ if (err)
+ goto err_cancel;
+
+ nla_nest_end(msg, link);
+ nla_nest_end(msg, links);
+ genlmsg_end(msg, hdr);
+ return 0;
+
+err_cancel:
+ genlmsg_cancel(msg, hdr);
+ return err;
+}
+
static int nl80211_dump_station(struct sk_buff *skb,
struct netlink_callback *cb)
{
@@ -8354,13 +8616,22 @@ static int nl80211_dump_station(struct sk_buff *skb,
struct wireless_dev *wdev;
struct nl80211_dump_station_cb *cb_data = (void *)cb->ctx;
struct nl80211_dump_station_ctx *ctx = cb_data->ctx;
+ struct nlattr **attrbuf = NULL;
int err, ret;
NL_ASSERT_CTX_FITS(struct nl80211_dump_station_cb);
- err = nl80211_prepare_wdev_dump(cb, &rdev, &wdev, NULL);
- if (err)
+ if (!ctx) {
+ attrbuf = kzalloc_objs(*attrbuf, NUM_NL80211_ATTR);
+ if (!attrbuf)
+ return -ENOMEM;
+ }
+
+ err = nl80211_prepare_wdev_dump(cb, &rdev, &wdev, attrbuf);
+ if (err) {
+ kfree(attrbuf);
return err;
+ }
/* nl80211_prepare_wdev_dump acquired it in the successful case */
__acquire(&rdev->wiphy.mtx);
@@ -8369,15 +8640,22 @@ static int nl80211_dump_station(struct sk_buff *skb,
if (!ctx) {
ctx = kzalloc_obj(*ctx);
if (!ctx) {
+ kfree(attrbuf);
err = -ENOMEM;
goto out_err;
}
ctx->phase = NL80211_DUMP_STA_PHASE_AGGREGATED;
ctx->sta_idx = 0;
+ ctx->link_idx = 0;
+ ctx->dump_link_stats =
+ !!attrbuf[NL80211_ATTR_STA_DUMP_LINK_STATS];
cb_data->ctx = ctx;
}
+ kfree(attrbuf);
+ attrbuf = NULL;
+
if (!wdev->netdev && wdev->iftype != NL80211_IFTYPE_NAN) {
err = -EINVAL;
goto out_err;
@@ -8388,9 +8666,9 @@ static int nl80211_dump_station(struct sk_buff *skb,
goto out_err;
}
- /* Phase 0: dump aggregated station info */
- if (ctx->phase == NL80211_DUMP_STA_PHASE_AGGREGATED) {
- while (true) {
+ while (true) {
+ /* Phase 0: dump aggregated station info */
+ if (ctx->phase == NL80211_DUMP_STA_PHASE_AGGREGATED) {
memset(&ctx->sinfo, 0, sizeof(ctx->sinfo));
for (int i = 0; i < IEEE80211_MLD_MAX_NUM_LINKS; i++) {
ctx->sinfo.links[i] =
@@ -8428,15 +8706,45 @@ static int nl80211_dump_station(struct sk_buff *skb,
goto out_err_release;
}
- /* Reset ctx for next station */
- cfg80211_sinfo_release_content(&ctx->sinfo);
- ctx->sta_idx++;
+ ctx->phase = NL80211_DUMP_STA_PHASE_PER_LINK;
}
- }
- ctx->phase = NL80211_DUMP_STA_PHASE_AGGREGATED;
- err = skb->len;
- goto out_err;
+ /* Phase 1: dump per-link station info */
+ if (ctx->phase == NL80211_DUMP_STA_PHASE_PER_LINK &&
+ ctx->dump_link_stats && ctx->sinfo.valid_links) {
+ while (ctx->link_idx < IEEE80211_MLD_MAX_NUM_LINKS) {
+ if (!(ctx->sinfo.valid_links &
+ BIT(ctx->link_idx))) {
+ ctx->link_idx++;
+ continue;
+ }
+
+ ret = nl80211_send_link_station(skb, cb, rdev,
+ wdev,
+ ctx->mac_addr,
+ &ctx->sinfo,
+ ctx->link_idx);
+ if (ret == -EMSGSIZE) {
+ err = skb->len;
+ goto out_err;
+ }
+
+ if (ret < 0) {
+ err = ret;
+ goto out_err_release;
+ }
+
+ ctx->link_idx++;
+ }
+ }
+
+ /* Reset ctx for next station */
+ cfg80211_sinfo_release_content(&ctx->sinfo);
+ memset(&ctx->sinfo, 0, sizeof(ctx->sinfo));
+ ctx->sta_idx++;
+ ctx->phase = NL80211_DUMP_STA_PHASE_AGGREGATED;
+ ctx->link_idx = 0;
+ }
out_err_release:
cfg80211_sinfo_release_content(&ctx->sinfo);
--
2.43.0
More information about the ath12k
mailing list