[PATCH v2 1/2] wifi: cfg80211: say why the auth/assoc BSS lookup failed

From: Louis Kotze

Date: Tue Jul 21 2026 - 14:26:49 EST


The BSS lookup for an authentication or association request can fail
for three distinct reasons: cfg80211 has no scan entry at all for the
BSSID/channel, an entry exists but is older than
IEEE80211_SCAN_RESULT_EXPIRE (and not held), or a fresh entry exists
but its use_for flags do not allow this use. All three currently
surface as the same generic extack message "Error fetching BSS for
link" on the MLO association path, and as a bare -ENOENT with no
message at all on the authentication and non-MLO association paths.

Since wpa_supplicant logs the extack message verbatim ("nl80211:
kernel reports: ..."), that message is often the only diagnostic a
user sees when an MLO association degrades to fewer links, and it
does not say whether rescanning would help. In practice the expired
case is common for MLO partner links: 6 GHz is passive-scan in many
regulatory domains, so the partner-link entry is routinely stale by
the time userspace requests the association even though the link is
perfectly usable.

Let __cfg80211_get_bss() take an optional extack and record, during
the same bss_lock walk that fails the lookup, whether any matching
entry was rejected only for being expired or only for not being
usable for the requested use, and set a distinct message for each
case. Reorder the checks in the walk so that an entry's identity
(type, privacy, channel, BSSID/SSID) is established before the
usability checks; this doesn't change which entry is returned since
an entry is only used when all checks pass. When matching entries
were rejected for both reasons, the use_for message wins: it is only
set for a current (non-expired) entry, so suggesting a rescan would
be misleading.

Also give the -EINVAL paths in nl80211_assoc_bss() proper messages
while at it, and keep pointing the bad_attr at the failing link on
the MLO path.

Signed-off-by: Louis Kotze <loukot@xxxxxxxxx>
---
v2: reworked per Johannes' feedback: the reason is captured inside
__cfg80211_get_bss() during the single bss_lock walk (no separate
re-walk helper, no race), reported via a new optional extack
parameter. The authentication path is now covered too, and the
patch grew a KUnit companion (patch 2). Retitled nl80211 -> cfg80211.

include/net/cfg80211.h | 7 +++++--
net/wireless/nl80211.c | 28 +++++++++++++++++-----------
net/wireless/scan.c | 35 +++++++++++++++++++++++++++--------
net/wireless/tests/scan.c | 2 +-
4 files changed, 50 insertions(+), 22 deletions(-)

diff --git a/include/net/cfg80211.h b/include/net/cfg80211.h
index b8e9fbb89e69..15c08b24502f 100644
--- a/include/net/cfg80211.h
+++ b/include/net/cfg80211.h
@@ -8424,6 +8424,8 @@ cfg80211_inform_bss(struct wiphy *wiphy,
* @bss_type: type of BSS, see &enum ieee80211_bss_type
* @privacy: privacy filter, see &enum ieee80211_privacy
* @use_for: indicates which use is intended
+ * @extack: (optional) extack that is filled with the reason when no
+ * usable entry was found; may be %NULL
*
* Return: Reference-counted BSS on success. %NULL on error.
*/
@@ -8433,7 +8435,8 @@ struct cfg80211_bss *__cfg80211_get_bss(struct wiphy *wiphy,
const u8 *ssid, size_t ssid_len,
enum ieee80211_bss_type bss_type,
enum ieee80211_privacy privacy,
- u32 use_for);
+ u32 use_for,
+ struct netlink_ext_ack *extack);

/**
* cfg80211_get_bss - get a BSS reference
@@ -8457,7 +8460,7 @@ cfg80211_get_bss(struct wiphy *wiphy, struct ieee80211_channel *channel,
{
return __cfg80211_get_bss(wiphy, channel, bssid, ssid, ssid_len,
bss_type, privacy,
- NL80211_BSS_USE_FOR_NORMAL);
+ NL80211_BSS_USE_FOR_NORMAL, NULL);
}

static inline struct cfg80211_bss *
diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 242071ad10d6..7fb6786a5e8e 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -12889,9 +12889,11 @@ static int nl80211_authenticate(struct sk_buff *skb, struct genl_info *info)
return -EINVAL;
}

- req.bss = cfg80211_get_bss(&rdev->wiphy, chan, bssid, ssid, ssid_len,
- IEEE80211_BSS_TYPE_ESS,
- IEEE80211_PRIVACY_ANY);
+ req.bss = __cfg80211_get_bss(&rdev->wiphy, chan, bssid, ssid, ssid_len,
+ IEEE80211_BSS_TYPE_ESS,
+ IEEE80211_PRIVACY_ANY,
+ NL80211_BSS_USE_FOR_NORMAL,
+ info->extack);
if (!req.bss)
return -ENOENT;

@@ -13036,6 +13038,7 @@ static int nl80211_crypto_settings(struct cfg80211_registered_device *rdev,
}

static struct cfg80211_bss *nl80211_assoc_bss(struct cfg80211_registered_device *rdev,
+ struct genl_info *info,
const u8 *ssid, int ssid_len,
struct nlattr **attrs,
int assoc_link_id, int link_id)
@@ -13045,8 +13048,10 @@ static struct cfg80211_bss *nl80211_assoc_bss(struct cfg80211_registered_device
const u8 *bssid;
u32 freq, use_for = 0;

- if (!attrs[NL80211_ATTR_MAC] || !attrs[NL80211_ATTR_WIPHY_FREQ])
+ if (!attrs[NL80211_ATTR_MAC] || !attrs[NL80211_ATTR_WIPHY_FREQ]) {
+ NL_SET_ERR_MSG(info->extack, "BSSID or frequency missing");
return ERR_PTR(-EINVAL);
+ }

bssid = nla_data(attrs[NL80211_ATTR_MAC]);

@@ -13055,8 +13060,10 @@ static struct cfg80211_bss *nl80211_assoc_bss(struct cfg80211_registered_device
freq += nla_get_u32(attrs[NL80211_ATTR_WIPHY_FREQ_OFFSET]);

chan = nl80211_get_valid_chan(&rdev->wiphy, freq);
- if (!chan)
+ if (!chan) {
+ NL_SET_ERR_MSG(info->extack, "invalid or disabled channel");
return ERR_PTR(-EINVAL);
+ }

if (assoc_link_id >= 0)
use_for = NL80211_BSS_USE_FOR_MLD_LINK;
@@ -13067,7 +13074,7 @@ static struct cfg80211_bss *nl80211_assoc_bss(struct cfg80211_registered_device
ssid, ssid_len,
IEEE80211_BSS_TYPE_ESS,
IEEE80211_PRIVACY_ANY,
- use_for);
+ use_for, info->extack);
if (!bss)
return ERR_PTR(-ENOENT);

@@ -13106,13 +13113,12 @@ static int nl80211_process_links(struct cfg80211_registered_device *rdev,
return -EINVAL;
}
links[link_id].bss =
- nl80211_assoc_bss(rdev, ssid, ssid_len, attrs,
+ nl80211_assoc_bss(rdev, info, ssid, ssid_len, attrs,
assoc_link_id, link_id);
if (IS_ERR(links[link_id].bss)) {
err = PTR_ERR(links[link_id].bss);
links[link_id].bss = NULL;
- NL_SET_ERR_MSG_ATTR(info->extack, link,
- "Error fetching BSS for link");
+ NL_SET_BAD_ATTR(info->extack, link);
return err;
}

@@ -13328,8 +13334,8 @@ static int nl80211_associate(struct sk_buff *skb, struct genl_info *info)
if (req.link_id >= 0)
return -EINVAL;

- req.bss = nl80211_assoc_bss(rdev, ssid, ssid_len, info->attrs,
- -1, -1);
+ req.bss = nl80211_assoc_bss(rdev, info, ssid, ssid_len,
+ info->attrs, -1, -1);
if (IS_ERR(req.bss))
return PTR_ERR(req.bss);
ap_addr = req.bss->bssid;
diff --git a/net/wireless/scan.c b/net/wireless/scan.c
index 05b7dc6b766c..f26e8436dbc1 100644
--- a/net/wireless/scan.c
+++ b/net/wireless/scan.c
@@ -1609,10 +1609,12 @@ struct cfg80211_bss *__cfg80211_get_bss(struct wiphy *wiphy,
const u8 *ssid, size_t ssid_len,
enum ieee80211_bss_type bss_type,
enum ieee80211_privacy privacy,
- u32 use_for)
+ u32 use_for,
+ struct netlink_ext_ack *extack)
{
struct cfg80211_registered_device *rdev = wiphy_to_rdev(wiphy);
struct cfg80211_internal_bss *bss, *res = NULL;
+ bool expired = false, unusable = false;
unsigned long now = jiffies;
int bss_privacy;

@@ -1634,22 +1636,39 @@ struct cfg80211_bss *__cfg80211_get_bss(struct wiphy *wiphy,
continue;
if (!is_valid_ether_addr(bss->pub.bssid))
continue;
- if ((bss->pub.use_for & use_for) != use_for)
+ if (!is_bss(&bss->pub, bssid, ssid, ssid_len))
continue;
+
/* Don't get expired BSS structs */
if (time_after(now, bss->ts + IEEE80211_SCAN_RESULT_EXPIRE) &&
- !atomic_read(&bss->hold))
+ !atomic_read(&bss->hold)) {
+ expired = true;
+ continue;
+ }
+
+ if ((bss->pub.use_for & use_for) != use_for) {
+ unusable = true;
continue;
- if (is_bss(&bss->pub, bssid, ssid, ssid_len)) {
- res = bss;
- bss_ref_get(rdev, res);
- break;
}
+
+ res = bss;
+ bss_ref_get(rdev, res);
+ break;
}

