[PATCH wireless-next 4/5] wifi: cfg80211: Fragment per-link station stats in nl80211_dump_station()

Praneesh P praneesh.p at oss.qualcomm.com
Mon Jun 8 12:01:36 PDT 2026


On 6/8/2026 1:12 PM, Johannes Berg wrote:
> On Sun, 2026-06-07 at 23:29 +0530, P Praneesh wrote:
>>   
>> +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);
> See previous note about _noflag() in all of this code - that shouldn't
> be there.
Noted, I will address it in next version.
>> +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;
> That seems really odd? why even bother going into the whole thing if
> it's invalid? Also, doesn't that ENOENT get propagated all the way and
> it aborts? I guess it does but it should never happen because of
> valid_links? Still seems a bit odd.
Both the link_sinfo null check and is_valid_ether_addr() check
will be moved to the top of the function, before any header or
nest construction. The null check for sinfo->links[link_idx] is
already guarded upstream by the valid_links bitmask so it should
never be NULL in practice, I'll add a WARN_ON_ONCE and skip rather
than returning -ENOENT, which would otherwise abort the entire dump.
Will it be okay ?
>> @@ -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);
> perhaps better to __free(kfree) instead of doing all these kfree()
> calls?
Yes, will use __free(kfree) for attrbuf.
>> -	/* 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) {
> changing it now also kind of argues for not having it in the first patch
> in the first place ;-)
Noted, the loop restructuring will be moved entirely into
patch 4, not split across patches 3 and 4.
>>   			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);
>>
> I think conceptually this code is structured a bit strangely. It would
> seem a lot simpler to me to do something like
>
>
> 	// not literally such a macro, just the while (true) loop
> 	for_each_station_starting_from_idx(...) {
> 		if (ctx->phase == 0) {
> 			// fill sinfo etc.
> 		}
>
> 		// common stuff
> 		nl80211hdr_put()
> 		nla_put_u32(msg, NL80211_ATTR_IFINDEX...)
> 		nla_put_u64_64bit(msg, NL80211_ATTR_WDEV..)
> 		nla_put(msg, NL80211_ATTR_MAC...)
> 		nla_put_u32(msg, NL80211_ATTR_GENERATION...)
>
> 		switch (phase) {
> 		case 0:
> 			nl80211_put_sta_info_common(...);
> 			phase++;
> 			ctx->link_id = 0;
> 			break;
> 		case 1:
> 			if (!multi-phase-requested)
> 				break;
> 			nl80211_put_link_station(..., ctx->link_id);
> 			ctx->link_id++;
> 			if (link_id == NUM_LINKS)
> 				ctx->phase = 0; // next sta
> 			break;
> 		}
> 	}
>
>
> or something like that, more like we handle wiphy dump with all the
> messages, for example?
>
> I think it'd be better to have just one place that creates the header
> and all the common info.
>
> But maybe it's just my brain pathways being trained for that scheme.
>
> johannes
ok, I will try to align with this suggested model. I'll refactor so the 
outer loop
produces exactly one netlink message per iteration, with header
construction in one place:

while (true) {
     if (ctx->phase == PHASE_AGGREGATED) {
         /* fetch sinfo from driver once per station */
         memset(&ctx->sinfo, ...);
         /* allocate links[], call rdev_dump_station() */
         /* apply cfg80211_sta_set_mld_sinfo() if needed */
     }

     hdr = nl80211hdr_put(msg, ...);
     /* common attrs: ifindex, wdev, mac, generation */
     nla_put_u32(msg, NL80211_ATTR_IFINDEX, ...);
     nla_put_u64_64bit(msg, NL80211_ATTR_WDEV, ...);
     nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, ctx->mac_addr);
     nla_put_u32(msg, NL80211_ATTR_GENERATION, ...);

     switch (ctx->phase) {
     case PHASE_AGGREGATED:
         nl80211_put_sta_info_common(msg, rdev, &ctx->sinfo);
         ctx->phase = PHASE_PER_LINK;
         ctx->link_idx = first_valid_link();
         break;
     case PHASE_PER_LINK:
         nl80211_put_link_station(msg, rdev, &ctx->sinfo, ctx->link_idx);
         advance ctx->link_idx to next valid link;
         if (no more links) {
             cfg80211_sinfo_release_content(&ctx->sinfo);
             ctx->sta_idx++;
             ctx->phase = PHASE_AGGREGATED;
         }
         break;
     }

     genlmsg_end(msg, hdr);   /* or genlmsg_cancel() on EMSGSIZE 
break-out */
}


This eliminates the duplicated nla_put_u32/u64/MAC header blocks
that currently exist in both nl80211_send_accumulated_station()
and nl80211_send_link_station(), and makes the EMSGSIZE bail-out
uniform since we return before committing the message.

should the rdev_dump_station() / sinfo fill stay outside the
per-message loop (as a separate if (phase == AGGREGATED) guard
at the top), or is it preferable to fold it into the switch
case? I've sketched the former above since fetching sinfo is
logically per-station, not per-message. I'm also ok to adjust
if you'd prefer it inside the switch ?

- Praneesh



More information about the ath12k mailing list