spin_unlock_bh(&rdev->bss_lock);
- if (!res)
+ if (!res) {
+ if (unusable)
+ NL_SET_ERR_MSG(extack,
+ "BSS cannot be used for the requested operation");
+ else if (expired)
+ NL_SET_ERR_MSG(extack,
+ "BSS entry is expired, scan again");
+ else
+ NL_SET_ERR_MSG(extack,
+ "BSS not found in scan results");
return NULL;
+ }
trace_cfg80211_return_bss(&res->pub);
return &res->pub;
}
diff --git a/net/wireless/tests/scan.c b/net/wireless/tests/scan.c
index b1a9c1466d6c..2fc717317ac3 100644
--- a/net/wireless/tests/scan.c
+++ b/net/wireless/tests/scan.c
@@ -617,7 +617,7 @@ static void test_inform_bss_ml_sta(struct kunit *test)
link_bss = __cfg80211_get_bss(wiphy, NULL, sta_prof.bssid, NULL, 0,
IEEE80211_BSS_TYPE_ANY,
IEEE80211_PRIVACY_ANY,
- 0);
+ 0, NULL);
KUNIT_ASSERT_NOT_NULL(test, link_bss);
KUNIT_EXPECT_EQ(test, link_bss->signal, 0);
KUNIT_EXPECT_EQ(test, link_bss->beacon_interval,
--
2.55.0