From johannes at sipsolutions.net  Sat Aug  1 01:01:52 2026
From: johannes at sipsolutions.net (Johannes Berg)
Date: Sat,  1 Aug 2026 10:01:52 +0200
Subject: [PATCH 2/2] wlantest: Allow longer PTK
In-Reply-To: <20260801100149.ef9803f1ba39.I8dbf8cf20299f83e890883bab66359717beecc0a@changeid>
References: <20260801100149.ef9803f1ba39.I8dbf8cf20299f83e890883bab66359717beecc0a@changeid>
Message-ID: <20260801100149.8b553e51675e.I69844f4c31028bcb42c5ffe19387d176f817e69a@changeid>

From: Johannes Berg <johannes.berg at intel.com>

Now with the SMD KDK, we need up to 112 bytes.

Signed-off-by: Johannes Berg <johannes.berg at intel.com>
---
 wlantest/wlantest.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/wlantest/wlantest.c b/wlantest/wlantest.c
index a76ae768eba1..51559e28b8c1 100644
--- a/wlantest/wlantest.c
+++ b/wlantest/wlantest.c
@@ -265,7 +265,7 @@ static void add_ptk_variants(struct wlantest *wt, const u8 *ptk,
 static int add_ptk_file(struct wlantest *wt, const char *ptk_file)
 {
 	FILE *f;
-	u8 ptk[96];
+	u8 ptk[112];
 	size_t ptk_len;
 	char buf[300], *pos;
 	struct wlantest_ptk *p;
@@ -289,7 +289,7 @@ static int add_ptk_file(struct wlantest *wt, const char *ptk_file)
 		ptk_len /= 2;
 		if (ptk_len != 16 && ptk_len != 32 &&
 		    ptk_len != 48 && ptk_len != 64 &&
-		    ptk_len != 80 && ptk_len != 96)
+		    ptk_len != 80 && ptk_len != 96 && ptk_len != 112)
 			continue;
 
 		if (hexstr2bin(buf, ptk, ptk_len) < 0)
-- 
2.55.0



From johannes at sipsolutions.net  Sat Aug  1 01:01:51 2026
From: johannes at sipsolutions.net (Johannes Berg)
Date: Sat,  1 Aug 2026 10:01:51 +0200
Subject: [PATCH 1/2] wlantest: Support various PTK configurations
Message-ID: <20260801100149.ef9803f1ba39.I8dbf8cf20299f83e890883bab66359717beecc0a@changeid>

From: Ilan Peer <ilan.peer at intel.com>

When reading a raw PTK from a file, parse it in various possible
combinations of <KCK length, KEK length, TK length> and store all
the variants, so they could later be used for decryption etc.

Signed-off-by: Ilan Peer <ilan.peer at intel.com>
Reviewed-by: Andrei Otcheretianski <andrei.otcheretianski at intel.com>
Signed-off-by: Johannes Berg <johannes.berg at intel.com>
---
 wlantest/wlantest.c | 98 ++++++++++++++++++++++++++++++++++-----------
 1 file changed, 75 insertions(+), 23 deletions(-)

diff --git a/wlantest/wlantest.c b/wlantest/wlantest.c
index 02ffadf98f2c..a76ae768eba1 100644
--- a/wlantest/wlantest.c
+++ b/wlantest/wlantest.c
@@ -204,10 +204,68 @@ static int add_pmk_file(struct wlantest *wt, const char *pmk_file)
 }
 
 
+static void add_ptk_variants(struct wlantest *wt, const u8 *ptk,
+			     size_t ptk_len)
+{
+	/*
+	 * PTK layout: KCK(16/24/32) | KEK(0/16/32/64) | TK(16/32) [| KDK]
+	 * Since we do not know the cipher, KCK, KEK or TK length, try all
+	 * valid combinations.
+	 * Note: The case of KCK length = 0 is not handled here, and
+	 * should be handled separately.
+	 */
+	static const size_t kck_sizes[] = { 16, 24, 32 };
+	static const size_t kek_sizes[] = { 0, 16, 32, 64 };
+	static const size_t tk_sizes[] = { 16, 32 };
+	size_t ci, ki, ti;
+	bool added = false;
+
+	for (ci = 0; ci < ARRAY_SIZE(kck_sizes); ci++) {
+		for (ki = 0; ki < ARRAY_SIZE(kek_sizes); ki++) {
+			for (ti = 0; ti < ARRAY_SIZE(tk_sizes); ti++) {
+				struct wlantest_ptk *p;
+				size_t kckl = kck_sizes[ci];
+				size_t kekl = kek_sizes[ki];
+				size_t tkl = tk_sizes[ti];
+				size_t tk_off = kckl + kekl;
+
+				if (tk_off + tkl > ptk_len)
+					continue;
+
+				p = os_zalloc(sizeof(*p));
+				if (!p)
+					return;
+
+				os_memcpy(p->ptk.kck, ptk, kckl);
+				p->ptk.kck_len = kckl;
+				if (kekl) {
+					os_memcpy(p->ptk.kek, ptk + kckl, kekl);
+					p->ptk.kek_len = kekl;
+				}
+
+				os_memcpy(p->ptk.tk, ptk + tk_off, tkl);
+				p->ptk.tk_len = tkl;
+				p->ptk_len = ptk_len;
+				dl_list_add(&wt->ptk, &p->list);
+				added = true;
+
+				wpa_printf(MSG_DEBUG,
+					   "Added PTK variant: KCK=%zu KEK=%zu TK=%zu",
+					   kckl, kekl, tkl);
+			}
+		}
+	}
+
+	if (added)
+		wpa_hexdump(MSG_DEBUG, "Added PTK from file",
+			    ptk, ptk_len);
+}
+
+
 static int add_ptk_file(struct wlantest *wt, const char *ptk_file)
 {
 	FILE *f;
-	u8 ptk[80];
+	u8 ptk[96];
 	size_t ptk_len;
 	char buf[300], *pos;
 	struct wlantest_ptk *p;
@@ -222,43 +280,37 @@ static int add_ptk_file(struct wlantest *wt, const char *ptk_file)
 		pos = buf;
 		while (*pos && *pos != '\r' && *pos != '\n')
 			pos++;
+
 		*pos = '\0';
 		ptk_len = pos - buf;
 		if (ptk_len & 1)
 			continue;
+
 		ptk_len /= 2;
 		if (ptk_len != 16 && ptk_len != 32 &&
 		    ptk_len != 48 && ptk_len != 64 &&
-		    ptk_len != 80)
+		    ptk_len != 80 && ptk_len != 96)
 			continue;
+
 		if (hexstr2bin(buf, ptk, ptk_len) < 0)
 			continue;
-		p = os_zalloc(sizeof(*p));
-		if (p == NULL)
-			break;
+
 		if (ptk_len < 48) {
+			/* Raw TK (16 or 32 bytes) */
+			p = os_zalloc(sizeof(*p));
+			if (!p)
+				break;
+
 			os_memcpy(p->ptk.tk, ptk, ptk_len);
 			p->ptk.tk_len = ptk_len;
 			p->ptk_len = 32 + ptk_len;
-		} else if (ptk_len == 80) {
-			os_memcpy(p->ptk.kck, ptk, 32);
-			p->ptk.kck_len = 32;
-			os_memcpy(p->ptk.kek, ptk + 32, 16);
-			p->ptk.kek_len = 16;
-			os_memcpy(p->ptk.tk, ptk + 32 + 16, ptk_len - 32 - 16);
-			p->ptk.tk_len = ptk_len - 32 - 16;
-			p->ptk_len = ptk_len;
-		} else {
-			os_memcpy(p->ptk.kck, ptk, 16);
-			p->ptk.kck_len = 16;
-			os_memcpy(p->ptk.kek, ptk + 16, 16);
-			p->ptk.kek_len = 16;
-			os_memcpy(p->ptk.tk, ptk + 32, ptk_len - 32);
-			p->ptk.tk_len = ptk_len - 32;
-			p->ptk_len = ptk_len;
+			dl_list_add(&wt->ptk, &p->list);
+			wpa_hexdump(MSG_DEBUG, "Added PTK from file",
+				    ptk, ptk_len);
+			continue;
 		}
-		dl_list_add(&wt->ptk, &p->list);
-		wpa_hexdump(MSG_DEBUG, "Added PTK from file", ptk, ptk_len);
+
+		add_ptk_variants(wt, ptk, ptk_len);
 	}
 
 	fclose(f);
-- 
2.55.0



From johannes at sipsolutions.net  Sat Aug  1 01:04:13 2026
From: johannes at sipsolutions.net (Johannes Berg)
Date: Sat,  1 Aug 2026 10:04:13 +0200
Subject: [PATCH] tests: ap_ciphers: read debugfs conf_flags if it exists
Message-ID: <20260801100412.7b18219f00af.Ib0da1cf638303424d5affc89adc412708a2eb69e@changeid>

From: Johannes Berg <johannes.berg at intel.com>

The code here in the tests differentiates pairwise and
group keys by the presence of the 'station' file in
mac80211's key debugfs. This currently works because
the code is only invoked on the client, where keys are
in fact currently installed that way by mac80211. In
other modes (IBSS, mesh, NAN) it wouldn't work, since
GTKs are installed per STA as well.

Given the complexity of this difference in mac80211,
I'm working on changing that, robbing the test of a
way to differentiate pairwise and group keys. To still
be able to differentiate, mac80211 will then expose a
new 'conf_flags' file, where we can check the pairwise
flag (bit 3) instead, if it exists.

Signed-off-by: Johannes Berg <johannes.berg at intel.com>
---
 tests/hwsim/test_ap_ciphers.py | 26 ++++++++++++++++++++++----
 1 file changed, 22 insertions(+), 4 deletions(-)

diff --git a/tests/hwsim/test_ap_ciphers.py b/tests/hwsim/test_ap_ciphers.py
index 9078e4497c1a..57fe47f6937c 100644
--- a/tests/hwsim/test_ap_ciphers.py
+++ b/tests/hwsim/test_ap_ciphers.py
@@ -550,8 +550,17 @@ def get_rx_spec(phy, keytype=KT_PTK):
             files = os.listdir(keydir)
             if keytype == KT_PTK and "station" not in files:
                 continue
-            if keytype != KT_PTK and "station" in files:
-                continue
+            try:
+                # if present, check pairwise flag
+                with open(keydir + '/conf_flags') as f:
+                    flags = int(f.read(), 0)
+                if keytype != KT_PTK and flags & (1 << 3):
+                    continue
+                if keytype == KT_PTK and not (flags & (1 << 3)):
+                    continue
+            except OSError as e:
+                if keytype != KT_PTK and "station" in files:
+                    continue
             with open(keydir + "/rx_spec") as f:
                 return f.read()
     except OSError as e:
@@ -574,8 +583,17 @@ def get_tk_replay_counter(phy, keytype=KT_PTK):
             files = os.listdir(keydir)
             if keytype == KT_PTK and "station" not in files:
                 continue
-            if keytype != KT_PTK and "station" in files:
-                continue
+            try:
+                # if present, check pairwise flag
+                with open(keydir + '/conf_flags') as f:
+                    flags = int(f.read(), 0)
+                if keytype != KT_PTK and flags & (1 << 3):
+                    continue
+                if keytype == KT_PTK and not (flags & (1 << 3)):
+                    continue
+            except OSError as e:
+                if keytype != KT_PTK and "station" in files:
+                    continue
             with open(keydir + "/replays") as f:
                 return int(f.read())
     except OSError as e:
-- 
2.55.0



From viper at landau.one  Sat Aug  1 11:39:07 2026
From: viper at landau.one (viper at landau.one)
Date: Sat,  1 Aug 2026 21:39:07 +0300
Subject: [PATCH 1/1] build: static libwpa_client supported
Message-ID: <20260801183907.1606878-1-viper@landau.one>

From: Konstantin Menyaev <viper at landau.one>

CONFIG_BUILD_WPA_CLIENT_STATIC option added to build the static
libwpa_client library.

This is useful for applications that statically link against the
wpa_supplicant client API.

Signed-off-by: Konstantin Menyaev <viper at landau.one>
---
 wpa_supplicant/Makefile | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
index 2b82c42c8..4ab7ae512 100644
--- a/wpa_supplicant/Makefile
+++ b/wpa_supplicant/Makefile
@@ -24,6 +24,12 @@ ifdef CONFIG_BUILD_WPA_CLIENT_SO
 _all: libwpa_client.so
 endif
 
+ifdef CONFIG_BUILD_WPA_CLIENT_STATIC
+# add the dependency this way to allow CONFIG_BUILD_WPA_CLIENT_STATIC
+# being set in the config which is read by build.rules
+_all: libwpa_client.a
+endif
+
 ifndef CONFIG_NO_WPA_PASSPHRASE
 # add the dependency this way to allow CONFIG_NO_WPA_PASSPHRASE
 # being set in the config which is read by build.rules
@@ -91,6 +97,10 @@ endif
 ifdef CONFIG_BUILD_WPA_CLIENT_SO
 	install -m 0644 -D libwpa_client.so $(DESTDIR)/$(LIBDIR)/libwpa_client.so
 	install -m 0644 -D ../src/common/wpa_ctrl.h $(DESTDIR)/$(INCDIR)/wpa_ctrl.h
+endif
+ifdef CONFIG_BUILD_WPA_CLIENT_STATIC
+	install -m 0644 -D libwpa_client.a $(DESTDIR)/$(LIBDIR)/libwpa_client.a
+	install -m 0644 -D ../src/common/wpa_ctrl.h $(DESTDIR)/$(INCDIR)/wpa_ctrl.h
 endif
 	if ls eap_*.so >/dev/null 2>&1; then \
 		install -d $(DESTDIR)$(LIBDIR)/wpa_supplicant && \
-- 
2.55.0



From j at w1.fi  Sat Aug  1 12:54:38 2026
From: j at w1.fi (Jouni Malinen)
Date: Sat, 1 Aug 2026 22:54:38 +0300
Subject: [PATCH v2] AP: Introduce support for testing ML link enable
In-Reply-To: <8e3cd11c6c4d4a507cd3697c68de24afad5ef191.1775724565.git.lorenzo@kernel.org>
References: <8e3cd11c6c4d4a507cd3697c68de24afad5ef191.1775724565.git.lorenzo@kernel.org>
Message-ID: <am5O_tZSM2L9xRQM@w1.fi>

On Thu, Apr 09, 2026 at 10:52:05AM +0200, Lorenzo Bianconi wrote:
> Introduce LINK_ENABLE control interface command in order to enable a link
> previously removed via hostapd_cli remove_link command.
> The LINK_ENABLE command relies on the following syntax:
> 
> $hostapd_cli -i <interface> link_enable bss_conf=<phy>:<config-file>
> 
> Similar to ML link removal support added in commit 'd95838b7932b
> ("AP: Add support for testing ML link removal")', compile LINK_ENABLE
> control interface command just if CONFIG_TESTING_OPTIONS is enabled
> since it depends on remove_link counterpart.

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sat Aug  1 12:55:07 2026
From: j at w1.fi (Jouni Malinen)
Date: Sat, 1 Aug 2026 22:55:07 +0300
Subject: [PATCH 0/3] MLD: Fix association failures from stale per-link
 BSS entries
In-Reply-To: <20260709191033.61941-1-loukot@gmail.com>
References: <20260709191033.61941-1-loukot@gmail.com>
Message-ID: <am5PG4Is3W4vmFUE@w1.fi>

On Thu, Jul 09, 2026 at 09:10:30PM +0200, Louis Kotze wrote:
> Louis Kotze (3):
>   MLD: Scan the channels of missing links for the ML probe
>   MLD: Treat stale per-link scan entries as missing links
>   tests: MLD reassociation with stale per-link BSS entries

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sat Aug  1 12:55:38 2026
From: j at w1.fi (Jouni Malinen)
Date: Sat, 1 Aug 2026 22:55:38 +0300
Subject: [PATCH v3 0/2] SME: Fall back from PMKSA caching on
 deauthentication
In-Reply-To: <20260728114601.327504-1-loukot@gmail.com>
References: <20260728114601.327504-1-loukot@gmail.com>
Message-ID: <am5POgB6WSy5OviK@w1.fi>

On Tue, Jul 28, 2026 at 01:45:59PM +0200, Louis Kotze wrote:
> Louis Kotze (2):
>   SME: Fall back from PMKSA caching if the AP deauthenticates
>   tests: SAE PMKSA caching attempt rejected with deauthentication

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sat Aug  1 12:55:59 2026
From: j at w1.fi (Jouni Malinen)
Date: Sat, 1 Aug 2026 22:55:59 +0300
Subject: [PATCH] tests: ap_ciphers: read debugfs conf_flags if it exists
In-Reply-To: <20260801100412.7b18219f00af.Ib0da1cf638303424d5affc89adc412708a2eb69e@changeid>
References: <20260801100412.7b18219f00af.Ib0da1cf638303424d5affc89adc412708a2eb69e@changeid>
Message-ID: <am5PT4LVi_l7nuNL@w1.fi>

On Sat, Aug 01, 2026 at 10:04:13AM +0200, Johannes Berg wrote:
> The code here in the tests differentiates pairwise and
> group keys by the presence of the 'station' file in
> mac80211's key debugfs. This currently works because
> the code is only invoked on the client, where keys are
> in fact currently installed that way by mac80211. In
> other modes (IBSS, mesh, NAN) it wouldn't work, since
> GTKs are installed per STA as well.
> 
> Given the complexity of this difference in mac80211,
> I'm working on changing that, robbing the test of a
> way to differentiate pairwise and group keys. To still
> be able to differentiate, mac80211 will then expose a
> new 'conf_flags' file, where we can check the pairwise
> flag (bit 3) instead, if it exists.

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sun Aug  2 01:33:48 2026
From: j at w1.fi (Jouni Malinen)
Date: Sun, 2 Aug 2026 11:33:48 +0300
Subject: [PATCH v2 3/5] PR: Use preferred frequency for channel selection
 in PASN ranging
In-Reply-To: <20260731095227.1153772-4-peddolla.reddy@oss.qualcomm.com>
References: <20260731095227.1153772-1-peddolla.reddy@oss.qualcomm.com>
 <20260731095227.1153772-4-peddolla.reddy@oss.qualcomm.com>
Message-ID: <am8A7IP5m-TrHJuh@w1.fi>

On Fri, Jul 31, 2026 at 03:22:25PM +0530, Peddolla Harshavardhan Reddy wrote:
> Add a preferred_freq field to pr_pasn_ranging_params. When set,
> the op_class with the highest bandwidth whose channel range covers
> the preferred frequency is selected from the common channel set.

What is the "preferred frequency" referring to here? If that is any
frequency, the design below does not really work in general.

> Channel numbers are op_class-specific so the match is done by
> comparing the preferred frequency against the frequency range
> [chan_freq, chan_freq + bw) of each entry.

Why? That chan_freq is most likely not the starting frequency of the
channel, but a center frequency of a 20 MHz channel that is within the
full frequency range. That would also imply that chan_freq + bw might be
beyond the end frequency of the channel.

-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sun Aug  2 01:35:03 2026
From: j at w1.fi (Jouni Malinen)
Date: Sun, 2 Aug 2026 11:35:03 +0300
Subject: [PATCH v2 5/5] PR: Prefer higher band when selecting best
 channel at equal bandwidth
In-Reply-To: <20260731095227.1153772-6-peddolla.reddy@oss.qualcomm.com>
References: <20260731095227.1153772-1-peddolla.reddy@oss.qualcomm.com>
 <20260731095227.1153772-6-peddolla.reddy@oss.qualcomm.com>
Message-ID: <am8BN9BwfWwxAq8R@w1.fi>

On Fri, Jul 31, 2026 at 03:22:27PM +0530, Peddolla Harshavardhan Reddy wrote:
> Currently pr_choose_best_channel() picks the first channel it finds
> with the highest bandwidth, with no tiebreaker when two op classes
> offer the same bandwidth on different bands.
> 
> Add pr_op_class_band_priority() as a tiebreaker so that among op
> classes with equal bandwidth a higher-band channel is preferred
> (6 GHz > 5 GHz > 2.4 GHz). Higher bands usually provide higher
> throughput due to less congestion and interference compared to the
> 2.4 GHz band, making them a more suitable choice for ranging when
> bandwidth is equal.

Why is 6 GHz better than 5 GHz here? That commit message is just
pointing out that 5 or 6 GHz bands would likely be better than 2.4 GHz.

-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Sun Aug  2 01:42:02 2026
From: j at w1.fi (Jouni Malinen)
Date: Sun, 2 Aug 2026 11:42:02 +0300
Subject: [PATCH v2 0/5] PR: resource leak fixes and improvements
In-Reply-To: <20260731095227.1153772-1-peddolla.reddy@oss.qualcomm.com>
References: <20260731095227.1153772-1-peddolla.reddy@oss.qualcomm.com>
Message-ID: <am8C2uaS3zf8Mm03@w1.fi>

On Fri, Jul 31, 2026 at 03:22:22PM +0530, Peddolla Harshavardhan Reddy wrote:
> The first two patches improve PASN robustness for out-of-band ranging
> triggers: a new return value distinguishes an unacked auth frame 1 from
> other TX status cases, and the PR initiator uses this to retransmit the
> frame after each unacked TX status until the responder comes on channel
> or the session times out.

Thanks, applied those two patches.

> The remaining three patches add caller control over channel selection.
> A preferred_freq field is added to pr_pasn_ranging_params and wired
> through to the ctrl interface PR_PASN_START command. When set, the
> op_class with the widest bandwidth whose channel range covers the
> preferred frequency is selected from the common channel set, falling
> back to automatic selection if no match is found. Among op classes with
> equal bandwidth, a higher band is preferred as a tiebreaker (6 GHz >
> 5 GHz > 2.4 GHz).

These have open comments/dependencies.

-- 
Jouni Malinen                                            PGP id EFC895FA


From n.rottgardt at gmail.com  Sun Aug  2 13:23:05 2026
From: n.rottgardt at gmail.com (Nils Rottgardt)
Date: Sun, 2 Aug 2026 22:23:05 +0200
Subject: =?UTF-8?Q?Re=3A_=5BPATCH=5D_WNM=3A_Add_API_for_Wi=E2=80=91Fi_contro?=
 =?UTF-8?Q?ller_to_cancel_planned_disassociation?=
In-Reply-To: <20260609211529.13481-1-n.rottgardt@gmail.com>
References: <20260609211529.13481-1-n.rottgardt@gmail.com>
Message-ID: <66614232-994a-43e0-989c-a34279785153@gmail.com>

Hi,

a small ping to this patch. If there is something missing let me know. 
It is necessary to allow the controller to handle station responses well.

Regards,

Nils

Am 09.06.2026 um 23:15 schrieb Nils Hendrik Rottgardt:
> sta_info: Move PMKSA cache deletion from the preparation step to the disassociation timer.
> This allows a planned disassociation to be cleanly cancelled.
>
> wnm_ap: Add an API for Wi?Fi controllers such as Usteer.
> Currently, a STA is disassociated in ieee802_11_rx_bss_trans_mgmt_resp even when it responds with a valid status code.
> To support scenarios between roaming without disassociation (soft) and roaming with imminent disassociation (hard), a controller must be able to stop a forced disassociation.
> This behaviour is aligned with IEEE specifications. Since the decision logic can be complex and infrastructure?dependent, it should be handled by the controller.
>
> Signed-off-by: Nils Hendrik Rottgardt <n.rottgardt at gmail.com>
> ---
>   src/ap/sta_info.c |  8 +++++++-
>   src/ap/wnm_ap.c   | 28 +++++++++++++++++++++-------
>   src/ap/wnm_ap.h   |  2 ++
>   3 files changed, 30 insertions(+), 8 deletions(-)
>
> diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
> index 2bb7e1235..700eae8bb 100644
> --- a/src/ap/sta_info.c
> +++ b/src/ap/sta_info.c
> @@ -741,8 +741,14 @@ skip_poll:
>   		eloop_register_timeout(AP_DISASSOC_DELAY, 0, ap_handle_timer,
>   				       hapd, sta);
>   		break;
> -	case STA_DISASSOC:
>   	case STA_DISASSOC_FROM_CLI:
> +		/*
> +		 * Prevent STA from reconnecting using cached PMKSA to force
> +		 * full authentication with the authentication server (which may
> +		 * decide to reject the connection),
> +		 */
> +		wpa_auth_pmksa_remove(hapd->wpa_auth, sta->addr);
> +	case STA_DISASSOC:
>   		ap_sta_set_authorized(hapd, sta, 0);
>   		sta->flags &= ~WLAN_STA_ASSOC;
>   		hostapd_set_sta_flags(hapd, sta);
> diff --git a/src/ap/wnm_ap.c b/src/ap/wnm_ap.c
> index 90922cd85..f3a998236 100644
> --- a/src/ap/wnm_ap.c
> +++ b/src/ap/wnm_ap.c
> @@ -882,13 +882,6 @@ static void set_disassoc_timer(struct hostapd_data *hapd, struct sta_info *sta,
>   {
>   	int timeout, beacon_int;
>   
> -	/*
> -	 * Prevent STA from reconnecting using cached PMKSA to force
> -	 * full authentication with the authentication server (which may
> -	 * decide to reject the connection),
> -	 */
> -	wpa_auth_pmksa_remove(hapd->wpa_auth, sta->addr);
> -
>   	beacon_int = hapd->iconf->beacon_int;
>   	if (beacon_int < 1)
>   		beacon_int = 100; /* best guess */
> @@ -905,6 +898,27 @@ static void set_disassoc_timer(struct hostapd_data *hapd, struct sta_info *sta,
>   }
>   
>   
> +int stop_disassoc_timer(struct hostapd_data *hapd,
> +                             struct sta_info *sta)
> +{
> +	if (!hapd || !sta)
> +		return -1;
> +
> +	if (sta->timeout_next == STA_DISASSOC_FROM_CLI) {
> +		sta->timeout_next = STA_NULLFUNC;
> +		eloop_cancel_timeout(ap_handle_timer, hapd, sta);
> +		eloop_register_timeout(2, 0, ap_handle_timer, hapd, sta);
> +		wpa_printf(MSG_DEBUG, "Disassociation timer for " MACSTR
> +			   " stopped", MAC2STR(sta->addr));
> +		return 0;
> +	} else {
> +		wpa_printf(MSG_DEBUG, "Disassociation timer for " MACSTR
> +			   " is not active - next timeout: %d", MAC2STR(sta->addr), sta->timeout_next);
> +		return -1;
> +	}
> +}
> +
> +
>   int wnm_send_ess_disassoc_imminent(struct hostapd_data *hapd,
>   				   struct sta_info *sta, const char *url,
>   				   int disassoc_timer)
> diff --git a/src/ap/wnm_ap.h b/src/ap/wnm_ap.h
> index f86c6b2af..f21812dd2 100644
> --- a/src/ap/wnm_ap.h
> +++ b/src/ap/wnm_ap.h
> @@ -23,6 +23,8 @@ int wnm_send_bss_tm_req(struct hostapd_data *hapd, struct sta_info *sta,
>   			const u8 *bss_term_dur, u8 dialog_token,
>   			const char *url, const u8 *nei_rep, size_t nei_rep_len,
>   			const u8 *mbo_attrs, size_t mbo_len);
> +int stop_disassoc_timer(struct hostapd_data *hapd,
> +                             struct sta_info *sta);
>   void ap_sta_reset_steer_flag_timer(void *eloop_ctx, void *timeout_ctx);
>   int wnm_send_coloc_intf_req(struct hostapd_data *hapd, struct sta_info *sta,
>   			    unsigned int auto_report, unsigned int timeout);


From chung-hsien.hsu at infineon.com  Mon Aug  3 03:41:46 2026
From: chung-hsien.hsu at infineon.com (Chung-Hsien Hsu)
Date: Mon, 3 Aug 2026 18:41:46 +0800
Subject: [PATCH] wpa_supplicant: Cache PMK for 802.1X 4-way handshake
 offload
In-Reply-To: <amcf9TX23Xvkdyco@w1.fi>
References: <20260603022228.3052-1-chung-hsien.hsu@infineon.com>
 <amcf9TX23Xvkdyco@w1.fi>
Message-ID: <20260803104146.GA354007@ISCN5CG4472QHC.infineon.com>

On Mon, Jul 27, 2026 at 11:08:05AM +0200, Jouni Malinen wrote:
> Could you please clarify why this is needed, i.e., whether this is
> fixing an issue (and if so, what kind of issue) or this is just to keep
> things more consistent or prepared for potential future changes?

This fixes a functional issue.

With IEEE 802.1X 4-way handshake offload, the normal host-side EAPOL-Key
processing path is bypassed, so the PMK is not stored in the WPA state
machine and no PMKSA cache entry is created.

As a result, a subsequent connection cannot use PMKSA caching and may
require a full EAP authentication again. This change stores the PMK and
creates the PMKSA cache entry for the applicable non-FT and non-Suite-B
RSN cases.

Chung-Hsien


From allen.ye at mediatek.com  Mon Aug  3 23:34:34 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:34 +0800
Subject: [PATCH v13 2/5] hostapd: Support 80/160/320 MHz widths in hostapd_is_usable_chans
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <20260804063437.646306-3-allen.ye@mediatek.com>

Add hostapd_is_usable_chan_seg() to check all constituent 20 MHz
sub-channels for a given bandwidth segment. The channel count is derived
from oper_chwidth (4 for 80 MHz, 8 for 160 MHz, 16 for 320 MHz), and
the start frequency is computed from the center frequency index. For
80+80 MHz, both seg0 and seg1 are checked independently.

hostapd_is_usable_chans() dispatches to hostapd_is_usable_chan_seg() for
5 GHz/6 GHz non-HT modes with a valid center frequency. The condition
intentionally excludes 2.4 GHz (mode != IEEE80211A) and HT-only
configurations (oper_chwidth == CONF_OPER_CHWIDTH_USE_HT), as well as
the 80+80 ACS case where the center frequency has not yet been filled in
by acs_adjust_center_freq(). These cases fall through to the existing
20/40 MHz path.

This is a preliminary patch to introduce AFC support.

Tested-by: Felix Fietkau <nbd at nbd.name>
Tested-by: Krishna Chaitanya <chaitanya.mgit at gmail.com>
Reviewed-by: Money Wang <money.wang at mediatek.com>
Signed-off-by: Allen Ye <allen.ye at mediatek.com>
---
 src/ap/hw_features.c | 94 ++++++++++++++++++++++++++++++++++++++++----
 src/ap/hw_features.h |  6 +++
 2 files changed, 93 insertions(+), 7 deletions(-)

diff --git a/src/ap/hw_features.c b/src/ap/hw_features.c
index 30edac214..01a693828 100644
--- a/src/ap/hw_features.c
+++ b/src/ap/hw_features.c
@@ -1083,16 +1083,83 @@ static bool hostapd_is_usable_punct_bitmap(struct hostapd_iface *iface)
 }
 
 
+static int hostapd_is_usable_chan_seg(struct hostapd_iface *iface)
+{
+	int central, oper_chwidth, err;
+	int start_chan, start_freq, chan_num, i;
+
+	oper_chwidth = hostapd_get_oper_chwidth(iface->conf);
+	central = hostapd_get_oper_centr_freq_seg0_idx(iface->conf);
+
+	switch (oper_chwidth) {
+	case CONF_OPER_CHWIDTH_80MHZ:
+	case CONF_OPER_CHWIDTH_80P80MHZ:
+		chan_num = 4;
+		break;
+	case CONF_OPER_CHWIDTH_160MHZ:
+		chan_num = 8;
+		break;
+	case CONF_OPER_CHWIDTH_320MHZ:
+		chan_num = 16;
+		break;
+	default:
+		return 0;
+	}
+	start_chan = central - chan_num * 2 + 2;
+	start_freq = hw_get_freq(iface->current_mode, start_chan);
+
+	if (!start_freq) {
+		wpa_printf(MSG_ERROR, "frequency not present");
+		return 0;
+	}
+
+	for (i = 0; i < chan_num; i++) {
+		int freq = start_freq + i * 20;
+
+		err = hostapd_is_usable_chan(iface, freq, freq == iface->freq);
+		if (err <= 0) {
+			wpa_printf(MSG_ERROR, "frequency %d is not allowed",
+				   freq);
+			return err;
+		}
+	}
+
+	if (oper_chwidth != CONF_OPER_CHWIDTH_80P80MHZ)
+		return 1;
+
+	central = hostapd_get_oper_centr_freq_seg1_idx(iface->conf);
+	start_chan = central - chan_num * 2 + 2;
+	start_freq = hw_get_freq(iface->current_mode, start_chan);
+
+	if (!start_freq) {
+		wpa_printf(MSG_ERROR, "frequency not present");
+		return 0;
+	}
+
+	for (i = 0; i < chan_num; i++) {
+		int freq = start_freq + i * 20;
+
+		err = hostapd_is_usable_chan(iface, freq, freq == iface->freq);
+		if (err <= 0) {
+			wpa_printf(MSG_ERROR, "frequency %d is not allowed",
+				   freq);
+			return err;
+		}
+	}
+	return 1;
+}
+
+
 /* Returns:
  * 1 = usable
  * 0 = not usable
  * -1 = not currently usable due to 6 GHz NO-IR
  */
-static int hostapd_is_usable_chans(struct hostapd_iface *iface)
+int hostapd_is_usable_chans(struct hostapd_iface *iface)
 {
 	int secondary_freq, new_sec, bw_flag;
 	struct hostapd_channel_data *pri_chan;
-	int err, err2;
+	int err, err2, central, oper_chwidth;
 
 	if (!iface->current_mode)
 		return 0;
@@ -1105,11 +1172,6 @@ static int hostapd_is_usable_chans(struct hostapd_iface *iface)
 		return 0;
 	}
 
-	err = hostapd_is_usable_chan(iface, pri_chan->freq, 1);
-	if (err <= 0) {
-		wpa_printf(MSG_ERROR, "Primary frequency not allowed");
-		return err;
-	}
 	err = hostapd_is_usable_edmg(iface);
 	if (err <= 0)
 		return err;
@@ -1117,6 +1179,24 @@ static int hostapd_is_usable_chans(struct hostapd_iface *iface)
 	if (!hostapd_is_usable_punct_bitmap(iface))
 		return 0;
 
+	oper_chwidth = hostapd_get_oper_chwidth(iface->conf);
+
+	/*
+	 * The central channel is not filled in acs_adjust_center_freq() after
+	 * ACS of 80p80. In the case we only check the primary 40 MHz.
+	 */
+	central = hostapd_get_oper_centr_freq_seg0_idx(iface->conf);
+
+	if (iface->current_mode->mode == HOSTAPD_MODE_IEEE80211A &&
+	    oper_chwidth != CONF_OPER_CHWIDTH_USE_HT && central)
+		return hostapd_is_usable_chan_seg(iface);
+
+	err = hostapd_is_usable_chan(iface, pri_chan->freq, 1);
+	if (err <= 0) {
+		wpa_printf(MSG_ERROR, "Primary frequency not allowed");
+		return err;
+	}
+
 	if (!iface->conf->secondary_channel)
 		return 1;
 
diff --git a/src/ap/hw_features.h b/src/ap/hw_features.h
index 6f945cc77..4993e68af 100644
--- a/src/ap/hw_features.h
+++ b/src/ap/hw_features.h
@@ -32,6 +32,7 @@ int hostapd_hw_skip_mode(struct hostapd_iface *iface,
 int hostapd_determine_mode(struct hostapd_iface *iface);
 void hostapd_free_multi_hw_info(struct hostapd_multi_hw_info *multi_hw_info);
 int hostapd_set_current_hw_info(struct hostapd_iface *iface, int oper_freq);
+int hostapd_is_usable_chans(struct hostapd_iface *iface);
 #else /* NEED_AP_MLME */
 static inline void
 hostapd_free_hw_features(struct hostapd_hw_modes *hw_features,
@@ -115,6 +116,11 @@ static inline int hostapd_set_current_hw_info(struct hostapd_iface *iface,
 {
 	return 0;
 }
+
+static inline int hostapd_is_usable_chans(struct hostapd_iface *iface)
+{
+	return 1;
+}
 #endif /* NEED_AP_MLME */
 
 #endif /* HW_FEATURES_H */
-- 
2.45.2



From allen.ye at mediatek.com  Mon Aug  3 23:34:32 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:32 +0800
Subject: [PATCH v13 0/5] Introduce Automated Frequency Coordination (AFC) support
Message-ID: <20260804063437.646306-1-allen.ye@mediatek.com>

This patch series introduces Automated Frequency Coordination (AFC) support
for Standard Power Devices (SPDs) operating in the 6GHz UNII-5 and UNII-7
bands. AFC is a regulatory requirement for SPDs to determine a list of
available channels and their permissible power levels.

The implementation introduces a new standalone daemon, afcd, to manage
communication with the AFC server. A client within hostapd then interacts
with afcd via a Unix socket to fetch spectrum availability and update AP
parameters accordingly. The series also includes hwsim tests for validation.

Note: The AFC daemon support (afcd) was merged in v11. This series contains
the remaining patches.

The implementation has been tested against the Wi-Fi Alliance's AFC DUT
Test Harness [0].

[0] https://github.com/Wi-FiTestSuite/AFC-DUT/tree/main
Changes since v12:
- Rework hostapd_is_usable_chans() to optimize ht40_plus_minus_allowed
  handling and introduce hostapd_is_usable_chan_seg() to handle 80/160/320
  MHz bandwidths
- Fix potential memory leaks when processing AFC server response
- Rebase on top of hostapd main branch
Changes since v11:
- Fix hwsim test fail
- Remove merged patches and rebase on top of hostapd main branch

Allen Ye (3):
  hostapd: Fix ht40_plus_minus_allowed secondary channel selection
  hostapd: Support 80/160/320 MHz widths in hostapd_is_usable_chans
  tests: Add AFC hwsim tests

Lorenzo Bianconi (2):
  AFC: Add AFC client support
  AFC: Update TPE IE according to AFC

 hostapd/Makefile                   |    8 +
 hostapd/config_file.c              |  250 ++++++
 hostapd/ctrl_iface.c               |   47 ++
 hostapd/defconfig                  |    3 +
 hostapd/hostapd.conf               |   41 +
 hostapd/hostapd_cli.c              |   29 +
 src/ap/afc.c                       | 1128 ++++++++++++++++++++++++++++
 src/ap/ap_config.c                 |   15 +
 src/ap/ap_config.h                 |   46 ++
 src/ap/hostapd.c                   |   24 +
 src/ap/hostapd.h                   |   61 ++
 src/ap/hw_features.c               |  118 ++-
 src/ap/hw_features.h               |    6 +
 src/ap/ieee802_11.c                |   53 +-
 src/common/wpa_ctrl.h              |    3 +
 tests/hwsim/example-hostapd.config |    1 +
 tests/hwsim/test_afc.py            |  335 +++++++++
 17 files changed, 2132 insertions(+), 36 deletions(-)
 create mode 100644 src/ap/afc.c
 create mode 100644 tests/hwsim/test_afc.py

-- 
2.45.2



From allen.ye at mediatek.com  Mon Aug  3 23:34:33 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:33 +0800
Subject: [PATCH v13 1/5] hostapd: Fix ht40_plus_minus_allowed secondary channel selection
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <20260804063437.646306-2-allen.ye@mediatek.com>

When both HT40+ and HT40- are allowed (ht40_plus_minus_allowed) and the
configured secondary_channel direction fails the usability check, the
code should probe only the opposing direction. The old code probed both
+20 and -20 unconditionally, redundantly retrying the already-failing
direction.

Replace the two fixed-offset probes with a single probe at
-secondary_channel * 20, select the corresponding allowed_bw flag
(HOSTAPD_CHAN_WIDTH_40P for +1, HOSTAPD_CHAN_WIDTH_40M for -1) based on
the new direction, and flip secondary_channel only if the probe succeeds.

Signed-off-by: Allen Ye <allen.ye at mediatek.com>
---
 src/ap/hw_features.c | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/ap/hw_features.c b/src/ap/hw_features.c
index be8ed5352..30edac214 100644
--- a/src/ap/hw_features.c
+++ b/src/ap/hw_features.c
@@ -1090,7 +1090,7 @@ static bool hostapd_is_usable_punct_bitmap(struct hostapd_iface *iface)
  */
 static int hostapd_is_usable_chans(struct hostapd_iface *iface)
 {
-	int secondary_freq;
+	int secondary_freq, new_sec, bw_flag;
 	struct hostapd_channel_data *pri_chan;
 	int err, err2;
 
@@ -1133,18 +1133,18 @@ static int hostapd_is_usable_chans(struct hostapd_iface *iface)
 	if (!iface->conf->ht40_plus_minus_allowed)
 		return err;
 
-	/* Both HT40+ and HT40- are set, pick a valid secondary channel */
-	secondary_freq = iface->freq + 20;
-	err2 = hostapd_is_usable_chan(iface, secondary_freq, 0);
-	if (err2 > 0 && (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40P)) {
-		iface->conf->secondary_channel = 1;
-		return 1;
-	}
+	/* Both HT40+ and HT40- are set, check the other secondary 40 */
+	new_sec = -iface->conf->secondary_channel;
+	bw_flag = (new_sec == 1) ?
+		  HOSTAPD_CHAN_WIDTH_40P : HOSTAPD_CHAN_WIDTH_40M;
 
-	secondary_freq = iface->freq - 20;
+	secondary_freq = iface->freq + new_sec * 20;
 	err2 = hostapd_is_usable_chan(iface, secondary_freq, 0);
-	if (err2 > 0 && (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40M)) {
-		iface->conf->secondary_channel = -1;
+	if (err2 <= 0)
+		return err;
+
+	if (pri_chan->allowed_bw & bw_flag) {
+		iface->conf->secondary_channel = new_sec;
 		return 1;
 	}
 
-- 
2.45.2



From allen.ye at mediatek.com  Mon Aug  3 23:34:36 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:36 +0800
Subject: [PATCH v13 4/5] AFC: Update TPE IE according to AFC
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <20260804063437.646306-5-allen.ye@mediatek.com>

From: Lorenzo Bianconi <lorenzo at kernel.org>

Update Transmit Power Envelope (TPE) IE according to the reply from AFC
coordinator on UNII-5 or UNII-7 6GHz bands.

Tested-by: Felix Fietkau <nbd at nbd.name>
Tested-by: Allen Ye <allen.ye at mediatek.com>
Tested-by: Krishna Chaitanya <chaitanya.mgit at gmail.com>
Signed-off-by: Lorenzo Bianconi <lorenzo at kernel.org>
Signed-off-by: Allen Ye <allen.ye at mediatek.com>
---
 src/ap/afc.c        | 42 +++++++++++++++++++++++++++++++++++
 src/ap/hostapd.h    |  9 ++++++++
 src/ap/ieee802_11.c | 53 ++++++++++++++++++++++++++++++---------------
 3 files changed, 86 insertions(+), 18 deletions(-)

diff --git a/src/ap/afc.c b/src/ap/afc.c
index bcdd487aa..f174c82f9 100644
--- a/src/ap/afc.c
+++ b/src/ap/afc.c
@@ -1084,3 +1084,45 @@ static int hostapd_afc_reset_channels(struct hostapd_iface *iface)
 
 	return ret;
 }
+
+
+int hostap_afc_get_chan_max_eirp_power(struct hostapd_iface *iface, bool psd,
+				       int *power)
+{
+	unsigned int i;
+
+	if (!he_reg_is_sp(iface->conf->he_6ghz_reg_pwr_type))
+		return -EINVAL;
+
+	if (!iface->afc.data_valid)
+		return -EINVAL;
+
+	if (psd) {
+		for (i = 0; i < iface->afc.num_freq_range; i++) {
+			struct afc_freq_range_elem *f;
+
+			f = &iface->afc.freq_range[i];
+			if (iface->freq >= f->low_freq &&
+			    iface->freq <= f->high_freq) {
+				*power = (int)(2 * f->max_psd);
+				return 0;
+			}
+		}
+	} else {
+		for (i = 0; i < iface->afc.num_chan_info; i++) {
+			struct afc_chan_info_elem *c;
+
+			c = &iface->afc.chan_info_list[i];
+			if (c->chan == iface->conf->channel) {
+				unsigned int j;
+
+				*power = 0;
+				for (j = 0; j < ARRAY_SIZE(c->power); j++)
+					*power = MAX(*power,
+						     (int)(2 * c->power[j]));
+				return 0;
+			}
+		}
+	}
+	return -EINVAL;
+}
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index 285fd0839..443d3e93b 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -847,11 +847,20 @@ void hostapd_ocv_check_csa_sa_query(void *eloop_ctx, void *timeout_ctx);
 void hostapd_switch_color(struct hostapd_data *hapd, u64 bitmap);
 void hostapd_cleanup_cca_params(struct hostapd_data *hapd);
 #ifdef CONFIG_AFC
+int hostap_afc_get_chan_max_eirp_power(struct hostapd_iface *iface, bool psd,
+				       int *power);
 int hostapd_afc_handle_request(struct hostapd_iface *iface);
 void hostapd_afc_send_request(struct hostapd_iface *iface);
 void hostapd_afc_stop(struct hostapd_iface *iface);
 void hostap_afc_disable_channels(struct hostapd_iface *iface);
 #else
+static inline int
+hostap_afc_get_chan_max_eirp_power(struct hostapd_iface *iface, bool psd,
+				   int *power)
+{
+	return -EINVAL;
+}
+
 static inline int hostapd_afc_handle_request(struct hostapd_iface *iface)
 {
 	return 1;
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 86806ef17..6c9a72d74 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -9319,42 +9319,59 @@ u8 * hostapd_eid_txpower_envelope(struct hostapd_data *hapd, u8 *eid)
 	 */
 	if (is_6ghz_op_class(iconf->op_class)) {
 		enum max_tx_pwr_interpretation tx_pwr_intrpn;
+		int err;
+		int max_eirp_psd = REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT * 2;
+		int max_eirp_power = iconf->reg_def_cli_eirp;
 
 		/* Same Maximum Transmit Power for all 20 MHz bands */
 		tx_pwr_count = 0;
 		tx_pwr_intrpn = REGULATORY_CLIENT_EIRP_PSD;
 
 		/* Default Transmit Power Envelope for Global Operating Class */
-		if (hapd->iconf->reg_def_cli_eirp_psd != -1)
-			tx_pwr = hapd->iconf->reg_def_cli_eirp_psd;
-		else
-			tx_pwr = REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT * 2;
+		err = hostap_afc_get_chan_max_eirp_power(iface, true,
+							 &max_eirp_psd);
+		if (err < 0) {
+			if (hapd->iconf->reg_def_cli_eirp_psd != -1)
+				max_eirp_psd =
+					hapd->iconf->reg_def_cli_eirp_psd;
+			else
+				max_eirp_psd =
+					REG_PSD_MAX_TXPOWER_FOR_DEFAULT_CLIENT * 2;
+		}
 
 		eid = hostapd_add_tpe_info(eid, tx_pwr_count, tx_pwr_intrpn,
-					   REG_DEFAULT_CLIENT, tx_pwr);
+					   REG_DEFAULT_CLIENT, max_eirp_psd);
 
 		/* Indoor Access Point must include an additional TPE for
 		 * subordinate devices */
 		if (he_reg_is_indoor(iconf->he_6ghz_reg_pwr_type)) {
-			/* TODO: Extract PSD limits from channel data */
-			if (hapd->iconf->reg_sub_cli_eirp_psd != -1)
-				tx_pwr = hapd->iconf->reg_sub_cli_eirp_psd;
-			else
-				tx_pwr = REG_PSD_MAX_TXPOWER_FOR_SUBORDINATE_CLIENT * 2;
+			if (err < 0) {
+				/* non-AFC connection */
+				if (hapd->iconf->reg_sub_cli_eirp_psd != -1)
+					max_eirp_psd =
+						hapd->iconf->reg_sub_cli_eirp_psd;
+				else
+					max_eirp_psd =
+						REG_PSD_MAX_TXPOWER_FOR_SUBORDINATE_CLIENT * 2;
+			}
 			eid = hostapd_add_tpe_info(eid, tx_pwr_count,
 						   tx_pwr_intrpn,
 						   REG_SUBORDINATE_CLIENT,
-						   tx_pwr);
+						   max_eirp_psd);
 		}
 
-		if (iconf->reg_def_cli_eirp != -1 &&
-		    he_reg_is_sp(iconf->he_6ghz_reg_pwr_type))
-			eid = hostapd_add_tpe_info(
-				eid, tx_pwr_count, REGULATORY_CLIENT_EIRP,
-				REG_DEFAULT_CLIENT,
-				hapd->iconf->reg_def_cli_eirp);
+		if (hostap_afc_get_chan_max_eirp_power(iface, false,
+						       &max_eirp_power)) {
+			max_eirp_power = iconf->reg_def_cli_eirp;
+			if (max_eirp_power == -1 ||
+			    !he_reg_is_sp(iconf->he_6ghz_reg_pwr_type))
+				return eid;
+		}
 
-		return eid;
+		return hostapd_add_tpe_info(eid, tx_pwr_count,
+					    REGULATORY_CLIENT_EIRP,
+					    REG_DEFAULT_CLIENT,
+					    max_eirp_power);
 	}
 #endif /* CONFIG_IEEE80211AX */
 
-- 
2.45.2



From allen.ye at mediatek.com  Mon Aug  3 23:34:37 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:37 +0800
Subject: [PATCH v13 5/5] tests: Add AFC hwsim tests
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <20260804063437.646306-6-allen.ye@mediatek.com>

Add hwsim tests for Automated Frequency Coordination (AFC) using a
mock AFCD server over a Unix domain socket. Tests cover:
- Empty AFC response (no usable channels)
- availableFrequencyInfo with 20/40/80/160/320 MHz channel widths
- availableChannelInfo with 20/40/80/160/320 MHz channel widths

Signed-off-by: Allen Ye <allen.ye at mediatek.com>
---
 tests/hwsim/example-hostapd.config |   1 +
 tests/hwsim/test_afc.py            | 335 +++++++++++++++++++++++++++++
 2 files changed, 336 insertions(+)
 create mode 100644 tests/hwsim/test_afc.py

diff --git a/tests/hwsim/example-hostapd.config b/tests/hwsim/example-hostapd.config
index 60b466807..92338aae0 100644
--- a/tests/hwsim/example-hostapd.config
+++ b/tests/hwsim/example-hostapd.config
@@ -131,3 +131,4 @@ CONFIG_ENC_ASSOC=y
 CONFIG_PMKSA_PRIVACY=y
 CONFIG_IEEE8021X_AUTH=y
 CONFIG_ROBUST_AV=y
+CONFIG_AFC=y
diff --git a/tests/hwsim/test_afc.py b/tests/hwsim/test_afc.py
new file mode 100644
index 000000000..110556517
--- /dev/null
+++ b/tests/hwsim/test_afc.py
@@ -0,0 +1,335 @@
+# AFC tests
+# Copyright (c) 2025, MediaTek Corporation
+#
+# This software may be distributed under the terms of the BSD license.
+# See README for more details.
+
+import logging
+logger = logging.getLogger()
+import os
+import threading
+from datetime import datetime, timedelta
+import socket
+import json
+
+import hwsim_utils
+import hostapd
+from utils import *
+
+class AFCDServer:
+    def __init__(self):
+        self.path = '/tmp/afcd.sock'
+        if os.path.exists(self.path):
+            os.remove(self.path)
+        self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.s.bind(self.path)
+        self.s.listen(1)
+
+    def listen_reply(self, freq_info, chan_info):
+        if freq_info is None:
+            freq_info = []
+        if chan_info is None:
+            chan_info = []
+
+        try:
+            self.s.settimeout(1)
+            self.conn, _ = self.s.accept()
+        except Exception as e:
+            logger.info("hostapd failed to connect to afcd")
+            self.close()
+            if os.path.exists(self.path):
+                os.unlink(self.path)
+            return
+
+        try:
+            req_raw = self.conn.recv(8192).decode()
+        except socket.timeout:
+            logger.info("AFCD failed to get request")
+            req_raw = None
+        if req_raw:
+            logger.info(f"Received: {req_raw}")
+            if 'availableSpectrumInquiryRequests' in req_raw:
+                try:
+                    req_json = json.loads(req_raw)
+                    requestId = req_json["availableSpectrumInquiryRequests"][0]["requestId"]
+                except Exception as e:
+                    logger.info(f"Failed to parse requestId: {e}")
+                    self.close()
+
+                expire_time = (datetime.now() + timedelta(hours=24)).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+                resp = {
+                    "availableSpectrumInquiryResponses": [
+                        {
+                            "response": {
+                                "responseCode": 0,
+                                "shortDescription": "SUCCESS"
+                            },
+                            "availableFrequencyInfo": freq_info,
+                            "availableChannelInfo": chan_info,
+                            "requestId": requestId,
+                            "availabilityExpireTime": expire_time,
+                            "rulesetId": "US_47_CFR_PART_15_SUBPART_E"
+                        }
+                    ],
+                    "version": "1.4"
+                }
+                reply = json.dumps(resp)
+                self.conn.sendall(reply.encode())
+                logger.info(f"response Sent: {reply}")
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self.s.close()
+
+def run_afcd_server(freq_info=None, chan_info=None):
+    server = AFCDServer()
+    server.listen_reply(freq_info, chan_info)
+    server.close()
+
+def init_afc_dut_params():
+    params = {"country_code": "CA",
+              "hw_mode": "a",
+              "ssid": "AFC",
+              "ieee80211ax": "1",
+              "wpa": "2",
+              "rsn_pairwise": "CCMP",
+              "wpa_key_mgmt": "SAE",
+              "sae_pwe": "1",
+              "sae_password": "password",
+              "ieee80211w": "2",
+              "afcd_sock": "/tmp/afcd.sock",
+              "afc_request_version": "1.4",
+              "afc_serial_number": "SN000",
+              "afc_cert_ids": "US_47_CFR_PART_15_SUBPART_E:CID000",
+              "afc_location_type": "0",
+              "afc_linear_polygon": "-121.98586998164663:37.38193354300452",
+              "afc_radial_polygon": "118.8:92.76,76.44:87.456,98.56:123.33",
+              "afc_major_axis": "150",
+              "afc_minor_axis": "150",
+              "afc_orientation": "0",
+              "afc_height": "15",
+              "afc_height_type": "AGL",
+              "afc_vertical_tolerance": "2",
+              "afc_freq_range": "5925:6425,6525:6875",
+              "afc_op_class": "131,132,133,134,136,137"
+              }
+    return params
+
+def test_afc_empty(dev, apdev):
+    """AFC server with no usable channel"""
+
+    dev[0].cmd_execute(['iw', 'reg', 'set', 'CA'])
+    wait_regdom_changes(dev[0])
+    try:
+        t = threading.Thread(target=run_afcd_server)
+        t.start()
+
+        hapd = None
+        params = init_afc_dut_params()
+        params["he_6ghz_reg_pwr_type"] = "1"
+        params["channel"] = "5"
+        params["op_class"] = "131"
+
+        if not he_6ghz_supported():
+            raise HwsimSkip("6 GHz frequency is not supported")
+
+        hapd = hostapd.add_ap(apdev[0], params, wait_enabled=False)
+        if hapd.get_status_field("state") != "COUNTRY_UPDATE":
+            raise Exception("Unexpected interface state - expected COUNTRY_UPDATE")
+    except Exception as e:
+        if isinstance(e, Exception) and \
+            str(e) == "Failed to set hostapd parameter afcd_sock":
+            raise HwsimSkip("AFC is not supported")
+        raise
+    finally:
+        t.join()
+        clear_regdom(hapd, dev)
+
+def _test_afc(dev, apdev, freq_info, chan_info, channel, op_class, ccfs1):
+    check_sae_capab(dev[0])
+    dev[0].cmd_execute(['iw', 'reg', 'set', 'CA'])
+    wait_regdom_changes(dev[0])
+
+    try:
+        t = threading.Thread(target=run_afcd_server, args=(freq_info, chan_info))
+        t.start()
+
+        hapd = None
+        params = init_afc_dut_params()
+        params["he_6ghz_reg_pwr_type"] = "1"
+        params["channel"] = str(channel)
+        params["op_class"] = str(op_class)
+
+        if op_class == 137:
+            if ccfs1 > channel:
+                he_ccfs1 = ccfs1 - 16
+            else:
+                he_ccfs1 = ccfs1 + 16
+            params["he_oper_centr_freq_seg0_idx"] = str(he_ccfs1)
+            params["ieee80211be"] = "1"
+            params["eht_oper_centr_freq_seg0_idx"] = str(ccfs1)
+        else:
+            params["he_oper_centr_freq_seg0_idx"] = str(ccfs1)
+
+        if not he_6ghz_supported():
+            raise HwsimSkip("6 GHz frequency is not supported")
+        if op_class == 137 and not eht_320mhz_supported():
+            raise HwsimSkip("320 MHz channels are not supported")
+
+        hapd = hostapd.add_ap(apdev[0], params)
+
+        status = hapd.get_status()
+        logger.info("hostapd STATUS: " + str(status))
+        if hapd.get_status_field("ieee80211ax") != "1":
+            raise Exception("STATUS did not indicate ieee80211ax=1")
+
+        freq = hapd.get_status_field("freq")
+        if int(freq) < 5955:
+            raise Exception("Unexpected frequency: " + freq)
+
+        dev[0].set("sae_pwe", "1")
+        dev[0].set("sae_groups", "")
+
+        dev[0].connect("AFC", key_mgmt="SAE", sae_password="password", ieee80211w="2",
+                       scan_freq=freq)
+        hwsim_utils.test_connectivity(dev[0], hapd)
+
+        sta = hapd.get_sta(dev[0].own_addr())
+        if 'supp_op_classes' not in sta:
+            raise Exception("supp_op_classes not indicated")
+        supp_op_classes = binascii.unhexlify(sta['supp_op_classes'])
+        if op_class not in supp_op_classes:
+            raise Exception("STA did not indicate support for opclass %d" % op_class)
+
+        dev[0].request("DISCONNECT")
+        dev[0].wait_disconnected()
+        hapd.wait_sta_disconnect()
+        hapd.disable()
+
+    except Exception as e:
+        if isinstance(e, Exception) and \
+            str(e) == "Failed to set hostapd parameter afcd_sock":
+            raise HwsimSkip("AFC is not supported")
+        raise
+
+    finally:
+        t.join()
+        dev[0].set("sae_pwe", "0")
+        clear_regdom(hapd, dev)
+
+def afc_gen_freq_info(freq, max_psd):
+    high = freq + 10
+    low = freq - 10
+    ranges = {
+        "frequencyRange": {
+            "highFrequency": high,
+            "lowFrequency": low
+        },
+        "maxPsd": max_psd
+    }
+    return ranges
+
+# Make current channel unusable to force using acs
+def test_afc_freq_20mhz(dev, apdev):
+    """AFC availableFrequencyInfo with 20 MHz channel width usable"""
+    freq_info = [afc_gen_freq_info(5975, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 5, 131, 5)
+
+def test_afc_freq_40mhz(dev, apdev):
+    """AFC availableFrequencyInfo with 40 MHz channel width usable"""
+    freq_info = [afc_gen_freq_info(5955, 10.0), afc_gen_freq_info(5975, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 5, 132, 3)
+
+def test_afc_freq_80mhz(dev, apdev):
+    """AFC availableFrequencyInfo with 80 MHz channel width usable"""
+    freq_info = [afc_gen_freq_info(5955, 10.0), afc_gen_freq_info(5975, 10.0),
+                 afc_gen_freq_info(5995, 10.0), afc_gen_freq_info(6015, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 5, 133, 7)
+
+def test_afc_freq_160mhz(dev, apdev):
+    """AFC availableFrequencyInfo with 160 MHz channel width usable"""
+    freq_info = [afc_gen_freq_info(5955, 10.0), afc_gen_freq_info(5975, 10.0),
+                 afc_gen_freq_info(5995, 10.0), afc_gen_freq_info(6015, 10.0),
+                 afc_gen_freq_info(6035, 10.0), afc_gen_freq_info(6055, 10.0),
+                 afc_gen_freq_info(6075, 10.0), afc_gen_freq_info(6095, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 5, 134, 15)
+
+def test_afc_freq_320mhz_1(dev, apdev):
+    """AFC availableFrequencyInfo with 320 MHz-1 channel width usable"""
+    freq_info = [afc_gen_freq_info(5955, 10.0), afc_gen_freq_info(5975, 10.0),
+                 afc_gen_freq_info(5995, 10.0), afc_gen_freq_info(6015, 10.0),
+                 afc_gen_freq_info(6035, 10.0), afc_gen_freq_info(6055, 10.0),
+                 afc_gen_freq_info(6075, 10.0), afc_gen_freq_info(6095, 10.0),
+                 afc_gen_freq_info(6115, 10.0), afc_gen_freq_info(6135, 10.0),
+                 afc_gen_freq_info(6155, 10.0), afc_gen_freq_info(6175, 10.0),
+                 afc_gen_freq_info(6195, 10.0), afc_gen_freq_info(6215, 10.0),
+                 afc_gen_freq_info(6235, 10.0), afc_gen_freq_info(6255, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 5, 137, 31)
+
+def test_afc_freq_320mhz_2(dev, apdev):
+    """AFC availableFrequencyInfo with 320 MHz-2 channel width usable"""
+    freq_info = [afc_gen_freq_info(6115, 10.0), afc_gen_freq_info(6135, 10.0),
+                 afc_gen_freq_info(6155, 10.0), afc_gen_freq_info(6175, 10.0),
+                 afc_gen_freq_info(6195, 10.0), afc_gen_freq_info(6215, 10.0),
+                 afc_gen_freq_info(6235, 10.0), afc_gen_freq_info(6255, 10.0),
+                 afc_gen_freq_info(6275, 10.0), afc_gen_freq_info(6295, 10.0),
+                 afc_gen_freq_info(6315, 10.0), afc_gen_freq_info(6335, 10.0),
+                 afc_gen_freq_info(6355, 10.0), afc_gen_freq_info(6375, 10.0),
+                 afc_gen_freq_info(6395, 10.0), afc_gen_freq_info(6415, 10.0)]
+    _test_afc(dev, apdev, freq_info, None, 37, 137, 63)
+
+
+def afc_gen_chan_info(chan, op_class, eirp):
+    chan_info = {
+        "channelCfi": chan,
+        "globalOperatingClass": op_class,
+        "maxEirp": [eirp] * len(chan)
+    }
+    return chan_info
+
+def test_afc_chan_20mhz(dev, apdev):
+    """AFC availableChannelInfo with 20 MHz channel width usable"""
+    chan_info = [afc_gen_chan_info([5], 131, 23.0)]
+    _test_afc(dev, apdev, None, chan_info, 5, 131, 5)
+
+def test_afc_chan_40mhz(dev, apdev):
+    """AFC availableChannelInfo with 40 MHz channel width usable"""
+    chan_info = [afc_gen_chan_info([1, 5], 131, 23.0),
+                 afc_gen_chan_info([3], 132, 26.0)]
+    _test_afc(dev, apdev, None, chan_info, 5, 132, 3)
+
+def test_afc_chan_80mhz(dev, apdev):
+    """AFC availableChannelInfo with 80 MHz channel width usable"""
+    chan_info = [afc_gen_chan_info([1, 5, 9, 13], 131, 23.0),
+                 afc_gen_chan_info([3, 11], 132, 26.0),
+                 afc_gen_chan_info([7], 133, 29.0)]
+    _test_afc(dev, apdev, None, chan_info, 5, 133, 7)
+
+def test_afc_chan_160mhz(dev, apdev):
+    """AFC availableChannelInfo with 160 MHz channel width usable"""
+    chan_info = [afc_gen_chan_info([1, 5, 9, 13, 17, 21, 25, 29], 131, 23.0),
+                 afc_gen_chan_info([3, 11, 19, 27], 132, 26.0),
+                 afc_gen_chan_info([7, 23], 133, 29.0),
+                 afc_gen_chan_info([15], 134, 32.0)]
+    _test_afc(dev, apdev, None, chan_info, 5, 134, 15)
+
+def test_afc_chan_320mhz_1(dev, apdev):
+    """AFC availableChannelInfo with 320 MHz-1 channel width usable"""
+    chan_info = [afc_gen_chan_info([1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53, 57, 61], 131, 23.0),
+                 afc_gen_chan_info([3, 11, 19, 27, 35, 43, 51, 59], 132, 26.0),
+                 afc_gen_chan_info([7, 23, 39, 55], 133, 29.0),
+                 afc_gen_chan_info([15, 47], 134, 32.0),
+                 afc_gen_chan_info([31], 137, 35.0)]
+    _test_afc(dev, apdev, None, chan_info, 5, 137, 31)
+
+def test_afc_chan_320mhz_2(dev, apdev):
+    """AFC availableChannelInfo with 320 MHz-2 channel width usable"""
+    chan_info = [afc_gen_chan_info([33, 37, 41, 45, 49, 53, 57, 61, 65, 69, 73, 77, 81, 85, 89, 93], 131, 23.0),
+                 afc_gen_chan_info([35, 43, 51, 59, 67, 75, 83, 91], 132, 26.0),
+                 afc_gen_chan_info([39, 55, 71, 87], 133, 29.0),
+                 afc_gen_chan_info([47, 79], 134, 32.0),
+                 afc_gen_chan_info([63], 137, 35.0)]
+    _test_afc(dev, apdev, None, chan_info, 37, 137, 63)
-- 
2.45.2



From allen.ye at mediatek.com  Mon Aug  3 23:34:35 2026
From: allen.ye at mediatek.com (Allen Ye)
Date: Tue, 4 Aug 2026 14:34:35 +0800
Subject: [PATCH v13 3/5] AFC: Add AFC client support
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <20260804063437.646306-4-allen.ye@mediatek.com>

From: Lorenzo Bianconi <lorenzo at kernel.org>

Introduce Automated Frequency Coordination (AFC) support for UNII-5 and
UNII-7 6GHz bands.
AFC client will connect to AFCD providing AP related parameters for AFC
coordinator (e.g. geolocation, supported frequencies, etc.).
AFC is required for Standard Power Devices (SPDs) to determine a lists
of channels and EIRP/PSD powers that are available in the 6GHz spectrum.
AFC hostapd client is tested with AFC DUT Test Harness [0].

[0] https://github.com/Wi-FiTestSuite/AFC-DUT/tree/main

Tested-by: Felix Fietkau <nbd at nbd.name>
Tested-by: Allen Ye <allen.ye at mediatek.com>
Tested-by: Krishna Chaitanya <chaitanya.mgit at gmail.com>
Signed-off-by: Lorenzo Bianconi <lorenzo at kernel.org>
Signed-off-by: Allen Ye <allen.ye at mediatek.com>
---
 hostapd/Makefile      |    8 +
 hostapd/config_file.c |  250 ++++++++++
 hostapd/ctrl_iface.c  |   47 ++
 hostapd/defconfig     |    3 +
 hostapd/hostapd.conf  |   41 ++
 hostapd/hostapd_cli.c |   29 ++
 src/ap/afc.c          | 1086 +++++++++++++++++++++++++++++++++++++++++
 src/ap/ap_config.c    |   15 +
 src/ap/ap_config.h    |   46 ++
 src/ap/hostapd.c      |   24 +
 src/ap/hostapd.h      |   52 ++
 src/ap/hw_features.c  |    2 +
 src/common/wpa_ctrl.h |    3 +
 13 files changed, 1606 insertions(+)
 create mode 100644 src/ap/afc.c

diff --git a/hostapd/Makefile b/hostapd/Makefile
index fd6ef1464..e4d765493 100644
--- a/hostapd/Makefile
+++ b/hostapd/Makefile
@@ -104,6 +104,14 @@ CFLAGS += -DCONFIG_TAXONOMY
 OBJS += ../src/ap/taxonomy.o
 endif
 
+ifdef CONFIG_IEEE80211AX
+ifdef CONFIG_AFC
+CFLAGS += -DCONFIG_AFC
+OBJS += ../src/ap/afc.o
+NEED_JSON=y
+endif
+endif
+
 ifdef CONFIG_MODULE_TESTS
 CFLAGS += -DCONFIG_MODULE_TESTS
 OBJS += hapd_module_tests.o
diff --git a/hostapd/config_file.c b/hostapd/config_file.c
index 4bf8eaed5..8d8b895f7 100644
--- a/hostapd/config_file.c
+++ b/hostapd/config_file.c
@@ -1281,6 +1281,198 @@ static int hostapd_parse_he_srg_bitmap(u8 *bitmap, char *val)
 	return 0;
 }
 
+
+#ifdef CONFIG_AFC
+static int hostapd_afc_parse_cert_ids(struct hostapd_config *conf, char *pos)
+{
+	struct cert_id *c = NULL, *tmp;
+	unsigned int i, count = 0;
+
+	while (pos && pos[0]) {
+		char *p;
+
+		tmp = os_realloc_array(c, count + 1, sizeof(*c));
+		if (!tmp)
+			goto error;
+		c = tmp;
+
+		i = count;
+		count++;
+		os_memset(&c[i], 0, sizeof(struct cert_id));
+
+		p = os_strchr(pos, ':');
+		if (!p)
+			goto error;
+
+		*p++ = '\0';
+		if (!p || !p[0])
+			goto error;
+
+		c[i].rulset = os_strdup(pos);
+		if (!c[i].rulset)
+			goto error;
+
+		pos = p;
+		p = os_strchr(pos, ',');
+		if (p)
+			*p++ = '\0';
+
+		c[i].id = os_strdup(pos);
+		if (!c[i].id)
+			goto error;
+
+		pos = p;
+	}
+
+	for (i = 0; i < conf->afc.n_cert_ids; i++) {
+		os_free(conf->afc.cert_ids[i].rulset);
+		os_free(conf->afc.cert_ids[i].id);
+	}
+	os_free(conf->afc.cert_ids);
+
+	conf->afc.n_cert_ids = count;
+	conf->afc.cert_ids = c;
+
+	return 0;
+
+error:
+	for (i = 0; i < count; i++) {
+		os_free(c[i].rulset);
+		os_free(c[i].id);
+	}
+	os_free(c);
+
+	return -ENOMEM;
+}
+
+
+static int hostapd_afc_parse_position_data(struct afc_linear_polygon **data,
+					   unsigned int *n_linear_polygon_data,
+					   char *pos)
+{
+	struct afc_linear_polygon *d = NULL, *tmp;
+	int count = 0;
+
+	while (pos && pos[0]) {
+		char *p, *end;
+
+		tmp = os_realloc_array(d, count + 1, sizeof(*d));
+		if (!tmp)
+			goto error;
+		d = tmp;
+
+		p = os_strchr(pos, ':');
+		if (!p)
+			goto error;
+
+		*p++ = '\0';
+		if (!p || !p[0])
+			goto error;
+
+		d[count].longitude = strtod(pos, &end);
+		if (*end)
+			goto error;
+
+		pos = p;
+		p = os_strchr(pos, ',');
+		if (p)
+			*p++ = '\0';
+
+		d[count].latitude = strtod(pos, &end);
+		if (*end)
+			goto error;
+
+		pos = p;
+		count++;
+	}
+
+	os_free(*data);
+	*n_linear_polygon_data = count;
+	*data = d;
+
+	return 0;
+
+error:
+	os_free(d);
+	return -ENOMEM;
+}
+
+
+static int hostapd_afc_parse_freq_range(struct hostapd_config *conf, char *pos)
+{
+	struct afc_freq_range *f = NULL, *tmp;
+	int count = 0;
+
+	while (pos && pos[0]) {
+		char *p;
+
+		tmp = os_realloc_array(f, count + 1, sizeof(*f));
+		if (!tmp)
+			goto error;
+		f = tmp;
+
+		p = os_strchr(pos, ':');
+		if (!p)
+			goto error;
+
+		*p++ = '\0';
+		if (!p || !p[0])
+			goto error;
+
+		f[count].low_freq = atoi(pos);
+		pos = p;
+		p = os_strchr(pos, ',');
+		if (p)
+			*p++ = '\0';
+
+		f[count].high_freq = atoi(pos);
+		pos = p;
+		count++;
+	}
+
+	os_free(conf->afc.freq_range);
+	conf->afc.n_freq_range = count;
+	conf->afc.freq_range = f;
+
+	return 0;
+
+error:
+	os_free(f);
+	return -ENOMEM;
+}
+
+
+static int hostapd_afc_parse_op_class(struct hostapd_config *conf, char *pos)
+{
+	unsigned int *oc = NULL, *tmp;
+	int count = 0;
+
+	while (pos && pos[0]) {
+		char *p;
+
+		tmp = os_realloc_array(oc, count + 1, sizeof(*oc));
+		if (!tmp) {
+			os_free(oc);
+			return -ENOMEM;
+		}
+		oc = tmp;
+
+		p = os_strchr(pos, ',');
+		if (p)
+			*p++ = '\0';
+
+		oc[count] = atoi(pos);
+		pos = p;
+		count++;
+	}
+
+	os_free(conf->afc.op_class);
+	conf->afc.n_op_class = count;
+	conf->afc.op_class = oc;
+
+	return 0;
+}
+#endif /* CONFIG_AFC */
 #endif /* CONFIG_IEEE80211AX */
 
 
@@ -3740,6 +3932,64 @@ static int hostapd_config_fill(struct hostapd_config *conf,
 			return 1;
 		}
 		bss->unsol_bcast_probe_resp_interval = val;
+#ifdef CONFIG_AFC
+	} else if (os_strcmp(buf, "afcd_sock") == 0) {
+		os_free(conf->afc.socket);
+		conf->afc.socket = os_strdup(pos);
+	} else if (os_strcmp(buf, "afc_request_version") == 0) {
+		os_free(conf->afc.request.version);
+		conf->afc.request.version = os_strdup((pos));
+	} else if (os_strcmp(buf, "afc_serial_number") == 0) {
+		os_free(conf->afc.request.sn);
+		conf->afc.request.sn = os_strdup((pos));
+	} else if (os_strcmp(buf, "afc_cert_ids") == 0) {
+		if (hostapd_afc_parse_cert_ids(conf, pos))
+			return 1;
+	} else if (os_strcmp(buf, "afc_location_type") == 0) {
+		conf->afc.location.type = atoi(pos);
+		if (conf->afc.location.type != ELLIPSE &&
+		    conf->afc.location.type != LINEAR_POLYGON &&
+		    conf->afc.location.type != RADIAL_POLYGON)
+			return 1;
+	} else if (os_strcmp(buf, "afc_linear_polygon") == 0) {
+		if (hostapd_afc_parse_position_data(
+			&conf->afc.location.linear_polygon_data,
+			&conf->afc.location.n_linear_polygon_data,
+			pos))
+			return 1;
+	} else if (os_strcmp(buf, "afc_radial_polygon") == 0) {
+		if (hostapd_afc_parse_position_data(
+			(struct afc_linear_polygon **)
+			&conf->afc.location.radial_polygon_data,
+			&conf->afc.location.n_radial_polygon_data,
+			pos))
+			return 1;
+	} else if (os_strcmp(buf, "afc_major_axis") == 0) {
+		conf->afc.location.major_axis = atoi(pos);
+	} else if (os_strcmp(buf, "afc_minor_axis") == 0) {
+		conf->afc.location.minor_axis = atoi(pos);
+	} else if (os_strcmp(buf, "afc_orientation") == 0) {
+		conf->afc.location.orientation = atoi(pos);
+	} else if (os_strcmp(buf, "afc_height") == 0) {
+		char *end;
+
+		conf->afc.location.height = strtod(pos, &end);
+		if (*end)
+			return 1;
+	} else if (os_strcmp(buf, "afc_height_type") == 0) {
+		os_free(conf->afc.location.height_type);
+		conf->afc.location.height_type = os_strdup(pos);
+	} else if (os_strcmp(buf, "afc_vertical_tolerance") == 0) {
+		conf->afc.location.vertical_tolerance = atoi(pos);
+	} else if (os_strcmp(buf, "afc_min_power") == 0) {
+		conf->afc.min_power = atoi(pos);
+	} else if (os_strcmp(buf, "afc_freq_range") == 0) {
+		if (hostapd_afc_parse_freq_range(conf, pos))
+			return 1;
+	} else if (os_strcmp(buf, "afc_op_class") == 0) {
+		if (hostapd_afc_parse_op_class(conf, pos))
+			return 1;
+#endif /* CONFIG_AFC */
 	} else if (os_strcmp(buf, "mbssid") == 0) {
 		int mbssid = atoi(pos);
 		if (mbssid < 0 || mbssid > ENHANCED_MBSSID_ENABLED) {
diff --git a/hostapd/ctrl_iface.c b/hostapd/ctrl_iface.c
index 876b484ef..94e76d631 100644
--- a/hostapd/ctrl_iface.c
+++ b/hostapd/ctrl_iface.c
@@ -4174,6 +4174,40 @@ static int hostapd_ctrl_iface_proc_coord_test(struct hostapd_data *hapd,
 #endif /* CONFIG_TESTING_OPTIONS */
 
 
+#ifdef CONFIG_AFC
+static int
+hostapd_cli_cmd_afc_get_request(struct hostapd_data *hapd, char *cmd,
+				char *buf, size_t buflen)
+{
+	if (!hapd->iface->afc_request)
+		return os_snprintf(buf, buflen, "%s\n",
+				   "request data is NULL!");
+
+	return os_snprintf(buf, buflen, "%s\n",
+			   (const char *) wpabuf_head(hapd->iface->afc_request));
+}
+
+static int
+hostapd_cli_cmd_afc_get_response(struct hostapd_data *hapd, char *cmd,
+				 char *buf, size_t buflen)
+{
+	if (!hapd->iface->afc_response)
+		return os_snprintf(buf, buflen, "%s\n",
+				   "response data is NULL!");
+
+	return os_snprintf(buf, buflen, "%s\n", hapd->iface->afc_response);
+}
+
+static int
+hostapd_cli_cmd_afc_send_request(struct hostapd_data *hapd, char *cmd,
+				 char *buf, size_t buflen)
+{
+	hostapd_afc_send_request(hapd->iface);
+	return os_snprintf(buf, buflen, "OK\n");
+}
+#endif /* CONFIG_AFC */
+
+
 static int hostapd_ctrl_iface_receive_process(struct hostapd_data *hapd,
 					      char *buf, char *reply,
 					      int reply_size,
@@ -4804,6 +4838,19 @@ static int hostapd_ctrl_iface_receive_process(struct hostapd_data *hapd,
 		if (hostapd_ctrl_send_unsolicited_dscp_req(hapd, buf + 26))
 			reply_len = -1;
 #endif /* CONFIG_ROBUST_AV */
+#ifdef CONFIG_AFC
+	}  else if (os_strncmp(buf, "AFC_GET_REQUEST", 15) == 0) {
+		reply_len = hostapd_cli_cmd_afc_get_request(hapd, buf + 15,
+							    reply, reply_size);
+	}  else if (os_strncmp(buf, "AFC_GET_RESPONSE", 16) == 0) {
+		reply_len = hostapd_cli_cmd_afc_get_response(hapd, buf + 16,
+							     reply,
+							     reply_size);
+	}  else if (os_strncmp(buf, "AFC_SEND_REQUEST", 16) == 0) {
+		reply_len = hostapd_cli_cmd_afc_send_request(hapd, buf + 16,
+							     reply,
+							     reply_size);
+#endif /* CONFIG_AFC */
 	} else {
 		os_memcpy(reply, "UNKNOWN COMMAND\n", 16);
 		reply_len = 16;
diff --git a/hostapd/defconfig b/hostapd/defconfig
index 0c6077a7b..a1b208ac4 100644
--- a/hostapd/defconfig
+++ b/hostapd/defconfig
@@ -434,3 +434,6 @@ CONFIG_DPP2=y
 # used as part of Robust AV Streaming. At present, this enables DSCP Policy
 # capability handling in hostapd.
 #CONFIG_ROBUST_AV=y
+
+# Enable Automated Frequency Coordination for 6GHz outdoor
+#CONFIG_AFC=y
diff --git a/hostapd/hostapd.conf b/hostapd/hostapd.conf
index 0a19a94b9..4eafebdc4 100644
--- a/hostapd/hostapd.conf
+++ b/hostapd/hostapd.conf
@@ -1050,6 +1050,47 @@ wmm_ac_vo_acm=0
 # 1 = Supports Peer-to-peer TWT
 #peer_to_peer_twt=0
 
+##### Automated Frequency Coordination for 6GHz UNII-5 and UNII-7 bands #######
+
+# AFC daemon connection socket
+#afcd_sock=/var/run/afcd.sock
+
+# AFC request identification parameters
+#afc_request_version=1.1
+#afc_serial_number=abcdefg
+#afc_cert_ids=US_47_CFR_PART_15_SUBPART_E:CID000
+#
+# AFC location type:
+# 0 = ellipse
+# 1 = linear polygon
+# 2 = radial polygon
+#afc_location_type=0
+#
+# AFC ellipse or linear polygon coordinations
+#afc_linear_polygon=-122.984157:37.425056
+#
+# AFC radial polygon coordinations
+#afc_radial_polygon=118.8:92.76,76.44:87.456,98.56:123.33
+#
+# AFC ellipse major/minor axis and orientation
+#afc_major_axis=100
+#afc_minor_axis=50
+#afc_orientation=70
+#
+# AFC device elevation parameters
+#afc_height=3.0
+#afc_height_type=AGL
+#afc_vertical_tolerance=7
+#
+# AFC minimum desired TX power (dbm)
+#afc_min_power=24
+#
+# AFC request frequency ranges
+#afc_freq_range=5925:6425,6525:6875
+#
+# AFC request operation classes
+#afc_op_class=131,132,133,134,136,137
+
 ##### IEEE 802.11be related configuration #####################################
 
 #ieee80211be: Whether IEEE 802.11be (EHT) is enabled
diff --git a/hostapd/hostapd_cli.c b/hostapd/hostapd_cli.c
index 57ab1e152..5758f924c 100644
--- a/hostapd/hostapd_cli.c
+++ b/hostapd/hostapd_cli.c
@@ -1765,6 +1765,27 @@ static int hostapd_cli_cmd_driver(struct wpa_ctrl *ctrl, int argc, char *argv[])
 #endif /* ANDROID */
 
 
+#ifdef CONFIG_AFC
+static int hostapd_cli_cmd_afc_get_request(struct wpa_ctrl *ctrl, int argc,
+					   char *argv[])
+{
+	return hostapd_cli_cmd(ctrl, "AFC_GET_REQUEST", 0, 0, NULL);
+}
+
+static int hostapd_cli_cmd_afc_get_response(struct wpa_ctrl *ctrl, int argc,
+					    char *argv[])
+{
+	return hostapd_cli_cmd(ctrl, "AFC_GET_RESPONSE", 0, 0, NULL);
+}
+
+static int hostapd_cli_cmd_afc_send_request(struct wpa_ctrl *ctrl, int argc,
+					    char *argv[])
+{
+	return hostapd_cli_cmd(ctrl, "AFC_SEND_REQUEST", 0, 0, NULL);
+}
+#endif /* CONFIG_AFC */
+
+
 struct hostapd_cli_cmd {
 	const char *cmd;
 	int (*handler)(struct wpa_ctrl *ctrl, int argc, char *argv[]);
@@ -2013,6 +2034,14 @@ static const struct hostapd_cli_cmd hostapd_cli_commands[] = {
 	 NULL,
 	 "<addr>, [reset=], [policy_id_list=], Send unsolicited DSCP request"},
 #endif /* CONFIG_ROBUST_AV */
+#ifdef CONFIG_AFC
+	{ "afc_get_request", hostapd_cli_cmd_afc_get_request, NULL,
+	  " = Show latest AFC request data"},
+	{ "afc_get_response", hostapd_cli_cmd_afc_get_response, NULL,
+	  " = Show latest AFC response data"},
+	{ "afc_send_request", hostapd_cli_cmd_afc_send_request, NULL,
+	  " = Send another AFC request and upadte the timeout"},
+#endif /* CONFIG_AFC */
 	{ NULL, NULL, NULL, NULL }
 };
 
diff --git a/src/ap/afc.c b/src/ap/afc.c
new file mode 100644
index 000000000..bcdd487aa
--- /dev/null
+++ b/src/ap/afc.c
@@ -0,0 +1,1086 @@
+/*
+ * Automated Frequency Coordination
+ * Copyright (c) 2024, Lorenzo Bianconi <lorenzo at kernel.org>
+ *
+ * This software may be distributed under the terms of the BSD license.
+ * See README for more details.
+ */
+
+#include "utils/includes.h"
+#include <sys/un.h>
+#include <time.h>
+
+#include "utils/common.h"
+#include "utils/eloop.h"
+#include "utils/json.h"
+#include "common/hw_features_common.h"
+#include "common/wpa_ctrl.h"
+#include "hostapd.h"
+#include "acs.h"
+#include "hw_features.h"
+
+#define HOSTAPD_AFC_RETRY_TIMEOUT	180
+#define HOSTAPD_AFC_TIMEOUT		86400 /* 24h */
+#define HOSTAPD_AFC_BUFSIZE		8192
+
+static void hostapd_afc_timeout_handler(void *eloop_ctx, void *timeout_ctx);
+static int hostapd_afc_reset_channels(struct hostapd_iface *iface);
+
+static struct wpabuf *
+hostapd_afc_build_location_request(struct hostapd_iface *iface)
+{
+	struct wpabuf *location_obj, *ellipse_obj = NULL;
+	struct hostapd_config *iconf = iface->conf;
+	bool is_ap_indoor = he_reg_is_indoor(iconf->he_6ghz_reg_pwr_type);
+	size_t location_len = 1024, ellipse_len = 128;
+	unsigned int i;
+
+	location_obj = wpabuf_alloc(location_len);
+	if (!location_obj)
+		return NULL;
+
+	json_start_object(location_obj, "location");
+	if (iconf->afc.location.type != LINEAR_POLYGON) {
+		struct afc_linear_polygon *lp =
+			iconf->afc.location.linear_polygon_data;
+
+		if (!lp)
+			goto error;
+
+		ellipse_obj = wpabuf_alloc(ellipse_len);
+		if (!ellipse_obj)
+			goto error;
+
+		json_start_object(ellipse_obj, "center");
+
+		json_add_double(ellipse_obj, "longitude", lp->longitude);
+		json_value_sep(ellipse_obj);
+
+		json_add_double(ellipse_obj, "latitude", lp->latitude);
+		json_end_object(ellipse_obj);
+		json_value_sep(ellipse_obj);
+	}
+
+	switch (iconf->afc.location.type) {
+	case LINEAR_POLYGON: {
+		json_start_object(location_obj, "linearPolygon");
+		json_start_array(location_obj, "outerBoundary");
+		for (i = 0;
+		     i < iconf->afc.location.n_linear_polygon_data; i++) {
+			struct afc_linear_polygon *lp =
+				&iconf->afc.location.linear_polygon_data[i];
+
+			json_start_object(location_obj, NULL);
+
+			json_add_double(location_obj, "longitude",
+					lp->longitude);
+			json_value_sep(location_obj);
+
+			json_add_double(location_obj, "latitude",
+					lp->latitude);
+
+			json_end_object(location_obj);
+			if (i < iconf->afc.location.n_linear_polygon_data - 1)
+				json_value_sep(location_obj);
+		}
+		json_end_array(location_obj);
+
+		/* linearPolygon */
+		json_end_object(location_obj);
+		json_value_sep(location_obj);
+		break;
+	}
+	case RADIAL_POLYGON: {
+		json_start_object(location_obj, "radialPolygon");
+		wpabuf_put_buf(location_obj, ellipse_obj);
+
+		json_start_array(location_obj, "outerBoundary");
+		for (i = 0;
+		     i < iconf->afc.location.n_radial_polygon_data; i++) {
+			struct afc_radial_polygon *rp =
+				&iconf->afc.location.radial_polygon_data[i];
+
+			json_start_object(location_obj, NULL);
+			json_add_double(location_obj, "length", rp->length);
+			json_value_sep(location_obj);
+
+			json_add_double(location_obj, "angle", rp->angle);
+			json_end_object(location_obj);
+			if (i < iconf->afc.location.n_radial_polygon_data - 1)
+				json_value_sep(location_obj);
+		}
+		json_end_array(location_obj);
+
+		/* radialPolygon */
+		json_end_object(location_obj);
+		json_value_sep(location_obj);
+		break;
+	}
+	case ELLIPSE:
+	default:
+		json_start_object(location_obj, "ellipse");
+		wpabuf_put_buf(location_obj, ellipse_obj);
+
+		json_add_int(location_obj, "majorAxis",
+			     iconf->afc.location.major_axis);
+		json_value_sep(location_obj);
+		json_add_int(location_obj, "minorAxis",
+			     iconf->afc.location.minor_axis);
+		json_value_sep(location_obj);
+		json_add_int(location_obj, "orientation",
+			     iconf->afc.location.orientation);
+		/* ellipse */
+		json_end_object(location_obj);
+		json_value_sep(location_obj);
+		break;
+	}
+
+	json_start_object(location_obj, "elevation");
+
+	json_add_double(location_obj, "height", iconf->afc.location.height);
+	json_value_sep(location_obj);
+	if (iconf->afc.location.height_type) {
+		json_add_string(location_obj, "heightType",
+				iconf->afc.location.height_type);
+		json_value_sep(location_obj);
+	}
+	json_add_int(location_obj, "verticalUncertainty",
+		     iconf->afc.location.vertical_tolerance);
+
+	/* elevation */
+	json_end_object(location_obj);
+	json_value_sep(location_obj);
+
+	json_add_int(location_obj, "indoorDeployment", is_ap_indoor);
+
+	/* location */
+	json_end_object(location_obj);
+	json_value_sep(location_obj);
+
+	wpabuf_free(ellipse_obj);
+	return location_obj;
+
+error:
+	wpabuf_free(location_obj);
+	wpabuf_free(ellipse_obj);
+
+	return NULL;
+}
+
+
+static struct wpabuf * hostapd_afc_get_opclass_chan_list(u8 op_class)
+{
+	struct wpabuf *chan_list_obj;
+	const struct oper_class_map *oper_class;
+	int chan_offset, chan;
+	size_t ch_len = 512;
+
+	oper_class = get_oper_class(NULL, op_class);
+	if (!oper_class)
+		return NULL;
+
+	chan_list_obj = wpabuf_alloc(ch_len);
+	if (!chan_list_obj)
+		return NULL;
+	json_start_array(chan_list_obj, "channelCfi");
+
+	switch (op_class) {
+	case 132: /*  40MHz */
+		chan_offset = 2;
+		break;
+	case 133: /*  80MHz */
+		chan_offset = 6;
+		break;
+	case 134: /* 160MHz */
+		chan_offset = 14;
+		break;
+	case 137: /* 320MHz */
+		/*
+		 * global_op_class already use the central channels for
+		 * 320 MHz, so fallthrough and use 0 for chan_offset.
+		 */
+	default:
+		chan_offset = 0;
+		break;
+	}
+
+	for (chan = oper_class->min_chan; chan <= oper_class->max_chan;
+	     chan += oper_class->inc) {
+		char ch_str[32] = {0};
+
+		if (chan + chan_offset > oper_class->max_chan)
+			break;
+
+		snprintf(ch_str, sizeof(ch_str), "%s%d",
+			 chan != oper_class->min_chan ? ", ":"",
+			 chan + chan_offset);
+		wpabuf_put_str(chan_list_obj, ch_str);
+	}
+	json_end_array(chan_list_obj);
+
+	return chan_list_obj;
+}
+
+
+static struct wpabuf *
+hostapd_afc_build_req_chan_list(struct hostapd_iface *iface)
+{
+	struct wpabuf *op_class_list_obj;
+	struct hostapd_config *iconf = iface->conf;
+	unsigned int i;
+
+	op_class_list_obj = wpabuf_alloc(HOSTAPD_AFC_BUFSIZE);
+	if (!op_class_list_obj)
+		return NULL;
+
+	json_start_array(op_class_list_obj, "inquiredChannels");
+	for (i = 0; i < iconf->afc.n_op_class; i++) {
+		struct wpabuf *chan_list_obj = NULL;
+		u8 op_class = iconf->afc.op_class[i];
+
+		if (!is_6ghz_op_class(op_class))
+			continue;
+
+		json_start_object(op_class_list_obj, NULL);
+
+		json_add_int(op_class_list_obj, "globalOperatingClass",
+			     op_class);
+		json_value_sep(op_class_list_obj);
+
+		chan_list_obj = hostapd_afc_get_opclass_chan_list(op_class);
+		if (!chan_list_obj)
+			goto error;
+
+		wpabuf_put_buf(op_class_list_obj, chan_list_obj);
+		json_end_object(op_class_list_obj);
+
+		if (i < iconf->afc.n_op_class - 1)
+			json_value_sep(op_class_list_obj);
+
+		wpabuf_free(chan_list_obj);
+		chan_list_obj = NULL;
+	}
+	json_end_array(op_class_list_obj);
+
+	return op_class_list_obj;
+
+error:
+	wpabuf_free(op_class_list_obj);
+	return NULL;
+}
+
+
+static struct wpabuf *
+hostapd_afc_build_request(struct hostapd_iface *iface)
+{
+	struct wpabuf *req_obj, *location_obj, *op_class_list_obj = NULL;
+	struct hostapd_config *iconf = iface->conf;
+	unsigned int i;
+	char request_id_str[16];
+
+	req_obj = wpabuf_alloc(HOSTAPD_AFC_BUFSIZE);
+	if (!req_obj)
+		return NULL;
+
+	json_start_object(req_obj, NULL);
+
+	if (iconf->afc.request.version) {
+		json_add_string(req_obj, "version",
+				iconf->afc.request.version);
+		json_value_sep(req_obj);
+	}
+
+	json_start_array(req_obj, "availableSpectrumInquiryRequests");
+	json_start_object(req_obj, NULL);
+
+	iface->afc.request_id++;
+	snprintf(request_id_str, sizeof(request_id_str), "%u",
+		 iface->afc.request_id);
+	json_add_string(req_obj, "requestId", request_id_str);
+	json_value_sep(req_obj);
+
+	json_start_object(req_obj, "deviceDescriptor");
+	if (iconf->afc.request.sn) {
+		json_add_string(req_obj, "serialNumber",
+				iconf->afc.request.sn);
+		json_value_sep(req_obj);
+	}
+
+	json_start_array(req_obj, "certificationId");
+
+	for (i = 0; i < iconf->afc.n_cert_ids; i++) {
+		json_start_object(req_obj, NULL);
+		json_add_string(req_obj, "rulesetId",
+				iconf->afc.cert_ids[i].rulset);
+		json_value_sep(req_obj);
+
+		json_add_string(req_obj, "id", iconf->afc.cert_ids[i].id);
+		json_end_object(req_obj);
+
+		if (i < iconf->afc.n_cert_ids - 1)
+			json_value_sep(req_obj);
+	}
+	/* certificationId */
+	json_end_array(req_obj);
+
+	/* deviceDescriptor */
+	json_end_object(req_obj);
+	json_value_sep(req_obj);
+
+	location_obj = hostapd_afc_build_location_request(iface);
+	if (!location_obj)
+		goto error;
+
+	wpabuf_put_buf(req_obj, location_obj);
+
+	if (iconf->afc.n_freq_range) {
+		json_start_array(req_obj, "inquiredFrequencyRange");
+		for (i = 0; i < iconf->afc.n_freq_range; i++) {
+			struct afc_freq_range *fr = &iconf->afc.freq_range[i];
+
+			json_start_object(req_obj, NULL);
+
+			json_add_int(req_obj, "lowFrequency", fr->low_freq);
+			json_value_sep(req_obj);
+			json_add_int(req_obj, "highFrequency", fr->high_freq);
+
+			json_end_object(req_obj);
+			if (i < iconf->afc.n_freq_range - 1)
+				json_value_sep(req_obj);
+		}
+		json_end_array(req_obj);
+		json_value_sep(req_obj);
+	}
+
+	op_class_list_obj = hostapd_afc_build_req_chan_list(iface);
+	if (!op_class_list_obj)
+		goto error;
+
+	wpabuf_put_buf(req_obj, op_class_list_obj);
+
+	if (iconf->afc.min_power) {
+		json_value_sep(req_obj);
+		json_add_int(req_obj, "minDesiredPower", iconf->afc.min_power);
+	}
+
+	/* availableSpectrumInquiryRequests */
+	json_end_object(req_obj);
+	json_end_array(req_obj);
+
+	json_end_object(req_obj);
+	wpa_printf(MSG_DEBUG, "Pending AFC request: %s",
+		   (const char *) wpabuf_head(req_obj));
+
+	wpabuf_free(location_obj);
+	wpabuf_free(op_class_list_obj);
+
+	return req_obj;
+
+error:
+	wpabuf_free(req_obj);
+	wpabuf_free(location_obj);
+	wpabuf_free(op_class_list_obj);
+
+	return NULL;
+}
+
+
+static int
+hostad_afc_parse_available_freq_info(struct hostapd_iface *iface,
+				     struct json_token *freq_info)
+{
+	struct afc_freq_range_elem *f = NULL, *tmp;
+	struct json_token *freq_obj;
+	int count = 0;
+
+	for (freq_obj = freq_info->child; freq_obj;
+	     freq_obj = freq_obj->sibling) {
+		struct json_token *freq_range_obj, *token;
+		int low_freq, high_freq;
+		double max_psd;
+
+		freq_range_obj = json_get_member(freq_obj, "frequencyRange");
+		if (!freq_range_obj || freq_range_obj->type != JSON_OBJECT)
+			continue;
+
+		token = json_get_member(freq_range_obj, "lowFrequency");
+		if (!token || token->type != JSON_NUMBER)
+			continue;
+		low_freq = token->number;
+
+		token = json_get_member(freq_range_obj, "highFrequency");
+		if (!token || token->type != JSON_NUMBER)
+			continue;
+		high_freq = token->number;
+
+		token = json_get_member(freq_obj, "maxPsd");
+		if (!token)
+			token = json_get_member(freq_obj, "maxPSD");
+		if (!token || (token->type != JSON_DOUBLE &&
+			       token->type != JSON_NUMBER))
+			continue;
+		max_psd = token->type == JSON_DOUBLE ?
+			token->dnumber : (double) token->number;
+
+		tmp = os_realloc_array(f, count + 1, sizeof(*f));
+		if (!tmp) {
+			os_free(f);
+			return -ENOMEM;
+		}
+		f = tmp;
+
+		f[count].low_freq = low_freq;
+		f[count].high_freq = high_freq;
+		f[count++].max_psd = max_psd;
+	}
+	os_free(iface->afc.freq_range);
+	iface->afc.freq_range = f;
+	iface->afc.num_freq_range = count;
+
+	return 0;
+}
+
+
+static int hostad_afc_update_chan_info(struct afc_chan_info_elem **chan_list,
+				       int *chan_list_size, u8 op_class,
+				       int center_chan, double power)
+{
+	int op_class_pwr_index, num_low_subchan, channel;
+	int count = *chan_list_size;
+	struct afc_chan_info_elem *c = *chan_list, *tmp;
+
+	switch (op_class) {
+	case 132: /*  40MHz */
+		op_class_pwr_index = 1;
+		num_low_subchan = 2;
+		break;
+	case 133: /*  80MHz */
+		op_class_pwr_index = 2;
+		num_low_subchan = 6;
+		break;
+	case 134: /* 160MHz */
+		op_class_pwr_index = 3;
+		num_low_subchan = 14;
+		break;
+	case 137: /* 320MHz */
+		op_class_pwr_index = 4;
+		num_low_subchan = 30;
+		break;
+	default:
+		op_class_pwr_index = 0;
+		num_low_subchan = 0;
+		break;
+	}
+
+	for (channel = center_chan - num_low_subchan;
+	     channel <= center_chan + num_low_subchan; channel += 4) {
+		int i;
+
+		for (i = 0; i < count; i++) {
+			if (c[i].chan == channel)
+				break;
+		}
+
+		if (i == count) {
+			tmp = os_realloc_array(c, count + 1, sizeof(*c));
+			if (!tmp) {
+				os_free(c);
+				*chan_list = NULL;
+				*chan_list_size = 0;
+				return -ENOMEM;
+			}
+			c = tmp;
+
+			c[count].chan = channel;
+			memset(c[count].power, 0, sizeof(c[count].power));
+			count++;
+		}
+		c[i].power[op_class_pwr_index] = power;
+	}
+
+	*chan_list_size = count;
+	*chan_list = c;
+
+	return 0;
+}
+
+
+static int
+hostad_afc_parse_available_chan_info(struct hostapd_iface *iface,
+				     struct json_token *chan_info)
+{
+	struct afc_chan_info_elem *c = NULL;
+	struct json_token *chan_obj;
+	int count = 0;
+
+	for (chan_obj = chan_info->child; chan_obj;
+	     chan_obj = chan_obj->sibling) {
+		struct json_token *chan_cfi, *max_eirp;
+		struct json_token *token, *chan_arr, *power_arr;
+		int op_class;
+
+		token = json_get_member(chan_obj, "globalOperatingClass");
+		if (!token || token->type != JSON_NUMBER)
+			continue;
+		op_class = token->number;
+
+		chan_cfi = json_get_member(chan_obj, "channelCfi");
+		if (!chan_cfi || chan_cfi->type != JSON_ARRAY)
+			continue;
+
+		max_eirp = json_get_member(chan_obj, "maxEirp");
+		if (!max_eirp || max_eirp->type != JSON_ARRAY)
+			continue;
+
+		chan_arr = chan_cfi->child;
+		power_arr = max_eirp->child;
+		while (chan_arr && power_arr) {
+			int channel, ret;
+			double power;
+
+			if (chan_arr->type != JSON_NUMBER ||
+			    (power_arr->type != JSON_DOUBLE &&
+			     power_arr->type != JSON_NUMBER)) {
+				wpa_printf(MSG_DEBUG,
+					   "Failed to parse array at opclass %d",
+					   op_class);
+				break;
+			}
+
+			channel = chan_arr->number;
+			power = power_arr->type == JSON_DOUBLE ?
+				power_arr->dnumber : (double) power_arr->number;
+
+			ret = hostad_afc_update_chan_info(&c, &count, op_class,
+							  channel, power);
+			if (ret) {
+				iface->afc.chan_info_list = NULL;
+				iface->afc.num_chan_info = 0;
+				return ret;
+			}
+			chan_arr = chan_arr->sibling;
+			power_arr = power_arr->sibling;
+		}
+	}
+	os_free(iface->afc.chan_info_list);
+	iface->afc.chan_info_list = c;
+	iface->afc.num_chan_info = count;
+
+	return 0;
+}
+
+
+static int hostad_afc_get_timeout(struct json_token *obj)
+{
+	int year, month, day, hour, min, sec;
+	os_time_t t;
+	time_t now;
+
+	if (sscanf(obj->string, "%d-%d-%dT%d:%d:%dZ",
+		   &year, &month, &day, &hour, &min, &sec) != 6)
+		return HOSTAPD_AFC_TIMEOUT;
+
+	if (os_mktime(year, month, day, hour, min, sec, &t) < 0)
+		return HOSTAPD_AFC_TIMEOUT;
+
+	time(&now);
+
+	return (time_t) t < now ? HOSTAPD_AFC_RETRY_TIMEOUT :
+				  ((time_t) t - now) * 80 / 100;
+}
+
+
+static int
+hostapd_afc_parse_reply(struct hostapd_iface *iface, char *reply, size_t len)
+{
+	struct hostapd_config *iconf = iface->conf;
+	int request_timeout = -1, ret = -EINVAL;
+	struct json_token *root = NULL, *reply_obj, *token;
+
+	wpa_printf(MSG_DEBUG,
+		   "Received AFC reply: %s, len: %lu, os_strlen: %lu",
+		   reply, len, os_strlen(reply));
+
+	iface->afc_response = os_strdup(reply);
+	if (!iface->afc_response) {
+		wpa_printf(MSG_ERROR,
+			   "Failed to alloc memory for storing AFC reply");
+		return -ENOMEM;
+	}
+
+	root = json_parse((const char *) reply, os_strlen(reply));
+	if (!root) {
+		wpa_printf(MSG_ERROR, "Failed to parse AFC reply payload");
+		goto fail;
+	}
+
+	token = json_get_member(root, "version");
+	if (!token || token->type != JSON_STRING) {
+		wpa_printf(MSG_ERROR, "Missing version in AFC reply");
+		goto fail;
+	}
+	if (iconf->afc.request.version &&
+	    os_strcmp(iconf->afc.request.version, token->string)) {
+		wpa_printf(MSG_ERROR, "Mismatch in AFC reply version");
+		goto fail;
+	}
+
+	reply_obj = json_get_member(root, "availableSpectrumInquiryResponses");
+	if (!reply_obj || reply_obj->type != JSON_ARRAY) {
+		wpa_printf(MSG_ERROR,
+			   "Missing availableSpectrumInquiry in AFC reply");
+		goto fail;
+	}
+	for (token = reply_obj->child; token; token = token->sibling) {
+		struct json_token *reply_elem, *response;
+		int status = -EINVAL, timeout;
+		unsigned int j;
+		char request_id_str[16];
+
+		reply_elem = json_get_member(token, "requestId");
+		if (!reply_elem || reply_elem->type != JSON_STRING) {
+			wpa_printf(MSG_DEBUG,
+				   "Missing requestId in reply element");
+			continue;
+		}
+
+		snprintf(request_id_str, sizeof(request_id_str), "%u",
+			 iface->afc.request_id);
+		if (os_strcmp(request_id_str, reply_elem->string)) {
+			wpa_printf(MSG_DEBUG,
+				   "RequestId mismatch in reply element");
+			continue;
+		}
+
+		reply_elem = json_get_member(token, "rulesetId");
+		if (!reply_elem || reply_elem->type != JSON_STRING) {
+			wpa_printf(MSG_DEBUG,
+				   "Missing rulesetId in reply element");
+			continue;
+		}
+
+		for (j = 0; j < iconf->afc.n_cert_ids; j++) {
+			if (!os_strcmp(iconf->afc.cert_ids[j].rulset,
+				       reply_elem->string))
+				break;
+		}
+
+		if (j == iconf->afc.n_cert_ids) {
+			wpa_printf(MSG_DEBUG,
+				   "RulesetId mismatch in reply element");
+			continue;
+		}
+
+		response = json_get_member(token, "response");
+		if (!response || response->type != JSON_OBJECT) {
+			wpa_printf(MSG_DEBUG,
+				   "Missing response field in reply element");
+			continue;
+		}
+
+		reply_elem = json_get_member(response, "shortDescription");
+		if (reply_elem && reply_elem->type == JSON_STRING)
+			wpa_printf(MSG_DEBUG, "AFC reply element: %s",
+				   reply_elem->string);
+
+		reply_elem = json_get_member(response, "responseCode");
+		if (reply_elem && reply_elem->type == JSON_NUMBER)
+			status = reply_elem->number;
+
+		if (status < 0) {
+			wpa_printf(MSG_DEBUG,
+				   "Reply invalid responseCode: %d", status);
+			continue;
+		}
+		reply_elem = json_get_member(token, "availableFrequencyInfo");
+		if (reply_elem && reply_elem->type == JSON_ARRAY &&
+		    hostad_afc_parse_available_freq_info(iface, reply_elem)) {
+			wpa_printf(MSG_DEBUG,
+				   "Failed to parse availableFrequencyInfo");
+			continue;
+		}
+
+		reply_elem = json_get_member(token, "availableChannelInfo");
+		if (reply_elem && reply_elem->type == JSON_ARRAY &&
+		    hostad_afc_parse_available_chan_info(iface, reply_elem)) {
+			wpa_printf(MSG_DEBUG,
+				   "Failed to parse availableChannelInfo");
+			continue;
+		}
+
+		reply_elem = json_get_member(token, "availabilityExpireTime");
+		if (!reply_elem || reply_elem->type != JSON_STRING) {
+			wpa_printf(MSG_DEBUG,
+				   "Missing expire time, use default timeout");
+			continue;
+		}
+
+		timeout = hostad_afc_get_timeout(reply_elem);
+		if (request_timeout < 0 || timeout < request_timeout)
+			request_timeout = timeout;
+
+		ret = status;
+	}
+
+	iface->afc.data_valid = true;
+	iface->afc.timeout = request_timeout;
+	if (iface->afc.timeout < 0)
+		iface->afc.timeout = HOSTAPD_AFC_RETRY_TIMEOUT;
+
+fail:
+	json_free(root);
+	return ret;
+}
+
+
+static int hostapd_afc_send_receive(struct hostapd_iface *iface)
+{
+	struct hostapd_config *iconf = iface->conf;
+	struct wpabuf *request_obj = NULL;
+	struct timeval sock_timeout = {
+		.tv_sec = 10,
+	};
+	struct sockaddr_un addr = {
+		.sun_family = AF_UNIX,
+#ifdef __FreeBSD__
+		.sun_len = sizeof(addr),
+#endif /* __FreeBSD__ */
+	};
+	char *buf = NULL;
+	int sockfd, ret;
+	fd_set read_set;
+
+	if (iface->afc.data_valid) {
+		/* AFC data already downloaded from the server */
+		return 0;
+	}
+
+	iface->afc.timeout = HOSTAPD_AFC_RETRY_TIMEOUT;
+	if (!iconf->afc.socket) {
+		wpa_printf(MSG_ERROR, "Missing AFC socket string");
+		return -EINVAL;
+	}
+
+	if (os_strlen(iconf->afc.socket) >= sizeof(addr.sun_path)) {
+		wpa_printf(MSG_ERROR, "Malformed AFC socket string %s",
+			   iconf->afc.socket);
+		return -EINVAL;
+	}
+
+	os_strlcpy(addr.sun_path, iconf->afc.socket, sizeof(addr.sun_path));
+	sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
+	if (sockfd < 0) {
+		wpa_printf(MSG_ERROR, "Failed creating AFC socket");
+		return sockfd;
+	}
+
+	if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+		wpa_printf(MSG_ERROR, "Failed connecting AFC socket");
+		ret = -EIO;
+		goto close_sock;
+	}
+
+	request_obj = hostapd_afc_build_request(iface);
+	if (!request_obj) {
+		ret = -EINVAL;
+		goto close_sock;
+	}
+
+	ret = send(sockfd, wpabuf_head(request_obj),
+		   wpabuf_len(request_obj), 0);
+	if (ret < 0 || (size_t)ret != wpabuf_len(request_obj)) {
+		wpa_printf(MSG_ERROR, "Failed sending AFC request");
+		ret = -EIO;
+		goto close_sock;
+	}
+
+	FD_ZERO(&read_set);
+	FD_SET(sockfd, &read_set);
+	if (select(sockfd + 1, &read_set, NULL, NULL, &sock_timeout) < 0) {
+		wpa_printf(MSG_ERROR, "Select failed on AFC socket");
+		ret = -errno;
+		goto close_sock;
+	}
+
+	if (!FD_ISSET(sockfd, &read_set)) {
+		ret = -EIO;
+		goto close_sock;
+	}
+
+	buf = os_zalloc(HOSTAPD_AFC_BUFSIZE);
+	if (!buf) {
+		ret = -ENOMEM;
+		goto close_sock;
+	}
+
+	ret = recv(sockfd, buf, HOSTAPD_AFC_BUFSIZE - 1, 0);
+	if (!ret)
+		ret = -EIO;
+	if (ret < 0)
+		goto close_sock;
+
+	ret = hostapd_afc_parse_reply(iface, buf, ret);
+	if (ret) {
+		wpa_printf(MSG_ERROR, "Failed parsing AFC reply: %d", ret);
+		goto close_sock;
+	}
+	wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AFC_EVENT_RECEIVED);
+close_sock:
+	os_free(buf);
+	wpabuf_free(iface->afc_request);
+	iface->afc_request = request_obj;
+	close(sockfd);
+
+	return ret;
+}
+
+
+static bool hostapd_afc_has_usable_chans(struct hostapd_iface *iface)
+{
+	const struct oper_class_map *oper_class;
+	int channel, chan_num;
+
+	oper_class = get_oper_class(NULL, iface->conf->op_class);
+	if (!oper_class)
+		return false;
+
+	switch (iface->conf->op_class) {
+	case 132: /*  40MHz */
+		chan_num = 2;
+		break;
+	case 133: /*  80MHz */
+		chan_num = 4;
+		break;
+	case 134: /* 160MHz */
+		chan_num = 8;
+		break;
+	case 137: /* 320MHz */
+		chan_num = 16;
+		break;
+	default:
+		chan_num = 1;
+		break;
+	}
+
+	for (channel = oper_class->min_chan; channel <= oper_class->max_chan;
+	     channel += oper_class->inc) {
+		struct hostapd_hw_modes *mode = iface->current_mode;
+		struct hostapd_channel_data *chan = NULL;
+		int i, start_ch = channel;
+
+		if (iface->conf->op_class == 137)
+			start_ch = channel - 30;
+
+		for (i = 0; i < chan_num; i++) {
+			chan = hw_get_channel_chan(mode, start_ch + i * 4,
+						   NULL);
+
+			if (!chan || chan->flag & HOSTAPD_CHAN_DISABLED)
+				break;
+		}
+
+		if (i == chan_num)
+			return true;
+	}
+
+	return false;
+}
+
+
+int hostapd_afc_handle_request(struct hostapd_iface *iface)
+{
+	struct hostapd_config *iconf = iface->conf;
+	int ret;
+
+	/* AFC is required just for standard power AP */
+	if (!he_reg_is_sp(iconf->he_6ghz_reg_pwr_type))
+		return 1;
+
+	if (!is_6ghz_op_class(iconf->op_class) || !is_6ghz_freq(iface->freq))
+		return 1;
+
+	if (iface->state == HAPD_IFACE_ACS)
+		return 1;
+
+	iface->afc.request_id = os_random();
+	ret = hostapd_afc_send_receive(iface);
+	if (ret < 0) {
+		/*
+		 * If the connection to the AFCD failed, resched for a
+		 * future attempt.
+		 */
+		wpa_printf(MSG_ERROR, "AFC connection failed: %d", ret);
+		if (ret == -EIO)
+			ret = 0;
+		goto resched;
+	}
+
+	hostap_afc_disable_channels(iface);
+	if (!hostapd_afc_has_usable_chans(iface))
+		goto resched;
+
+	ret = hostapd_is_usable_chans(iface);
+	if (ret != 1) {
+		/* Trigger an ACS freq scan */
+		iconf->channel = 0;
+		iface->freq = 0;
+		hostapd_set_and_check_bw320_offset(iface->conf, 0);
+
+		if (!ret && acs_init(iface) != HOSTAPD_CHAN_ACS) {
+			wpa_printf(MSG_ERROR, "Could not start ACS");
+			ret = -EINVAL;
+		}
+	}
+
+resched:
+	eloop_cancel_timeout(hostapd_afc_timeout_handler, iface, NULL);
+	eloop_register_timeout(iface->afc.timeout, 0,
+			       hostapd_afc_timeout_handler, iface, NULL);
+
+	return ret;
+}
+
+
+static void hostapd_afc_delete_data_from_server(struct hostapd_iface *iface)
+{
+	wpabuf_free(iface->afc_request);
+	os_free(iface->afc_response);
+	os_free(iface->afc.chan_info_list);
+	os_free(iface->afc.freq_range);
+
+	iface->afc_request = NULL;
+	iface->afc_response = NULL;
+
+	iface->afc.num_freq_range = 0;
+	iface->afc.num_chan_info = 0;
+
+	iface->afc.chan_info_list = NULL;
+	iface->afc.freq_range = NULL;
+
+	iface->afc.data_valid = false;
+}
+
+
+static void hostapd_afc_timeout_handler(void *eloop_ctx, void *timeout_ctx)
+{
+	struct hostapd_iface *iface = eloop_ctx;
+	bool restart_iface = true;
+
+	hostapd_afc_delete_data_from_server(iface);
+	if (iface->state != HAPD_IFACE_ENABLED) {
+		/* Hostapd is not fully enabled yet, toggle the interface */
+		goto restart_interface;
+	}
+
+	if (hostapd_afc_send_receive(iface) < 0 ||
+	    hostapd_afc_reset_channels(iface)) {
+		restart_iface = false;
+		goto restart_interface;
+	}
+
+	if (hostapd_is_usable_chans(iface) > 0) {
+		wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AFC_EVENT_COMPLETE);
+		goto resched;
+	}
+
+	restart_iface = hostapd_afc_has_usable_chans(iface);
+	if (restart_iface) {
+		/* Trigger an ACS freq scan */
+		iface->conf->channel = 0;
+		iface->freq = 0;
+		hostapd_set_and_check_bw320_offset(iface->conf, 0);
+	} else
+		wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AFC_EVENT_COMPLETE);
+
+restart_interface:
+	hostapd_disable_iface(iface);
+	if (restart_iface)
+		hostapd_enable_iface(iface);
+resched:
+	eloop_register_timeout(iface->afc.timeout, 0,
+			       hostapd_afc_timeout_handler, iface, NULL);
+}
+
+
+void hostapd_afc_send_request(struct hostapd_iface *iface)
+{
+	eloop_cancel_timeout(hostapd_afc_timeout_handler, iface, NULL);
+	eloop_register_timeout(0, 0, hostapd_afc_timeout_handler, iface, NULL);
+}
+
+
+void hostapd_afc_stop(struct hostapd_iface *iface)
+{
+	eloop_cancel_timeout(hostapd_afc_timeout_handler, iface, NULL);
+}
+
+
+void hostap_afc_disable_channels(struct hostapd_iface *iface)
+{
+	struct hostapd_hw_modes *mode = NULL;
+	int i;
+
+	for (i = 0; i < iface->num_hw_features; i++) {
+		mode = &iface->hw_features[i];
+		if (mode->mode == HOSTAPD_MODE_IEEE80211A &&
+		    mode->is_6ghz)
+			break;
+	}
+
+	if (i == iface->num_hw_features)
+		return;
+
+	if (!he_reg_is_sp(iface->conf->he_6ghz_reg_pwr_type))
+		return;
+
+	if (!iface->afc.data_valid)
+		return;
+
+	for (i = 0; i < mode->num_channels; i++) {
+		struct hostapd_channel_data *chan = &mode->channels[i];
+		unsigned int j;
+
+		if (!is_6ghz_freq(chan->freq))
+			continue;
+
+		for (j = 0; j < iface->afc.num_freq_range; j++) {
+			if (chan->freq >= iface->afc.freq_range[j].low_freq &&
+			    chan->freq <= iface->afc.freq_range[j].high_freq)
+				break;
+		}
+
+		if (j != iface->afc.num_freq_range)
+			continue;
+
+		for (j = 0; j < iface->afc.num_chan_info; j++) {
+			if (chan->chan == iface->afc.chan_info_list[j].chan)
+				break;
+		}
+
+		if (j != iface->afc.num_chan_info)
+			continue;
+
+		chan->flag |= HOSTAPD_CHAN_DISABLED;
+		wpa_printf(MSG_MSGDUMP,
+			   "Disabling freq=%d MHz (not allowed by AFC)",
+			   chan->freq);
+	}
+}
+
+
+static int hostapd_afc_reset_channels(struct hostapd_iface *iface)
+{
+	int ret;
+
+	ret = hostapd_get_hw_features(iface);
+	if (ret) {
+		wpa_printf(MSG_ERROR, "Failed to reset channel flags");
+		return ret;
+	}
+
+	ret = hostapd_set_current_hw_info(iface, iface->freq);
+	if (ret)
+		wpa_printf(MSG_ERROR, "Failed to set current hardware info");
+
+	return ret;
+}
diff --git a/src/ap/ap_config.c b/src/ap/ap_config.c
index 7146d618a..0c8fb0710 100644
--- a/src/ap/ap_config.c
+++ b/src/ap/ap_config.c
@@ -1049,6 +1049,21 @@ void hostapd_config_free(struct hostapd_config *conf)
 #endif /* CONFIG_ACS */
 	wpabuf_free(conf->lci);
 	wpabuf_free(conf->civic);
+#ifdef CONFIG_AFC
+	os_free(conf->afc.socket);
+	os_free(conf->afc.request.version);
+	os_free(conf->afc.request.sn);
+	for (i = 0; i < conf->afc.n_cert_ids; i++) {
+		os_free(conf->afc.cert_ids[i].rulset);
+		os_free(conf->afc.cert_ids[i].id);
+	}
+	os_free(conf->afc.cert_ids);
+	os_free(conf->afc.location.height_type);
+	os_free(conf->afc.location.linear_polygon_data);
+	os_free(conf->afc.location.radial_polygon_data);
+	os_free(conf->afc.freq_range);
+	os_free(conf->afc.op_class);
+#endif /* CONFIG_AFC */
 
 	os_free(conf);
 }
diff --git a/src/ap/ap_config.h b/src/ap/ap_config.h
index 7b11aca1b..abb063c5d 100644
--- a/src/ap/ap_config.h
+++ b/src/ap/ap_config.h
@@ -1284,6 +1284,52 @@ struct hostapd_config {
 
 	/* Disable MCS15 Subfield in EHT operation element */
 	bool disable_mcs15_rx;
+
+#ifdef CONFIG_AFC
+	struct {
+		char *socket;
+		struct {
+			char *version;
+			char *sn;
+		} request;
+		unsigned int n_cert_ids;
+		struct cert_id {
+			char *rulset;
+			char *id;
+		} *cert_ids;
+		struct {
+			enum afc_location_type {
+				ELLIPSE,
+				LINEAR_POLYGON,
+				RADIAL_POLYGON,
+			} type;
+			unsigned int n_linear_polygon_data;
+			struct afc_linear_polygon {
+				double longitude;
+				double latitude;
+			} *linear_polygon_data;
+			unsigned int n_radial_polygon_data;
+			struct afc_radial_polygon {
+				double length;
+				double angle;
+			} *radial_polygon_data;
+			int major_axis;
+			int minor_axis;
+			int orientation;
+			double height;
+			char *height_type;
+			int vertical_tolerance;
+		} location;
+		unsigned int n_freq_range;
+		struct afc_freq_range {
+			int low_freq;
+			int high_freq;
+		} *freq_range;
+		unsigned int n_op_class;
+		unsigned int *op_class;
+		int min_power;
+	} afc;
+#endif /* CONFIG_AFC */
 };
 
 
diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c
index 8c88d023b..f1b4b25e0 100644
--- a/src/ap/hostapd.c
+++ b/src/ap/hostapd.c
@@ -794,6 +794,7 @@ void hostapd_cleanup_iface_partial(struct hostapd_iface *iface)
 static void hostapd_cleanup_iface(struct hostapd_iface *iface)
 {
 	wpa_printf(MSG_DEBUG, "%s(%p)", __func__, iface);
+	hostapd_afc_stop(iface);
 	eloop_cancel_timeout(hostapd_interface_setup_failure_handler, iface,
 			     NULL);
 
@@ -803,6 +804,12 @@ static void hostapd_cleanup_iface(struct hostapd_iface *iface)
 
 	os_free(iface->config_fname);
 	os_free(iface->bss);
+#ifdef CONFIG_AFC
+	wpabuf_free(iface->afc_request);
+	os_free(iface->afc_response);
+	os_free(iface->afc.chan_info_list);
+	os_free(iface->afc.freq_range);
+#endif
 	wpa_printf(MSG_DEBUG, "%s: free iface=%p", __func__, iface);
 	os_free(iface);
 }
@@ -2679,6 +2686,16 @@ static int hostapd_setup_interface_complete_sync(struct hostapd_iface *iface,
 		}
 #endif /* CONFIG_MESH */
 
+#ifdef CONFIG_IEEE80211AX
+		/* check AFC for 6GHz channels. */
+		res = hostapd_afc_handle_request(iface);
+		if (res <= 0) {
+			if (res < 0)
+				goto fail;
+			return res;
+		}
+#endif /* CONFIG_IEEE80211AX */
+
 		if (!delay_apply_cfg &&
 		    hostapd_set_freq(hapd, hapd->iconf->hw_mode, iface->freq,
 				     hapd->iconf->channel,
@@ -2804,6 +2821,11 @@ dfs_offload:
 	}
 #endif /* CONFIG_FST */
 
+#ifdef CONFIG_AFC
+	if (iface->afc.data_valid)
+		wpa_msg(iface->bss[0]->msg_ctx, MSG_INFO, AFC_EVENT_COMPLETE);
+#endif /* CONFIG_AFC */
+
 	hostapd_set_state(iface, HAPD_IFACE_ENABLED);
 	hostapd_owe_update_trans(iface);
 	airtime_policy_update_init(iface);
@@ -3063,6 +3085,7 @@ void hostapd_interface_deinit(struct hostapd_iface *iface)
 
 	hostapd_set_state(iface, HAPD_IFACE_DISABLED);
 
+	hostapd_afc_stop(iface);
 	eloop_cancel_timeout(channel_list_update_timeout, iface, NULL);
 	iface->wait_channel_update = 0;
 	iface->is_no_ir = false;
@@ -3136,6 +3159,7 @@ void hostapd_interface_free(struct hostapd_iface *iface)
 			   __func__, iface->bss[j]);
 		os_free(iface->bss[j]);
 	}
+
 	hostapd_cleanup_iface(iface);
 }
 
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index 974964d34..285fd0839 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -761,6 +761,35 @@ struct hostapd_iface {
 	/* Use assisted DFS functionality from the driver. This is used only
 	 * in wpa_supplicant builds for P2P GO functionality. */
 	bool assisted_dfs;
+
+#ifdef CONFIG_AFC
+	struct wpabuf *afc_request;
+	char *afc_response;
+	struct {
+		unsigned int request_id;
+		int timeout;
+		unsigned int num_freq_range;
+		struct afc_freq_range_elem {
+			int low_freq;
+			int high_freq;
+			/**
+			 * max eirp power spectral density received from
+			 * the AFC coordinator for this band
+			 */
+			double max_psd;
+		} *freq_range;
+		unsigned int num_chan_info;
+		struct afc_chan_info_elem {
+			int chan;
+			/**
+			 * max eirp power received from the AFC coordinator
+			 * for this channel for each op_class
+			 */
+			double power[5];
+		} *chan_info_list;
+		bool data_valid;
+	} afc;
+#endif /* CONFIG_AFC */
 };
 
 /* hostapd.c */
@@ -817,6 +846,29 @@ void hostapd_ocv_check_csa_sa_query(void *eloop_ctx, void *timeout_ctx);
 
 void hostapd_switch_color(struct hostapd_data *hapd, u64 bitmap);
 void hostapd_cleanup_cca_params(struct hostapd_data *hapd);
+#ifdef CONFIG_AFC
+int hostapd_afc_handle_request(struct hostapd_iface *iface);
+void hostapd_afc_send_request(struct hostapd_iface *iface);
+void hostapd_afc_stop(struct hostapd_iface *iface);
+void hostap_afc_disable_channels(struct hostapd_iface *iface);
+#else
+static inline int hostapd_afc_handle_request(struct hostapd_iface *iface)
+{
+	return 1;
+}
+
+static inline void hostapd_afc_send_request(struct hostapd_iface *iface)
+{
+}
+
+static inline void hostapd_afc_stop(struct hostapd_iface *iface)
+{
+}
+
+static inline void hostap_afc_disable_channels(struct hostapd_iface *iface)
+{
+}
+#endif /* CONFIG_AFC */
 
 /* utils.c */
 int hostapd_register_probereq_cb(struct hostapd_data *hapd,
diff --git a/src/ap/hw_features.c b/src/ap/hw_features.c
index 01a693828..651ec3ebb 100644
--- a/src/ap/hw_features.c
+++ b/src/ap/hw_features.c
@@ -221,6 +221,8 @@ int hostapd_get_hw_features(struct hostapd_iface *iface)
 	iface->hw_features = modes;
 	iface->num_hw_features = num_modes;
 
+	hostap_afc_disable_channels(iface);
+
 	if (orig_mode_valid && !iface->current_mode) {
 		wpa_printf(MSG_ERROR,
 			   "%s: Could not update iface->current_mode",
diff --git a/src/common/wpa_ctrl.h b/src/common/wpa_ctrl.h
index 939b7ccd3..ae93e1e4e 100644
--- a/src/common/wpa_ctrl.h
+++ b/src/common/wpa_ctrl.h
@@ -524,6 +524,9 @@ extern "C" {
  */
 #define PR_PEER_FOUND "PR-PEER-FOUND "
 
+#define AFC_EVENT_RECEIVED "AFC-EVENT-RECEIVED "
+#define AFC_EVENT_COMPLETE "AFC-EVENT-COMPLETE "
+
 /* BSS command information masks */
 
 #define WPA_BSS_MASK_ALL		0xFFFDFFFF
-- 
2.45.2



From junyulai at google.com  Tue Aug  4 00:43:18 2026
From: junyulai at google.com (Junyu Lai)
Date: Tue, 4 Aug 2026 15:43:18 +0800
Subject: [PATCH] supplicant: schedule recovery timeout when P2P listen fails
In-Reply-To: <CAG=E8CGkgwH3sFVAV6SYe=PLcnp+7EbpV=TzbGGVzf0CMBb9eA@mail.gmail.com>
References: <CAG=E8CGkgwH3sFVAV6SYe=PLcnp+7EbpV=TzbGGVzf0CMBb9eA@mail.gmail.com>
Message-ID: <CAG=E8CG_0yVHDZXaqt8kxjpQo35zRH-SjdhjOqrrdiJ6CSc0tQ@mail.gmail.com>

When P2P remain-on-channel (listen) request fails with EBUSY due
to concurrent wlan0 startup scans, the supplicant state machine
clears the pending frequency but fails to schedule a recovery
timeout. As a result, it hangs permanently in the SEARCH state.

Fix this by scheduling a 100 ms recovery timeout when the listen
request fails in p2p_listen_failed, allowing wpa_supplicant to
automatically retry p2p_search() once the driver collision settles.

This issue is reproducible on devices where simultaneous scans on
concurrent interfaces (e.g. station and P2P) are disabled or
restricted in the wireless driver configuration, leading to EBUSY
failures when concurrent active scans are attempted.

The scheduled 100 ms recovery timeout is a state timeout
(p2p_state_timeout) and does not affect the global find timeout
(p2p_find_timeout) registered when p2p_find was initiated. The
global timeout remains active and will still fire to stop the
search when the overall discovery duration expires, ensuring the
supplicant still follows the discovery time constraint.

Signed-off-by: Junyu Lai <junyulai at google.com>
---
 src/p2p/p2p.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/p2p/p2p.c b/src/p2p/p2p.c
index d04802f1..00465fab 100644
--- a/src/p2p/p2p.c
+++ b/src/p2p/p2p.c
@@ -4191,6 +4191,17 @@ void p2p_listen_failed(struct p2p_data *p2p,
unsigned int freq)

        p2p_dbg(p2p, "Listen failed on freq=%u", freq);
        p2p->pending_listen_freq = 0;
+       p2p->pending_listen_wait_drv = false;
+
+       if (p2p->state == P2P_SEARCH ||
+           p2p->state == P2P_CONNECT_LISTEN ||
+           p2p->state == P2P_WAIT_PEER_CONNECT ||
+           p2p->state == P2P_WAIT_PEER_IDLE ||
+           p2p->state == P2P_INVITE_LISTEN) {
+               p2p_dbg(p2p,
+                       "Schedule timeout after listen failure to
continue state machine");
+               p2p_set_timeout(p2p, 0, 100000);
+       }
 }


--
2.55.0.571.g244d577d93-goog


From Allen.Ye at mediatek.com  Tue Aug  4 01:22:57 2026
From: Allen.Ye at mediatek.com (=?utf-8?B?QWxsZW4gWWUgKOiRieiKt+WLsyk=?=)
Date: Tue, 4 Aug 2026 08:22:57 +0000
Subject: [PATCH v12 1/4] hostapd: Support 80/160/320 MHz widths in
 hostapd_is_usable_chans
In-Reply-To: <agyKmNlFBatcBQ_Z@w1.fi>
References: <20260408055640.3298054-1-allen.ye@mediatek.com>
	 <20260408055640.3298054-2-allen.ye@mediatek.com> <agyKmNlFBatcBQ_Z@w1.fi>
Message-ID: <7df7389b4c5ebc3300dd3209cc996b1f5a5a55e4.camel@mediatek.com>

On Tue, 2026-05-19 at 19:06 +0300, Jouni Malinen wrote:
> 
> External email : Please do not click links or open attachments until
> you have verified the sender or the content.
> 
> 
> On Wed, Apr 08, 2026 at 01:56:37PM +0800, Allen Ye wrote:
> > Update hostapd_is_usable_chans utility routine logic to take into
> > account 80MHz, 160MHz and 320MHz channel widths.
> > This is a preliminary patch to introduce AFC support.
> 
> > diff --git a/src/ap/hw_features.c b/src/ap/hw_features.c
> > @@ -1105,11 +1106,6 @@ static int hostapd_is_usable_chans(struct
> > hostapd_iface *iface)
> > -???? err = hostapd_is_usable_chan(iface, pri_chan->freq, 1);
> > -???? if (err <= 0) {
> > -???????????? wpa_printf(MSG_ERROR, "Primary frequency not
> > allowed");
> > -???????????? return err;
> > -???? }
> 
> Why does this remove this call to hostapd_is_usable_chan() with
> primary=1 completely? That feels wrong and at minimum, needs to be
> clearly justified in the commit message. All the new checks use
> primary=0 in the calls so it looks like this patch would completely
> eliminate the check with chan_pri_allowed().
> 
> 
Hi Jouni,

Thank you for pointing this out. We will restore the primary=1 check
and have fixed this in the v13 patch series.

> > @@ -1117,38 +1113,111 @@ static int hostapd_is_usable_chans(struct
> > hostapd_iface *iface)
> > -???? err = hostapd_is_usable_chan(iface, iface->freq +
> > -????????????????????????????????? iface->conf->secondary_channel *
> > 20, 0);
> > -???? if (err > 0) {
> > ????????????? if (iface->conf->secondary_channel == 1 &&
> > -???????????????? (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40P))
> > +???????????????? (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40M))
> > {
> > +???????????????????? iface->conf->secondary_channel = -1;
> > ????????????????????? return 1;
> > +???????????? }
> > +
> > ????????????? if (iface->conf->secondary_channel == -1 &&
> > -???????????????? (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40M))
> > +???????????????? (pri_chan->allowed_bw & HOSTAPD_CHAN_WIDTH_40P))
> > {
> > +???????????????????? iface->conf->secondary_channel = 1;
> > ????????????????????? return 1;
> > -???? }
> 
> Why do these swap the HOSTAPD_CHAN_WIDTH_40P and
> HOSTAPD_CHAN_WIDTH_40M
> checks?

> It is unfortunately difficult to follow the changes proposed into
> this
> function and this feels far from being obviously correct. Would it be
> possible to split this into smaller changes and describe the needed
> change in each individual commit message in a manner that would make
> it
> much easier to understand what is being changed here and why?

This swap was intended to the incorrect secondary_channel handling.
However, as you mentioned, this made the logic hard to follow.
To address this, we have reworked this logic in the v13 series and
split this change into a separate, standalone patch to make it much
clearer and easier to review.

Thanks,
Allen

> 
> --
> Jouni Malinen??????????????????????????????????????????? PGP id
> EFC895FA


From j at w1.fi  Tue Aug  4 01:24:24 2026
From: j at w1.fi (Jouni Malinen)
Date: Tue, 4 Aug 2026 11:24:24 +0300
Subject: [PATCH 1/4] gas_server: resend a response fragment after a lost
 ACK
In-Reply-To: <20260623155726.7399-2-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260623155726.7399-2-gubertoli@gmail.com>
Message-ID: <anGhuCKq-p-8jxyT@w1.fi>

On Tue, Jun 23, 2026 at 05:57:14PM +0200, Gustavo Bertoli wrote:
> A fragmented GAS response advances its cursor as each fragment is handed to
> the driver, regardless of whether that fragment was ACKed. On a
> remain-on-channel rendezvous a fragment can be lost to dwell misalignment
> after the cursor has moved past it, so it was never resent and the transfer
> stalled. Track the previous offset and a tx_pending flag, and on a no-ACK TX
> status roll back and resend that fragment on the next Comeback Request.

What would happen if it is the ACK frame itself that is lost instead of
the frame carrying the fragment of the response? Sending the same
fragment ID in two different MMPDUs feels wrong and it is not clear what
the receiver might do when receiving that.

Working around this cleanly might require low-level operations that
would allow the same MMPDU to be transmitted additional times until an
ACK frame is received or a longer timeout is reached. That said, this
would be a bit inconvenient to implement since it would involve
potentially driver specific changes in kernel..

> A Comeback Request that arrives before the previous fragment's TX status has
> resolved is not yet known to be missed; resending then would supersede the
> still in-flight frame and discard its TX status. Wait for the status
> instead.

This feels like an independent change and it would be good to split into
a separate patch (i.e., that tx_pending tracking that is mentioned above
does not seem to be used at all for resending a prior fragment and it is
just for this part of the patch).

-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Tue Aug  4 01:36:54 2026
From: j at w1.fi (Jouni Malinen)
Date: Tue, 4 Aug 2026 11:36:54 +0300
Subject: [PATCH 3/4] gas_query: retry GAS comeback requests instead of
 failing
In-Reply-To: <20260623155726.7399-4-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260623155726.7399-4-gubertoli@gmail.com>
Message-ID: <anGkphYHwXkAXloh@w1.fi>

On Tue, Jun 23, 2026 at 05:57:16PM +0200, Gustavo Bertoli wrote:
> A transient TX failure, a missed comeback response, or a duplicate fragment
> (a retransmit after a lost ACK) ended the whole GAS query, even though the
> responder keeps its dialog entry alive. When comeback_recover is set, retry
> the comeback request on the same dialog token in each of those cases instead
> of giving up.

> diff --git a/wpa_supplicant/gas_query.c b/wpa_supplicant/gas_query.c
>  	if (gas_query_tx(gas, query, req, wait_time) < 0) {
>  		wpa_printf(MSG_DEBUG, "GAS: Failed to send Action frame to "
>  			   MACSTR, MAC2STR(query->addr));
> -		gas_query_done(gas, query, GAS_QUERY_INTERNAL_ERROR);
> +		if (query->wait_comeback && query->comeback_recover)
> +			eloop_register_timeout(0,
> +					       GAS_QUERY_WAIT_TIME_COMEBACK * 1000,
> +					       gas_query_tx_comeback_timeout,
> +					       gas, query);
> +		else
> +			gas_query_done(gas, query, GAS_QUERY_INTERNAL_ERROR);
>  	}

What is behind this TX failure case? Is this that "transient TX failure"
from the commit message? I can understand a missed comeback response,
but when would this TX operation fail?

Is the "duplicate fragment" case referring to functionality from patch
1? If so, that gives even more reason for my comments on that patch if
it does indeed need changes on the peer device as well.

-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Tue Aug  4 01:48:02 2026
From: j at w1.fi (Jouni Malinen)
Date: Tue, 4 Aug 2026 11:48:02 +0300
Subject: [PATCH] wpa_supplicant: Cache PMK for 802.1X 4-way handshake
 offload
In-Reply-To: <20260803104146.GA354007@ISCN5CG4472QHC.infineon.com>
References: <20260603022228.3052-1-chung-hsien.hsu@infineon.com>
 <amcf9TX23Xvkdyco@w1.fi>
 <20260803104146.GA354007@ISCN5CG4472QHC.infineon.com>
Message-ID: <anGnQsoOSSr-FmoF@w1.fi>

On Mon, Aug 03, 2026 at 06:41:46PM +0800, Chung-Hsien Hsu wrote:
> This fixes a functional issue.
> 
> With IEEE 802.1X 4-way handshake offload, the normal host-side EAPOL-Key
> processing path is bypassed, so the PMK is not stored in the WPA state
> machine and no PMKSA cache entry is created.
> 
> As a result, a subsequent connection cannot use PMKSA caching and may
> require a full EAP authentication again.

It would be good to state that more clearly in the commit message since
that part about PMKSA caching not being available was not obvious since
the focused more on storing the PMK rather than adding a PMKSA cache
entry.

> This change stores the PMK and
> creates the PMKSA cache entry for the applicable non-FT and non-Suite-B
> RSN cases.

If the main point here is in adding that PMKSA cache entry, why the
changes allow wpa_sm_set_pmk() to be called with auth_addr == NULL which
is explicitly documented to not cache the PMK? I can understand the need
to create a PMKSA cache entry, but what is the need, if any, to set
sm->pmk?

-- 
Jouni Malinen                                            PGP id EFC895FA


From gubertoli at gmail.com  Tue Aug  4 11:03:09 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Tue, 4 Aug 2026 20:03:09 +0200
Subject: [PATCH 1/4] gas_server: resend a response fragment after a lost ACK
In-Reply-To: <anGhuCKq-p-8jxyT@w1.fi>
References: <20260623155726.7399-1-gubertoli@gmail.com> <20260623155726.7399-2-gubertoli@gmail.com>
 <anGhuCKq-p-8jxyT@w1.fi>
Message-ID: <CAN=5+sSXfyUWkiVXXSjqXOn3xRNY=rkCzE2hFfP8U7qTjsw8+w@mail.gmail.com>

On Tue, Aug 4, 2026 at 10:24 AM Jouni Malinen <j at w1.fi> wrote:
> What would happen if it is the ACK frame itself that is lost instead of
> the frame carrying the fragment of the response? Sending the same
> fragment ID in two different MMPDUs feels wrong and it is not clear what
> the receiver might do when receiving that.

The peer then requests the next fragment and receives the same
fragment again, which lands in the receiver's existing duplicate
handling: gas_query_rx_comeback() drops frag_id + 1 == next_frag_id as
a "possible retry of previous fragment", so the Fragment ID field
makes the retransmission detectable. Today either loss is terminal
anyway: any no-ACK mid-transfer drops the pending entry. These losses
occur across ROC dwell boundaries, after the driver's own retries are
exhausted, which is why the recovery is done at the GAS layer.

> This feels like an independent change and it would be good to split into
> a separate patch (i.e., that tx_pending tracking that is mentioned above
> does not seem to be used at all for resending a prior fragment and it is
> just for this part of the patch).

Agreed, I will split it in v2 and place right after this rollback
patch; its commit message will describe the race and when it becomes
reachable.

Gustavo


From gubertoli at gmail.com  Tue Aug  4 11:32:57 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Tue, 4 Aug 2026 20:32:57 +0200
Subject: [PATCH 3/4] gas_query: retry GAS comeback requests instead of failing
In-Reply-To: <anGkphYHwXkAXloh@w1.fi>
References: <20260623155726.7399-1-gubertoli@gmail.com> <20260623155726.7399-4-gubertoli@gmail.com>
 <anGkphYHwXkAXloh@w1.fi>
Message-ID: <CAN=5+sQ=_RusLznj546t0r5h-+7w7UiUrW+cciaZgEYfsirBNA@mail.gmail.com>

On Tue, Aug 4, 2026 at 10:36 AM Jouni Malinen <j at w1.fi> wrote:
> What is behind this TX failure case? Is this that "transient TX failure"
> from the commit message? I can understand a missed comeback response,
> but when would this TX operation fail?

Yes. The comeback request can be handed to offchannel_send_action()
just after the remain-on-channel window has lapsed, and the driver
then rejects the off-channel TX synchronously, so it never reaches
gas_query_tx_status(). Today that ends the query, as does the
asynchronous TX failure; with comeback_recover set both become a
bounded retry, and the retry succeeds once the next ROC window is
armed. Seen repeatedly on rtw88 and ath9k_htc, not reproducible
under hwsim.

> Is the "duplicate fragment" case referring to functionality from patch
> 1? If so, that gives even more reason for my comments on that patch if
> it does indeed need changes on the peer device as well.

Yes, it is the requester-side counterpart of the patch 1 resend. With
only one side updated the unpatched peer still sees valid GAS frames,
and the cases that side cannot recover still end the exchange, as
they do today. For example, a patched requester retrying the same
dialog token against an unpatched responder stops on the existing
unexpected-frag_id check (PEER_ERROR), where the current code fails
through its one-shot new-token restart instead. The retry is opt-in
for the DPP certBag exchange only.

Gustavo


From lachlan.hodges at morsemicro.com  Tue Aug  4 21:51:55 2026
From: lachlan.hodges at morsemicro.com (Lachlan Hodges)
Date: Wed, 5 Aug 2026 14:51:55 +1000
Subject: [RFC 0/8] Introduce S1G Support
In-Reply-To: <quf3x6ptlagwta6dpp4nsilmm5d4wepwucydkxkm7yb7dxwsmm@jvep7xtzta67>
References: <20260528063857.950556-1-lachlan.hodges@morsemicro.com>
 <quf3x6ptlagwta6dpp4nsilmm5d4wepwucydkxkm7yb7dxwsmm@jvep7xtzta67>
Message-ID: <zvxk5nyytqjp5afbtwp66isi6qy3erg2dsivxajbgsjujakupi@zybx5ljhjt2p>

On Tue, Jul 14, 2026 at 11:25:13AM +1000, Lachlan Hodges wrote:
> Hi Jouni,
> 
> > Over the last ~1.5 years we have been improving / fixing the S1G support
> > within mac80211/cfg80211 to the point where you can bring up a native
> > S1G AP/STA. We also have a driver currently under review [1].

Hi Jouni, now that the driver is in the kernel, i am currently rebasing
this patchset on the tip of hostap.git and would love to get your
input so we can send a more useful v2.

Thanks,
lachlan


From gubertoli at gmail.com  Wed Aug  5 10:53:47 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:47 +0200
Subject: [PATCH v2 0/5] dpp: recover Enterprise provisioning from ROC dwell misalignment
In-Reply-To: <20260623155726.7399-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-1-gubertoli@gmail.com>

DPP Enterprise provisioning (sta-dot1x, certBag) is the hard case for
GAS when the Configurator is a station: the exchange is an off-channel
station-to-station rendezvous, each peer reachable only inside its own
remain-on-channel (ROC) dwell. The external CA signing delay forces many
comeback rounds and the large certBag fragments the Configuration
response, so one provisioning is many off-channel exchanges -- and a
frame sent just as the peer's dwell has lapsed is not received. The
current code treats the first such miss as terminal: the query restarts
on a new dialog token, abandoning the Configurator's pending entry
(keyed by dialog token).

This series adds bounded, same-dialog-token recovery so a missed
off-channel exchange is retried when the dwells realign. The failure
modes were observed on real off-channel hardware (a station Configurator
provisioning a station Enrollee) across repeated runs with external CA
signing delays up to 50 s: the unpatched tree loses provisioning at the
misaligned dwells; this series recovers it.

Changes in v2, addressing the review comments on 1/4 and 3/4:
  - split the gas_server patch in two: the no-ACK rollback/resend (1/5)
    and waiting for TX status before answering a Comeback Request (2/5),
    each with its own rationale
  - 1/5 commit message describes the lost-ACK case: the resent fragment
    is detectable through the Fragment ID field and the requester
    already drops it as a possible retry of the previous fragment
  - 4/5 commit message spells out the transient TX failure (a
    synchronous offchannel_send_action() failure on a lapsed ROC window)
    and the duplicate fragment case (the responder retransmission after
    a lost ACK)

The code itself is unchanged from v1; only the patch split and commit
messages differ.

Gustavo Bertoli (5):
  gas_server: resend a response fragment after a lost ACK
  gas_server: wait for TX status before answering a Comeback Request
  gas_query: add comeback-recovery opt-in for DPP certBag
  gas_query: retry GAS comeback requests instead of failing
  tests: add DPP GAS comeback fault injection and regression tests

 src/common/dpp.h                |  2 +
 src/common/gas_server.c         | 41 +++++++++++++++
 tests/hwsim/test_dpp.py         | 82 +++++++++++++++++++++++++++++
 wpa_supplicant/dpp_supplicant.c |  6 +++
 wpa_supplicant/gas_query.c      | 92 ++++++++++++++++++++++++++++-----
 wpa_supplicant/gas_query.h      |  2 +
 6 files changed, 213 insertions(+), 12 deletions(-)

-- 
2.39.5



From gubertoli at gmail.com  Wed Aug  5 10:53:48 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:48 +0200
Subject: [PATCH v2 1/5] gas_server: resend a response fragment after a lost ACK
In-Reply-To: <20260805175353.7697-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260805175353.7697-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-2-gubertoli@gmail.com>

A fragmented GAS response advances its cursor as each fragment is handed
to the driver, regardless of whether that fragment was ACKed. On a
remain-on-channel rendezvous a fragment can be lost to dwell
misalignment after the cursor has moved past it, and the no-ACK TX
status then drops the whole pending entry, ending the exchange. Track
the previous offset and on a no-ACK TX status roll back and resend that
fragment on the next Comeback Request.

If it was the ACK frame that was lost rather than the fragment, the peer
receives the same fragment again in a new MMPDU. The Fragment ID field
makes that retransmission detectable and gas_query_rx_comeback() already
drops such a frame as a possible retry of the previous fragment; the
current code fails in both loss cases by dropping the pending entry.

Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
 src/common/gas_server.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/common/gas_server.c b/src/common/gas_server.c
index 52c14df36..012bf2939 100644
--- a/src/common/gas_server.c
+++ b/src/common/gas_server.c
@@ -36,6 +36,7 @@ struct gas_server_handler {
 struct gas_server_response {
 	struct dl_list list;
 	size_t offset;
+	size_t last_offset;
 	u8 frag_id;
 	struct wpabuf *resp;
 	int freq;
@@ -332,6 +333,7 @@ gas_server_handle_rx_comeback_req(struct gas_server_response *response)
 	wpabuf_put_data(resp, wpabuf_head_u8(response->resp) + response->offset,
 			resp_frag_len);
 
+	response->last_offset = response->offset;
 	response->offset += resp_frag_len;
 
 	if (remaining > resp_frag_len)
@@ -430,6 +432,15 @@ static void gas_server_handle_tx_status(struct gas_server_response *response,
 		return;
 	}
 
+	if (!ack && response->resp) {
+		/* Fragment not ACKed; roll back to resend on the next request */
+		if (response->frag_id > 0) {
+			response->offset = response->last_offset;
+			response->frag_id--;
+		}
+		return;
+	}
+
 	if (!ack)
 		wpa_printf(MSG_DEBUG,
 			   "GAS: No ACK received - drop pending entry");
-- 
2.39.5



From gubertoli at gmail.com  Wed Aug  5 10:53:49 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:49 +0200
Subject: [PATCH v2 2/5] gas_server: wait for TX status before answering a Comeback Request
In-Reply-To: <20260805175353.7697-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260805175353.7697-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-3-gubertoli@gmail.com>

A Comeback Request that arrives while the previous fragment's TX
status is still pending is not yet known to be missed. Responding to
it immediately supersedes the in-flight frame, so that frame's TX
status is reported against a stale cookie and lost; combined with the
rollback from the previous patch this can leave the entry parked, with
the final fragment never confirmed as delivered.

The duplicate Comeback Requests that trigger this arise from the
requester-side same-token retry added later in this series, and the
previous patch's no-ACK rollback is what keeps the entry alive to be
parked, so this hardens the recovery added by the series rather than
fixing a standalone problem.

Track a tx_pending flag and ignore a Comeback Request while the
previous fragment's TX status is pending; the frame answering it is
already in flight.

Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
 src/common/gas_server.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/common/gas_server.c b/src/common/gas_server.c
index 012bf2939..80c75251a 100644
--- a/src/common/gas_server.c
+++ b/src/common/gas_server.c
@@ -38,6 +38,7 @@ struct gas_server_response {
 	size_t offset;
 	size_t last_offset;
 	u8 frag_id;
+	bool tx_pending;
 	struct wpabuf *resp;
 	int freq;
 	u8 dst[ETH_ALEN];
@@ -276,6 +277,11 @@ gas_server_handle_rx_comeback_req(struct gas_server_response *response)
 	struct wpabuf *resp;
 	unsigned int wait_time = 0;
 
+	if (response->tx_pending && response->resp) {
+		/* Previous fragment's TX status still pending; wait for it */
+		return;
+	}
+
 	if (!response->resp) {
 		resp = gas_build_comeback_resp(response->dialog_token,
 					       WLAN_STATUS_SUCCESS, 0, 0,
@@ -341,6 +347,7 @@ gas_server_handle_rx_comeback_req(struct gas_server_response *response)
 
 send_resp:
 	gas->tx(gas->ctx, response->freq, response->dst, resp, wait_time);
+	response->tx_pending = true;
 	wpabuf_free(resp);
 }
 
@@ -419,6 +426,8 @@ int gas_server_rx(struct gas_server *gas, const u8 *da, const u8 *sa,
 static void gas_server_handle_tx_status(struct gas_server_response *response,
 					int ack)
 {
+	response->tx_pending = false;
+
 	if (ack && response->resp &&
 	    response->offset < wpabuf_len(response->resp)) {
 		wpa_printf(MSG_DEBUG,
-- 
2.39.5



From gubertoli at gmail.com  Wed Aug  5 10:53:50 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:50 +0200
Subject: [PATCH v2 3/5] gas_query: add comeback-recovery opt-in for DPP certBag
In-Reply-To: <20260805175353.7697-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260805175353.7697-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-4-gubertoli@gmail.com>

The GAS client in gas_query.c is shared by ANQP/Interworking, Hotspot 2.0
and DPP. The next patch adds comeback recovery to it, but that recovery is
only wanted for DPP Enterprise certBag provisioning, where the Configurator
may take seconds to return a CA-signed certificate; a failed GAS exchange
there is terminal. Other DPP configurations are small and immediate, and
ANQP/HS2.0 have their own retry behaviour.

Add a per-query comeback_recover flag (default off) and a small
gas_query_set_comeback_recover() that turns it on for one in-flight query.
DPP enables recovery only on the Configuration Request that carries a CSR
(auth->csr set). This patch only stores the flag and changes no behaviour,
so the recovery added next is limited to the DPP certBag exchange.

Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
 wpa_supplicant/dpp_supplicant.c |  6 ++++++
 wpa_supplicant/gas_query.c      | 18 ++++++++++++++++++
 wpa_supplicant/gas_query.h      |  2 ++
 3 files changed, 26 insertions(+)

diff --git a/wpa_supplicant/dpp_supplicant.c b/wpa_supplicant/dpp_supplicant.c
index 6a3bf6a3e..47be2ad53 100644
--- a/wpa_supplicant/dpp_supplicant.c
+++ b/wpa_supplicant/dpp_supplicant.c
@@ -2090,6 +2090,12 @@ static void wpas_dpp_start_gas_client(struct wpa_supplicant *wpa_s)
 		wpa_printf(MSG_DEBUG,
 			   "DPP: GAS query started with dialog token %u", res);
 		wpa_s->dpp_gas_dialog_token = res;
+		if (auth->csr &&
+		    gas_query_set_comeback_recover(wpa_s->gas,
+						   auth->peer_mac_addr,
+						   res) < 0)
+			wpa_printf(MSG_DEBUG,
+				   "DPP: Could not enable GAS comeback recovery");
 	}
 }
 
diff --git a/wpa_supplicant/gas_query.c b/wpa_supplicant/gas_query.c
index 73cc34e0a..e6f18e856 100644
--- a/wpa_supplicant/gas_query.c
+++ b/wpa_supplicant/gas_query.c
@@ -45,6 +45,7 @@ struct gas_query_pending {
 	unsigned int wait_comeback:1;
 	unsigned int offchannel_tx_started:1;
 	unsigned int retry:1;
+	unsigned int comeback_recover:1;
 	unsigned int wildcard_bssid:1;
 	unsigned int maintain_addr:1;
 	unsigned int sent:1;
@@ -921,6 +922,23 @@ int gas_query_req(struct gas_query *gas, const u8 *dst, int freq,
 }
 
 
+int gas_query_set_comeback_recover(struct gas_query *gas, const u8 *dst,
+				   u8 dialog_token)
+{
+	struct gas_query_pending *query;
+
+	dl_list_for_each(query, &gas->pending, struct gas_query_pending, list) {
+		if (query->dialog_token == dialog_token &&
+		    ether_addr_equal(query->addr, dst)) {
+			query->comeback_recover = 1;
+			return 0;
+		}
+	}
+
+	return -1;
+}
+
+
 int gas_query_stop(struct gas_query *gas, u8 dialog_token)
 {
 	struct gas_query_pending *query;
diff --git a/wpa_supplicant/gas_query.h b/wpa_supplicant/gas_query.h
index 6ccecd4dd..d23503d50 100644
--- a/wpa_supplicant/gas_query.h
+++ b/wpa_supplicant/gas_query.h
@@ -40,6 +40,8 @@ int gas_query_req(struct gas_query *gas, const u8 *dst, int freq,
 			     const struct wpabuf *adv_proto,
 			     const struct wpabuf *resp, u16 status_code),
 		  void *ctx);
+int gas_query_set_comeback_recover(struct gas_query *gas, const u8 *dst,
+				   u8 dialog_token);
 int gas_query_stop(struct gas_query *gas, u8 dialog_token);
 
 #else /* CONFIG_GAS */
-- 
2.39.5



From gubertoli at gmail.com  Wed Aug  5 10:53:51 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:51 +0200
Subject: [PATCH v2 4/5] gas_query: retry GAS comeback requests instead of failing
In-Reply-To: <20260805175353.7697-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260805175353.7697-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-5-gubertoli@gmail.com>

A transient TX failure, a missed comeback response, or a duplicate
fragment ended the whole GAS query, even though the responder keeps its
dialog entry alive. When comeback_recover is set, retry the comeback
request on the same dialog token in each of those cases instead of
giving up.

The transient TX failure is a synchronous offchannel_send_action()
failure, seen on real hardware when the comeback request is issued just
as the remain-on-channel window has lapsed and the driver cannot
schedule the off-channel TX at that moment; being synchronous, it never
reaches gas_query_tx_status(), and either TX failure flavor ends the
query in the current code. The duplicate fragment is the responder
retransmission after a lost ACK: the requester already drops it as a
possible retry of the previous fragment, and now re-requests the next
fragment instead of stalling.

A stuck leg is bounded by a per-leg recovery watchdog:
GAS_QUERY_DPP_COMEBACK_TIMEOUT (5 s), armed on real progress (a received
comeback response) and not reset on a retransmission, so it fires once
no comeback response has arrived for that long. This assumes the
announced comeback delay stays well under it.

Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
 wpa_supplicant/gas_query.c | 74 +++++++++++++++++++++++++++++++-------
 1 file changed, 62 insertions(+), 12 deletions(-)

diff --git a/wpa_supplicant/gas_query.c b/wpa_supplicant/gas_query.c
index e6f18e856..d00d25f49 100644
--- a/wpa_supplicant/gas_query.c
+++ b/wpa_supplicant/gas_query.c
@@ -26,6 +26,9 @@
 /** GAS query timeout in seconds */
 #define GAS_QUERY_TIMEOUT_PERIOD 2
 
+/* DPP GAS comeback recovery watchdog in seconds */
+#define GAS_QUERY_DPP_COMEBACK_TIMEOUT 5
+
 /* GAS query wait-time / duration in ms */
 #define GAS_QUERY_WAIT_TIME_INITIAL 1000
 #define GAS_QUERY_WAIT_TIME_COMEBACK 150
@@ -268,18 +271,33 @@ static void gas_query_tx_status(struct wpa_supplicant *wpa_s,
 	}
 	os_get_reltime(&query->last_oper);
 
+	if ((result == OFFCHANNEL_SEND_ACTION_NO_ACK ||
+	     result == OFFCHANNEL_SEND_ACTION_FAILED) && query->wait_comeback &&
+	    query->comeback_recover) {
+		/* Comeback request not delivered; retry (watchdog bounds the leg) */
+		wpa_printf(MSG_DEBUG, "GAS: No ACK to comeback request - retry");
+		eloop_cancel_timeout(gas_query_rx_comeback_timeout, gas, query);
+		eloop_cancel_timeout(gas_query_tx_comeback_timeout, gas, query);
+		eloop_register_timeout(0, GAS_QUERY_WAIT_TIME_COMEBACK * 1000,
+				       gas_query_tx_comeback_timeout, gas, query);
+		return;
+	}
 	if (result == OFFCHANNEL_SEND_ACTION_SUCCESS ||
 	    result == OFFCHANNEL_SEND_ACTION_NO_ACK) {
-		eloop_cancel_timeout(gas_query_timeout, gas, query);
-		if (result == OFFCHANNEL_SEND_ACTION_NO_ACK) {
-			wpa_printf(MSG_DEBUG, "GAS: No ACK to GAS request");
-			eloop_register_timeout(0, 250000,
-					       gas_query_timeout, gas, query);
-		} else {
-			eloop_register_timeout(GAS_QUERY_TIMEOUT_PERIOD, 0,
-					       gas_query_timeout, gas, query);
+		if (!(query->wait_comeback && query->comeback_recover)) {
+			/* Recovery watchdog tracks progress, not each TX; keep it */
+			eloop_cancel_timeout(gas_query_timeout, gas, query);
+			if (result == OFFCHANNEL_SEND_ACTION_NO_ACK) {
+				wpa_printf(MSG_DEBUG, "GAS: No ACK to GAS request");
+				eloop_register_timeout(0, 250000,
+						       gas_query_timeout, gas, query);
+			} else {
+				eloop_register_timeout(GAS_QUERY_TIMEOUT_PERIOD, 0,
+						       gas_query_timeout, gas, query);
+			}
 		}
-		if (query->wait_comeback && !query->retry) {
+		if (query->wait_comeback &&
+		    (query->comeback_recover || !query->retry)) {
 			eloop_cancel_timeout(gas_query_rx_comeback_timeout,
 					     gas, query);
 			eloop_register_timeout(
@@ -343,19 +361,28 @@ static void gas_query_tx_comeback_req(struct gas_query *gas,
 	struct wpabuf *req;
 	unsigned int wait_time;
 
+	eloop_cancel_timeout(gas_query_rx_comeback_timeout, gas, query);
+
 	req = gas_build_comeback_req(query->dialog_token);
 	if (req == NULL) {
 		gas_query_done(gas, query, GAS_QUERY_INTERNAL_ERROR);
 		return;
 	}
 
-	wait_time = (query->retry || !query->offchannel_tx_started) ?
+	wait_time = (query->retry || !query->offchannel_tx_started ||
+		     (query->comeback_recover && query->wait_comeback)) ?
 		GAS_QUERY_WAIT_TIME_INITIAL : GAS_QUERY_WAIT_TIME_COMEBACK;
 
 	if (gas_query_tx(gas, query, req, wait_time) < 0) {
 		wpa_printf(MSG_DEBUG, "GAS: Failed to send Action frame to "
 			   MACSTR, MAC2STR(query->addr));
-		gas_query_done(gas, query, GAS_QUERY_INTERNAL_ERROR);
+		if (query->wait_comeback && query->comeback_recover)
+			eloop_register_timeout(0,
+					       GAS_QUERY_WAIT_TIME_COMEBACK * 1000,
+					       gas_query_tx_comeback_timeout,
+					       gas, query);
+		else
+			gas_query_done(gas, query, GAS_QUERY_INTERNAL_ERROR);
 	}
 
 	wpabuf_free(req);
@@ -371,7 +398,17 @@ static void gas_query_rx_comeback_timeout(void *eloop_data, void *user_ctx)
 	wpa_printf(MSG_DEBUG,
 		   "GAS: No response to comeback request received (retry=%u)",
 		   query->retry);
-	if (gas->current != query || query->retry)
+	if (gas->current != query)
+		return;
+
+	if (query->wait_comeback && query->comeback_recover) {
+		wpa_printf(MSG_DEBUG,
+			   "GAS: Comeback response missed, retry same token");
+		gas_query_tx_comeback_req(gas, query);
+		return;
+	}
+
+	if (query->retry)
 		return;
 	dialog_token = gas_query_new_dialog_token(gas, query->addr);
 	if (dialog_token < 0)
@@ -420,6 +457,12 @@ static void gas_query_tx_comeback_req_delay(struct gas_query *gas,
 	eloop_cancel_timeout(gas_query_tx_comeback_timeout, gas, query);
 	eloop_register_timeout(secs, usecs, gas_query_tx_comeback_timeout,
 			       gas, query);
+	if (query->comeback_recover) {
+		/* Bound a stuck leg by a fixed recovery watchdog */
+		eloop_cancel_timeout(gas_query_timeout, gas, query);
+		eloop_register_timeout(GAS_QUERY_DPP_COMEBACK_TIMEOUT, 0,
+				       gas_query_timeout, gas, query);
+	}
 }
 
 
@@ -496,11 +539,18 @@ static void gas_query_rx_comeback(struct gas_query *gas,
 		if (frag_id + 1 == query->next_frag_id) {
 			wpa_printf(MSG_DEBUG, "GAS: Drop frame as possible "
 				   "retry of previous fragment");
+			if (query->comeback_recover)
+				gas_query_tx_comeback_req(gas, query);
 			return;
 		}
 		gas_query_done(gas, query, GAS_QUERY_PEER_ERROR);
 		return;
 	}
+	if (query->comeback_recover) {
+		eloop_cancel_timeout(gas_query_timeout, gas, query);
+		eloop_register_timeout(GAS_QUERY_DPP_COMEBACK_TIMEOUT, 0,
+				       gas_query_timeout, gas, query);
+	}
 	query->next_frag_id++;
 
 	if (gas_query_append(query, resp, len) < 0) {
-- 
2.39.5



From gubertoli at gmail.com  Wed Aug  5 10:53:52 2026
From: gubertoli at gmail.com (Gustavo Bertoli)
Date: Wed,  5 Aug 2026 19:53:52 +0200
Subject: [PATCH v2 5/5] tests: add DPP GAS comeback fault injection and regression tests
In-Reply-To: <20260805175353.7697-1-gubertoli@gmail.com>
References: <20260623155726.7399-1-gubertoli@gmail.com>
 <20260805175353.7697-1-gubertoli@gmail.com>
Message-ID: <20260805175353.7697-6-gubertoli@gmail.com>

The recovery reacts to fragment loss and reordering on a remain-on-channel
constrained link during DPP Enterprise certBag provisioning, which plain
hwsim does not reproduce. A failed DPP configuration is terminal, so drive
both regressions through the DPP Enterprise (sta-dot1x) certBag exchange,
the path the recovery is scoped to.

Reuse the existing dpp_test knob with two new values that inject one adverse
comeback condition at the GAS server, and add two DPP Enterprise tests, one
per recovery case:
  test_dpp_gas_comeback_enterprise_missed_response - a missed comeback
    response (DPP_TEST_GAS_COMEBACK_DROP): the Configurator drops one comeback
    response while preparing the certBag; the Enrollee must re-request on the
    same dialog token. An upstream new-token restart abandons the
    Configurator's pending entry (keyed by dialog token) and fails.
  test_dpp_gas_comeback_enterprise_lost_ack - a lost fragment ACK
    (DPP_TEST_GAS_COMEBACK_NO_ACK): the Configurator treats one certBag
    fragment's TX as not ACKed. It advances its offset only after a fragment
    is acknowledged, so it resends the unacknowledged one; upstream advanced
    on send and dropped the fragment for good.

Both fail (DPP-CONF-FAILED) on the unpatched tree.

Signed-off-by: Gustavo Bertoli <gubertoli at gmail.com>
---
 src/common/dpp.h        |  2 +
 src/common/gas_server.c | 21 +++++++++++
 tests/hwsim/test_dpp.py | 82 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 105 insertions(+)

diff --git a/src/common/dpp.h b/src/common/dpp.h
index 6aef72165..3cdf86105 100644
--- a/src/common/dpp.h
+++ b/src/common/dpp.h
@@ -568,6 +568,8 @@ enum dpp_test_behavior {
 	DPP_TEST_INVALID_R_BOOTSTRAP_KEY_HASH_PB_REQ = 98,
 	DPP_TEST_INVALID_I_BOOTSTRAP_KEY_HASH_PB_RESP = 99,
 	DPP_TEST_INVALID_R_BOOTSTRAP_KEY_HASH_PB_RESP = 100,
+	DPP_TEST_GAS_COMEBACK_NO_ACK = 101,
+	DPP_TEST_GAS_COMEBACK_DROP = 102,
 };
 
 extern enum dpp_test_behavior dpp_test;
diff --git a/src/common/gas_server.c b/src/common/gas_server.c
index 80c75251a..880b5c968 100644
--- a/src/common/gas_server.c
+++ b/src/common/gas_server.c
@@ -16,6 +16,9 @@
 #include "ieee802_11_defs.h"
 #include "gas.h"
 #include "gas_server.h"
+#ifdef CONFIG_TESTING_OPTIONS
+#include "dpp.h"
+#endif /* CONFIG_TESTING_OPTIONS */
 
 
 #define MAX_ADV_PROTO_ID_LEN 10
@@ -277,6 +280,14 @@ gas_server_handle_rx_comeback_req(struct gas_server_response *response)
 	struct wpabuf *resp;
 	unsigned int wait_time = 0;
 
+#ifdef CONFIG_TESTING_OPTIONS
+	if (dpp_test == DPP_TEST_GAS_COMEBACK_DROP) {
+		dpp_test = DPP_TEST_DISABLED;
+		wpa_printf(MSG_INFO, "GAS: TESTING - drop one comeback response");
+		return;
+	}
+#endif /* CONFIG_TESTING_OPTIONS */
+
 	if (response->tx_pending && response->resp) {
 		/* Previous fragment's TX status still pending; wait for it */
 		return;
@@ -492,6 +503,16 @@ void gas_server_tx_status(struct gas_server *gas, const u8 *dst, const u8 *data,
 		if (response->dialog_token != dialog_token ||
 		    !ether_addr_equal(dst, response->dst))
 			continue;
+#ifdef CONFIG_TESTING_OPTIONS
+		/* Pretend the first fragment was not ACKed once */
+		if (dpp_test == DPP_TEST_GAS_COMEBACK_NO_ACK &&
+		    response->resp && response->frag_id == 1) {
+			dpp_test = DPP_TEST_DISABLED;
+			wpa_printf(MSG_INFO,
+				   "GAS: TESTING - treat comeback fragment TX as not ACKed");
+			ack = 0;
+		}
+#endif /* CONFIG_TESTING_OPTIONS */
 		gas_server_handle_tx_status(response, ack);
 		return;
 	}
diff --git a/tests/hwsim/test_dpp.py b/tests/hwsim/test_dpp.py
index 13636801f..2f915c963 100644
--- a/tests/hwsim/test_dpp.py
+++ b/tests/hwsim/test_dpp.py
@@ -7981,3 +7981,85 @@ def test_dpp_proto_stop_after_auth_hostapd(dev, apdev):
     ev = hapd.wait_event(["DPP-CONF-FAILED"], timeout=11)
     if ev is None:
         raise Exception("DPP config failure not reported")
+
+def run_dpp_gas_comeback_enterprise(dev, params, fault):
+    # Drive DPP Enterprise (sta-dot1x) certBag provisioning while injecting one
+    # GAS comeback fault at the Configurator's gas_server, and assert the
+    # Enrollee still completes. The large certBag fragments the Configuration
+    # response and the external CA signing forces comeback rounds, so this is
+    # the path the recovery is scoped to (auth->csr set on the Enrollee).
+    if not openssl_imported:
+        raise HwsimSkip("OpenSSL python method not available")
+    check_dpp_capab(dev[0])
+    check_dpp_capab(dev[1])
+    with open("auth_serv/ec-ca.pem", "rb") as f:
+        cacert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
+                                                 f.read())
+    with open("auth_serv/ec-ca.key", "rb") as f:
+        cakey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM,
+                                               f.read())
+    conf_id = dev[1].dpp_configurator_add()
+    id0 = dev[0].dpp_bootstrap_gen(chan="81/1", mac=True)
+    uri0 = dev[0].request("DPP_BOOTSTRAP_GET_URI %d" % id0)
+    dev[0].dpp_listen(2412)
+    csrattrs = "MAsGCSqGSIb3DQEJBw=="
+    cert_file = params['prefix'] + ".cert.pem"
+    pkcs7_file = params['prefix'] + ".pkcs7.der"
+    try:
+        dev[1].set("dpp_test", str(fault))
+        id1 = dev[1].dpp_auth_init(uri=uri0, configurator=conf_id,
+                                   conf="sta-dot1x", csrattrs=csrattrs)
+        ev = dev[1].wait_event(["DPP-CSR"], timeout=10)
+        if ev is None:
+            raise Exception("Configurator did not receive CSR")
+        id1_csr = int(ev.split(' ')[1].split('=')[1])
+        csr = ev.split(' ')[2]
+        if not csr.startswith("csr="):
+            raise Exception("Could not parse CSR event: " + ev)
+        csr = base64.b64decode(csr[4:].encode())
+        cert = dpp_sign_cert(cacert, cakey, csr)
+        with open(cert_file, 'wb') as f:
+            f.write(OpenSSL.crypto.dump_certificate(
+                OpenSSL.crypto.FILETYPE_PEM, cert))
+        subprocess.check_call(['openssl', 'crl2pkcs7', '-nocrl',
+                               '-certfile', cert_file,
+                               '-certfile', 'auth_serv/ec-ca.pem',
+                               '-outform', 'DER', '-out', pkcs7_file])
+        with open(pkcs7_file, 'rb') as f:
+            certbag = base64.b64encode(f.read()).decode()
+        res = dev[1].request("DPP_CA_SET peer=%d name=certBag value=%s" %
+                             (id1_csr, certbag))
+        if "OK" not in res:
+            raise Exception("Failed to set certBag")
+        ev = dev[0].wait_event(["DPP-CONF-RECEIVED", "DPP-CONF-FAILED"],
+                               timeout=20)
+    finally:
+        dev[1].set("dpp_test", "0", allow_fail=True)
+    if ev is None:
+        raise Exception("DPP configuration result not reported (Enrollee)")
+    return ev
+
+def test_dpp_gas_comeback_enterprise_missed_response(dev, apdev, params):
+    """DPP Enterprise certBag survives a missed GAS comeback response
+
+    Case: a missed comeback response keeping the dialog token. The Configurator
+    drops one comeback response while still preparing the certBag. The Enrollee
+    must re-request on the SAME dialog token; an upstream restart on a new token
+    abandons the Configurator's pending entry (keyed by dialog token) and the
+    certBag is lost."""
+    ev = run_dpp_gas_comeback_enterprise(dev, params,
+                                         102)  # DPP_TEST_GAS_COMEBACK_DROP
+    if "DPP-CONF-FAILED" in ev:
+        raise Exception("Enrollee did not recover from a missed comeback response")
+
+def test_dpp_gas_comeback_enterprise_lost_ack(dev, apdev, params):
+    """DPP Enterprise certBag survives a lost GAS comeback fragment ACK
+
+    Case: a certBag fragment whose TX is not ACKed. The Configurator advances
+    its offset only after a fragment's TX is acknowledged, so it can resend an
+    unacknowledged fragment. Upstream advanced on send regardless of the TX
+    result, losing a fragment dropped to dwell misalignment and the certBag."""
+    ev = run_dpp_gas_comeback_enterprise(dev, params,
+                                         101)  # DPP_TEST_GAS_COMEBACK_NO_ACK
+    if "DPP-CONF-FAILED" in ev:
+        raise Exception("Enrollee did not recover from a lost comeback ACK")
-- 
2.39.5



From j at w1.fi  Thu Aug  6 02:36:45 2026
From: j at w1.fi (Jouni Malinen)
Date: Thu, 6 Aug 2026 12:36:45 +0300
Subject: [PATCH] supplicant: schedule recovery timeout when P2P listen
 fails
In-Reply-To: <CAG=E8CG_0yVHDZXaqt8kxjpQo35zRH-SjdhjOqrrdiJ6CSc0tQ@mail.gmail.com>
References: <CAG=E8CGkgwH3sFVAV6SYe=PLcnp+7EbpV=TzbGGVzf0CMBb9eA@mail.gmail.com>
 <CAG=E8CG_0yVHDZXaqt8kxjpQo35zRH-SjdhjOqrrdiJ6CSc0tQ@mail.gmail.com>
Message-ID: <anRVrWt2knGA_QhL@w1.fi>

On Tue, Aug 04, 2026 at 03:43:18PM +0800, Junyu Lai wrote:
> When P2P remain-on-channel (listen) request fails with EBUSY due
> to concurrent wlan0 startup scans, the supplicant state machine
> clears the pending frequency but fails to schedule a recovery
> timeout. As a result, it hangs permanently in the SEARCH state.
> 
> Fix this by scheduling a 100 ms recovery timeout when the listen
> request fails in p2p_listen_failed, allowing wpa_supplicant to
> automatically retry p2p_search() once the driver collision settles.
> 
> This issue is reproducible on devices where simultaneous scans on
> concurrent interfaces (e.g. station and P2P) are disabled or
> restricted in the wireless driver configuration, leading to EBUSY
> failures when concurrent active scans are attempted.
> 
> The scheduled 100 ms recovery timeout is a state timeout
> (p2p_state_timeout) and does not affect the global find timeout
> (p2p_find_timeout) registered when p2p_find was initiated. The
> global timeout remains active and will still fire to stop the
> search when the overall discovery duration expires, ensuring the
> supplicant still follows the discovery time constraint.

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Thu Aug  6 02:42:47 2026
From: j at w1.fi (Jouni Malinen)
Date: Thu, 6 Aug 2026 12:42:47 +0300
Subject: [PATCH v13 0/5] Introduce Automated Frequency Coordination (AFC)
 support
In-Reply-To: <20260804063437.646306-1-allen.ye@mediatek.com>
References: <20260804063437.646306-1-allen.ye@mediatek.com>
Message-ID: <anRXF7cV2YBycIuu@w1.fi>

On Tue, Aug 04, 2026 at 02:34:32PM +0800, Allen Ye wrote:
> This patch series introduces Automated Frequency Coordination (AFC) support
> for Standard Power Devices (SPDs) operating in the 6GHz UNII-5 and UNII-7
> bands. AFC is a regulatory requirement for SPDs to determine a list of
> available channels and their permissible power levels.
> 
> The implementation introduces a new standalone daemon, afcd, to manage
> communication with the AFC server. A client within hostapd then interacts
> with afcd via a Unix socket to fetch spectrum availability and update AP
> parameters accordingly. The series also includes hwsim tests for validation.

> Allen Ye (3):
>   hostapd: Fix ht40_plus_minus_allowed secondary channel selection
>   hostapd: Support 80/160/320 MHz widths in hostapd_is_usable_chans
>   tests: Add AFC hwsim tests
> 
> Lorenzo Bianconi (2):
>   AFC: Add AFC client support
>   AFC: Update TPE IE according to AFC

Thanks, I applied this with cleanup and fixes to make progress. Please
note that I change the encoding of the afc_freq_range configuration
parameter to use min-max for the ranges so that this is consistent with
other cases of frequency ranges and can use existing parsing functions
for that.

There is one significant issue in the current design that needs to be
addressed, but I did not want to push this out to yet another review
round at this point.

hostapd_afc_send_receive() uses a synchronous operation to send the
request and receive the response. That can block the hostapd process for
a significant amount of time while there could be multiple other BSSs
operating and those operations could be impacted. In other words, that
design should be changed to use asynchronous operations where the
request is sent out first and instead of the blocking wait for the
response within this functions, an eloop socket reader should be
registered to wait for the response to become available. The response
would then be processed from the eloop callback (which might need to
schedule additional callbacks if the full TCP response is not available
in one recv() call).

-- 
Jouni Malinen                                            PGP id EFC895FA


From xiayucheng at google.com  Thu Aug  6 08:13:49 2026
From: xiayucheng at google.com (Emily Xia)
Date: Thu,  6 Aug 2026 15:13:49 +0000
Subject: [PATCH] PR: Fix PASN responder ROC duration, MAC filtering and
 termination notification
Message-ID: <20260806151349.1134998-1-xiayucheng@google.com>

- Increase PR_PASN_RESPONDER_ROC_DURATION from 5s to 10s to allow
  sufficient time for PASN responder authentication and ranging setup.
- Notify upper layers with PR_SESSION_END_NEG_FAILED in
  wpas_pr_pasn_roc_start_cb when driver remain-on-channel fails to start
  for responder role.
- Copy pr_responder_src_addr to radio work src_addr in
  wpas_pr_schedule_responder_roc to preserve custom MAC filter address
  when rescheduling subsequent ROC chunks.

Signed-off-by: Emily Xia <xiayucheng at google.com>
---
 wpa_supplicant/pr_supplicant.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/wpa_supplicant/pr_supplicant.c b/wpa_supplicant/pr_supplicant.c
index abfc85be1..60eb6da85 100644
--- a/wpa_supplicant/pr_supplicant.c
+++ b/wpa_supplicant/pr_supplicant.c
@@ -27,7 +27,7 @@ static void wpas_pr_pasn_auth_work_done(struct wpa_supplicant *wpa_s);
 static void wpas_pr_pasn_auth_retry_timeout(void *eloop_ctx, void *timeout_ctx);
 
 /* Total listen window (ms) for the PASN responder ROC */
-#define PR_PASN_RESPONDER_ROC_DURATION 5000
+#define PR_PASN_RESPONDER_ROC_DURATION 10000
 /* Initiator PASN authentication timeout (s) */
 #define PR_PASN_AUTH_TIMEOUT           10
 /* Retry interval (ms) for unacked PASN Authentication frame 1 */
@@ -1140,6 +1140,7 @@ static void wpas_pr_pasn_roc_start_cb(struct wpa_radio_work *work, int deinit)
 			   "PR PASN: Failed to start ROC for responder");
 		wpas_pr_pasn_roc_work_done(wpa_s);
 		wpas_pr_pasn_abort_responder(wpa_s);
+		wpas_notify_pr_ranging_terminated(wpa_s, PR_SESSION_END_NEG_FAILED);
 		return;
 	}
 
@@ -1163,6 +1164,10 @@ static void wpas_pr_schedule_responder_roc(struct wpa_supplicant *wpa_s,
 	}
 	rwork->freq = freq;
 
+	if (wpa_s->pr_responder_mode && !is_zero_ether_addr(wpa_s->pr_responder_src_addr)) {
+		os_memcpy(rwork->src_addr, wpa_s->pr_responder_src_addr, ETH_ALEN);
+	}
+
 	if (!radio_add_work(wpa_s, freq, "pr-pasn-roc", 0,
 			    wpas_pr_pasn_roc_start_cb, rwork)) {
 		wpa_printf(MSG_INFO, "PR PASN: Failed to reschedule ROC");
-- 
2.55.0.629.g250fe7f194-goog



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:41 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:41 +0530
Subject: [PATCH 0/6] hostapd: handle nl80211 no_sta flag in mgmt frames
Message-ID: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>

Current hostapd design assumes that a STA entry already exists in the
driver when management frames receive with the source address translated
to the MLD address.

In some roaming and steering scenarios, an associated STA can reuse
the same MLD address with a different link address. hostapd cannot
find whether such a frame came from the real client or a spoofed
source when it sees only the MLD address, which can break SAE and
(re)association flows for MLD STAs, especially after SA Query timeout
or when the link address changes.

To address this, nl80211 CMD FRAME is extended with a new attribute
NL80211_ATTR_FRAME_CMD_NO_STA that classifies authentication and
association frames as coming from a unauthorized STA. hostapd uses
this hint to handle these frames without requiring a pre?existing STA
entry and to allow the station to reconnect cleanly after the SA Query
procedure has completed.

The new hwsim testcase eht_mld_sae_three_links_legacy_no_sta in
tests/hwsim/test_sta_reconnect_no_sta.py simulates an MLD AP where
the first station connects on two links using M1 as the MLD address
and L1 and L2 as link addresses, and a second station then tries
to connect using the same MLD address but different link addresses.
This provides a base to extend hwsim coverage for other no_sta
handling scenarios.

Note:
This patchset, validated with the following driver-dependent patches,
[PATCH wireless-next 0/8] Adding NO_STA flag and reworking RX link resolution

Ramasamy Kaliappan (6):
  nl80211: Add support for NL80211_ATTR_FRAME_CMD_NO_STA attribute
  hostapd: Handle no_sta flag in auth
  hostapd: Handle no_sta flag in association
  hostapd: Allow STA association after SA Query timeout
  hostapd: Add debug ack status for tx status
  hwsim: Add hwsim test case for no_sta handling

 src/ap/ap_drv_ops.c                |  22 +++-
 src/ap/ap_drv_ops.h                |   4 +
 src/ap/drv_callbacks.c             |   1 +
 src/ap/hostapd.h                   |   2 +
 src/ap/ieee802_11.c                | 197 ++++++++++++++++++++++-------
 src/ap/sta_info.c                  |   4 +-
 src/ap/sta_info.h                  |   6 +
 src/drivers/driver.h               |   9 +-
 src/drivers/driver_nl80211.c       |  33 +++--
 src/drivers/driver_nl80211_event.c |  27 ++--
 tests/hwsim/test_eht.py            |  63 +++++++++
 wpa_supplicant/driver_i.h          |   2 +-
 wpa_supplicant/mesh_mpm.c          |   1 +
 13 files changed, 295 insertions(+), 76 deletions(-)


base-commit: 2328ccef7ef12b4ea226d3aa91cdb9c680711045
prerequisite-patch-id: 0d057d961b216c134a5f0547b8b29797a4d4bd71
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:42 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:42 +0530
Subject: [PATCH 1/6] nl80211: Add support for NL80211_ATTR_FRAME_CMD_NO_STA attribute
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-2-ramasamy.kaliappan@oss.qualcomm.com>

Add support for the new NL80211_ATTR_FRAME_CMD_NO_STA attribute in
nl80211 driver handling. This attribute indicates that the kernel
did not have a station entry for a received management frame or that
frame transmission should avoid using an existing station entry.

Expose this information to upper layers using the new
rx_mgmt.no_sta flag, allowing hostapd to determine whether
MLD address translation was applied or whether it should
be disabled when transmitting a frame.

Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 src/ap/drv_callbacks.c             |  1 +
 src/ap/hostapd.h                   |  1 +
 src/drivers/driver.h               |  5 +++++
 src/drivers/driver_nl80211_event.c | 27 ++++++++++++++++++---------
 4 files changed, 25 insertions(+), 9 deletions(-)

diff --git a/src/ap/drv_callbacks.c b/src/ap/drv_callbacks.c
index 116a055033a0..2c59a662d908 100644
--- a/src/ap/drv_callbacks.c
+++ b/src/ap/drv_callbacks.c
@@ -2025,6 +2025,7 @@ static int hostapd_mgmt_rx(struct hostapd_data *hapd, struct rx_mgmt *rx_mgmt)
 	fi.freq = rx_mgmt->freq;
 	fi.datarate = rx_mgmt->datarate;
 	fi.ssi_signal = rx_mgmt->ssi_signal;
+	fi.no_sta = rx_mgmt->no_sta;
 
 	if (hapd == HAPD_BROADCAST) {
 		size_t i;
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index 974964d341e9..a0f569e8417e 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -132,6 +132,7 @@ struct hostapd_frame_info {
 	u32 channel;
 	u32 datarate;
 	int ssi_signal; /* dBm */
+	bool no_sta;
 };
 
 enum wps_status {
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index e790120467be..7284d863d2e4 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -7272,6 +7272,11 @@ union wpa_event_data {
 		 * non MLD.
 		 */
 		int link_id;
+		/**
+		 * no_sta - set when the kernel has no corresponding station entry
+		 * for the received frame.
+		 */
+		bool no_sta;
 	} rx_mgmt;
 
 	/**
diff --git a/src/drivers/driver_nl80211_event.c b/src/drivers/driver_nl80211_event.c
index 0b111bf66bda..9204756f9251 100644
--- a/src/drivers/driver_nl80211_event.c
+++ b/src/drivers/driver_nl80211_event.c
@@ -1428,7 +1428,8 @@ static void mlme_timeout_event(struct wpa_driver_nl80211_data *drv,
 static void mlme_event_mgmt(struct i802_bss *bss,
 			    struct nlattr *freq, struct nlattr *sig,
 			    const u8 *frame, size_t len,
-			    int link_id)
+			    int link_id,
+			    bool no_sta)
 {
 	struct wpa_driver_nl80211_data *drv = bss->drv;
 	const struct ieee80211_mgmt *mgmt;
@@ -1457,17 +1458,19 @@ static void mlme_event_mgmt(struct i802_bss *bss,
 	}
 	wpa_printf(MSG_DEBUG,
 		   "nl80211: RX frame da=" MACSTR " sa=" MACSTR " bssid=" MACSTR
-		   " freq=%d ssi_signal=%d fc=0x%x seq_ctrl=0x%x stype=%u (%s) len=%u",
+		   " freq=%d ssi_signal=%d fc=0x%x seq_ctrl=0x%x stype=%u (%s) len=%u"
+		   " no_sta=%d",
 		   MAC2STR(mgmt->da), MAC2STR(mgmt->sa), MAC2STR(mgmt->bssid),
 		   rx_freq, ssi_signal, fc,
 		   le_to_host16(mgmt->seq_ctrl), stype, fc2str(fc),
-		   (unsigned int) len);
+		   (unsigned int) len, no_sta);
 	event.rx_mgmt.frame = frame;
 	event.rx_mgmt.frame_len = len;
 	event.rx_mgmt.ssi_signal = ssi_signal;
 	event.rx_mgmt.drv_priv = bss;
 	event.rx_mgmt.ctx = bss->ctx;
 	event.rx_mgmt.link_id = link_id;
+	event.rx_mgmt.no_sta = no_sta;
 
 	wpa_supplicant_event(drv->ctx, EVENT_RX_MGMT, &event);
 }
@@ -1847,7 +1850,8 @@ static void mlme_event(struct i802_bss *bss,
 		       struct nlattr *freq, struct nlattr *ack,
 		       struct nlattr *cookie, struct nlattr *sig,
 		       struct nlattr *wmm, struct nlattr *req_ie,
-		       struct nlattr *link)
+		       struct nlattr *link,
+		       struct nlattr *drv_no_sta)
 {
 	struct wpa_driver_nl80211_data *drv = bss->drv;
 	u16 stype = 0, auth_type = 0;
@@ -1856,6 +1860,7 @@ static void mlme_event(struct i802_bss *bss,
 	int link_id = -1;
 	struct i802_link *mld_link = NULL;
 	const struct ieee80211_mgmt *mgmt = NULL;
+	bool no_sta;
 
 	if (timed_out && addr) {
 		mlme_timeout_event(drv, cmd, addr);
@@ -1879,6 +1884,8 @@ static void mlme_event(struct i802_bss *bss,
 	if (nl80211_link_valid(bss->valid_links, link_id))
 		mld_link = nl80211_get_link(bss, link_id);
 
+	no_sta = nla_get_flag(drv_no_sta);
+
 	data = nla_data(frame);
 	len = nla_len(frame);
 	if (len < 4 + 2 * ETH_ALEN) {
@@ -1889,10 +1896,10 @@ static void mlme_event(struct i802_bss *bss,
 		return;
 	}
 	wpa_printf(MSG_MSGDUMP, "nl80211: MLME event %d (%s) on %s(" MACSTR
-		   ") A1=" MACSTR " A2=" MACSTR " on link_id=%d", cmd,
+		   ") A1=" MACSTR " A2=" MACSTR " on link_id=%d no_sta=%d", cmd,
 		   nl80211_command_to_string(cmd), bss->ifname,
 		   MAC2STR(bss->addr), MAC2STR(data + 4),
-		   MAC2STR(data + 4 + ETH_ALEN), link_id);
+		   MAC2STR(data + 4 + ETH_ALEN), link_id, no_sta);
 
 	/* PASN Authentication frame can be received with a different source MAC
 	 * address. Allow NL80211_CMD_FRAME event with foreign addresses also.
@@ -1962,7 +1969,7 @@ static void mlme_event(struct i802_bss *bss,
 		break;
 	case NL80211_CMD_FRAME:
 		mlme_event_mgmt(bss, freq, sig, nla_data(frame),
-				nla_len(frame), link_id);
+				nla_len(frame), link_id, no_sta);
 		break;
 	case NL80211_CMD_FRAME_TX_STATUS:
 		mlme_event_mgmt_tx_status(bss, cookie, nla_data(frame),
@@ -4940,7 +4947,8 @@ static void do_process_drv_event(struct i802_bss *bss, int cmd,
 			   tb[NL80211_ATTR_RX_SIGNAL_DBM],
 			   tb[NL80211_ATTR_STA_WME],
 			   tb[NL80211_ATTR_REQ_IE],
-			   tb[NL80211_ATTR_MLO_LINK_ID]);
+			   tb[NL80211_ATTR_MLO_LINK_ID],
+			   tb[NL80211_ATTR_FRAME_CMD_NO_STA]);
 		break;
 	case NL80211_CMD_CONNECT:
 	case NL80211_CMD_ROAM:
@@ -5332,7 +5340,8 @@ int process_bss_event(struct nl_msg *msg, void *arg)
 			   tb[NL80211_ATTR_COOKIE],
 			   tb[NL80211_ATTR_RX_SIGNAL_DBM],
 			   tb[NL80211_ATTR_STA_WME], NULL,
-			   tb[NL80211_ATTR_MLO_LINK_ID]);
+			   tb[NL80211_ATTR_MLO_LINK_ID],
+			   tb[NL80211_ATTR_FRAME_CMD_NO_STA]);
 		break;
 	case NL80211_CMD_UNEXPECTED_FRAME:
 		nl80211_spurious_frame(bss, tb, 0);
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:44 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:44 +0530
Subject: [PATCH 3/6] hostapd: Handle no_sta flag in association
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-4-ramasamy.kaliappan@oss.qualcomm.com>

When handling association frames, no_sta flag is used to determine
whether MLD address translation occurred, and the association
response is sent to the corresponding MLD address. Otherwise,
the response is sent to the source address of the received
frame.

Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 src/ap/ieee802_11.c | 37 ++++++++++++++++++++++++++++---------
 1 file changed, 28 insertions(+), 9 deletions(-)

diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 3d46a37ca27c..45e1386d7a11 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -6733,8 +6733,13 @@ static u16 send_assoc_resp(struct hostapd_data *hapd, struct sta_info *sta,
 	struct ieee80211_mgmt *reply;
 	u8 *p;
 	u16 res = WLAN_STATUS_SUCCESS;
+	bool drv_no_sta = false;
 
 	buflen = sizeof(struct ieee80211_mgmt) + 1024;
+
+	if (sta && sta->no_sta)
+		drv_no_sta = sta->no_sta;
+
 #ifdef CONFIG_FILS
 	if (sta && sta->fils_hlp_resp)
 		buflen += wpabuf_len(sta->fils_hlp_resp);
@@ -7155,13 +7160,18 @@ rsnxe_done:
 	}
 #endif /* CONFIG_FILS */
 
-	if (hostapd_drv_send_mlme(hapd, reply, send_len, 0, NULL, 0, 0) < 0) {
+	if (hostapd_drv_send_mgmt_resp(hapd, reply, send_len, 0, NULL, 0, 0,
+				       drv_no_sta) < 0) {
 		wpa_printf(MSG_INFO, "Failed to send assoc resp: %s",
 			   strerror(errno));
 		res = WLAN_STATUS_UNSPECIFIED_FAILURE;
 	}
 
 done:
+	if (sta && sta->no_sta)
+		/* Reset no_sta flag after sending the response */
+		sta->no_sta = false;
+
 	os_free(buf);
 	return res;
 }
@@ -7321,7 +7331,7 @@ static struct sta_info * handle_mlo_translate(struct hostapd_data *hapd,
 
 static void handle_assoc(struct hostapd_data *hapd,
 			 const struct ieee80211_mgmt *mgmt, size_t len,
-			 int reassoc, int rssi)
+			 int reassoc, int rssi, bool drv_no_sta)
 {
 	u16 capab_info, listen_interval, seq_ctrl, fc;
 	int resp = WLAN_STATUS_SUCCESS;
@@ -7336,6 +7346,8 @@ static void handle_assoc(struct hostapd_data *hapd,
 	int omit_rsnxe = 0;
 	bool set_beacon = false;
 	bool mld_addrs_not_translated = false;
+	bool no_sta = drv_no_sta;
+	const u8 *dst = mgmt->sa;
 
 	if (len < IEEE80211_HDRLEN + (reassoc ? sizeof(mgmt->u.reassoc_req) :
 				      sizeof(mgmt->u.assoc_req))) {
@@ -7373,10 +7385,11 @@ static void handle_assoc(struct hostapd_data *hapd,
 			mgmt->u.reassoc_req.listen_interval);
 		wpa_printf(MSG_DEBUG, "reassociation request: STA=" MACSTR
 			   " capab_info=0x%02x listen_interval=%d current_ap="
-			   MACSTR " seq_ctrl=0x%x%s",
+			   MACSTR " seq_ctrl=0x%x%s no_sta=%d",
 			   MAC2STR(mgmt->sa), capab_info, listen_interval,
 			   MAC2STR(mgmt->u.reassoc_req.current_ap),
-			   seq_ctrl, (fc & WLAN_FC_RETRY) ? " retry" : "");
+			   seq_ctrl, (fc & WLAN_FC_RETRY) ? " retry" : "",
+			   no_sta);
 		left = len - (IEEE80211_HDRLEN + sizeof(mgmt->u.reassoc_req));
 		pos = mgmt->u.reassoc_req.variable;
 	} else {
@@ -7385,9 +7398,10 @@ static void handle_assoc(struct hostapd_data *hapd,
 			mgmt->u.assoc_req.listen_interval);
 		wpa_printf(MSG_DEBUG, "association request: STA=" MACSTR
 			   " capab_info=0x%02x listen_interval=%d "
-			   "seq_ctrl=0x%x%s",
+			   " seq_ctrl=0x%x%s no_sta=%d",
 			   MAC2STR(mgmt->sa), capab_info, listen_interval,
-			   seq_ctrl, (fc & WLAN_FC_RETRY) ? " retry" : "");
+			   seq_ctrl, (fc & WLAN_FC_RETRY) ? " retry" : "",
+			   no_sta);
 		left = len - (IEEE80211_HDRLEN + sizeof(mgmt->u.assoc_req));
 		pos = mgmt->u.assoc_req.variable;
 	}
@@ -7501,6 +7515,9 @@ static void handle_assoc(struct hostapd_data *hapd,
 		}
 	}
 
+	if (!no_sta)
+		dst = sta->addr;
+
 	if ((fc & WLAN_FC_RETRY) &&
 	    sta->last_seq_ctrl != WLAN_INVALID_MGMT_SEQ &&
 	    sta->last_seq_ctrl == seq_ctrl &&
@@ -7768,11 +7785,13 @@ static void handle_assoc(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_TESTING_OPTIONS */
 
+	sta->no_sta = no_sta;
+
 	if (resp >= 0)
 		reply_res = send_assoc_resp(hapd,
 					    mld_addrs_not_translated ?
 					    NULL : sta,
-					    mgmt->sa, resp, reassoc,
+					    dst, resp, reassoc,
 					    pos, left, rssi, omit_rsnxe);
 
 	if (set_beacon)
@@ -8429,12 +8448,12 @@ int ieee802_11_mgmt(struct hostapd_data *hapd, const u8 *buf, size_t len,
 		break;
 	case WLAN_FC_STYPE_ASSOC_REQ:
 		wpa_printf(MSG_DEBUG, "mgmt::assoc_req");
-		handle_assoc(hapd, mgmt, len, 0, ssi_signal);
+		handle_assoc(hapd, mgmt, len, 0, ssi_signal, no_sta);
 		ret = 1;
 		break;
 	case WLAN_FC_STYPE_REASSOC_REQ:
 		wpa_printf(MSG_DEBUG, "mgmt::reassoc_req");
-		handle_assoc(hapd, mgmt, len, 1, ssi_signal);
+		handle_assoc(hapd, mgmt, len, 1, ssi_signal, no_sta);
 		ret = 1;
 		break;
 	case WLAN_FC_STYPE_DISASSOC:
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:43 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:43 +0530
Subject: [PATCH 2/6] hostapd: Handle no_sta flag in auth
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-3-ramasamy.kaliappan@oss.qualcomm.com>

Use the driver-provided no_sta indication on received authentication
frames to determine the correct reply address and whether replies
should be sent to the MLD address or to the source address.

The no_sta flag is set when the kernel has no station entry for the
received frame or when a frame should be transmitted without using a
known station entry.

When handling authentication frames, this flag is used to determine
whether MLD address translation was applied and to ensure that responses
are sent to the corresponding MLD address. Otherwise,
the response is sent to the source address of the received frame.

Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 src/ap/ap_drv_ops.c          | 22 +++++++++-
 src/ap/ap_drv_ops.h          |  4 ++
 src/ap/hostapd.h             |  1 +
 src/ap/ieee802_11.c          | 79 ++++++++++++++++++++----------------
 src/ap/sta_info.h            |  4 ++
 src/drivers/driver.h         |  4 +-
 src/drivers/driver_nl80211.c | 33 ++++++++-------
 wpa_supplicant/driver_i.h    |  2 +-
 wpa_supplicant/mesh_mpm.c    |  1 +
 9 files changed, 99 insertions(+), 51 deletions(-)

diff --git a/src/ap/ap_drv_ops.c b/src/ap/ap_drv_ops.c
index 19a264ce7867..83a7573cc55f 100644
--- a/src/ap/ap_drv_ops.c
+++ b/src/ap/ap_drv_ops.c
@@ -889,6 +889,26 @@ int hostapd_drv_set_key(const char *ifname, struct hostapd_data *hapd,
 }
 
 
+int hostapd_drv_send_mgmt_resp(struct hostapd_data *hapd,
+			  const void *msg, size_t len, int noack,
+			  const u16 *csa_offs, size_t csa_offs_len,
+			  int no_encrypt, bool drv_no_sta)
+{
+	int link_id = -1;
+
+#ifdef CONFIG_IEEE80211BE
+	if (hapd->conf->mld_ap)
+		link_id = hapd->mld_link_id;
+#endif /* CONFIG_IEEE80211BE */
+
+	if (!hapd->driver || !hapd->driver->send_mlme || !hapd->drv_priv)
+		return 0;
+	return hapd->driver->send_mlme(hapd->drv_priv, msg, len, noack, 0,
+				       csa_offs, csa_offs_len, no_encrypt, 0,
+				       link_id, drv_no_sta);
+}
+
+
 int hostapd_drv_send_mlme(struct hostapd_data *hapd,
 			  const void *msg, size_t len, int noack,
 			  const u16 *csa_offs, size_t csa_offs_len,
@@ -905,7 +925,7 @@ int hostapd_drv_send_mlme(struct hostapd_data *hapd,
 		return 0;
 	return hapd->driver->send_mlme(hapd->drv_priv, msg, len, noack, 0,
 				       csa_offs, csa_offs_len, no_encrypt, 0,
-				       link_id);
+				       link_id, false);
 }
 
 
diff --git a/src/ap/ap_drv_ops.h b/src/ap/ap_drv_ops.h
index f09b3ad9a306..5684ed9e4b18 100644
--- a/src/ap/ap_drv_ops.h
+++ b/src/ap/ap_drv_ops.h
@@ -103,6 +103,10 @@ int hostapd_drv_set_key(const char *ifname,
 			int key_idx, int vlan_id, int set_tx,
 			const u8 *seq, size_t seq_len,
 			const u8 *key, size_t key_len, enum key_flag key_flag);
+int hostapd_drv_send_mgmt_resp(struct hostapd_data *hapd,
+			       const void *msg, size_t len, int noack,
+			       const u16 *csa_offs, size_t csa_offs_len,
+			       int no_encrypt, bool drv_no_sta);
 int hostapd_drv_send_mlme(struct hostapd_data *hapd,
 			  const void *msg, size_t len, int noack,
 			  const u16 *csa_offs, size_t csa_offs_len,
diff --git a/src/ap/hostapd.h b/src/ap/hostapd.h
index a0f569e8417e..736a58028fe1 100644
--- a/src/ap/hostapd.h
+++ b/src/ap/hostapd.h
@@ -171,6 +171,7 @@ struct hostapd_neighbor_entry {
 struct hostapd_sae_commit_queue {
 	struct dl_list list;
 	int rssi;
+	bool no_sta;
 	size_t len;
 	u8 msg[];
 };
diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index fc78c3bd6721..3d46a37ca27c 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -88,7 +88,7 @@ static void pasn_fils_auth_resp(struct hostapd_data *hapd,
 
 static void handle_auth(struct hostapd_data *hapd,
 			const struct ieee80211_mgmt *mgmt, size_t len,
-			int rssi, int from_queue);
+			int rssi, int from_queue, bool no_sta);
 static int add_associated_sta(struct hostapd_data *hapd,
 			      struct sta_info *sta, int reassoc);
 #ifdef CONFIG_IEEE8021X_AUTH
@@ -451,10 +451,10 @@ static int send_auth_reply(struct hostapd_data *hapd, struct sta_info *sta,
 		    sta && sta->sae_postponed_commit) {
 			wpa_printf(MSG_DEBUG,
 				   "TESTING: Send postponed SAE Commit first, immediately followed by SAE Confirm");
-			if (hostapd_drv_send_mlme(hapd,
-						  sta->sae_postponed_commit,
-						  sta->sae_postponed_commit_len,
-						  0, NULL, 0, 0) < 0)
+			if (hostapd_drv_send_mgmt_resp(hapd,
+						       sta->sae_postponed_commit,
+						       sta->sae_postponed_commit_len,
+						       0, NULL, 0, 0, sta->no_sta) < 0)
 				wpa_printf(MSG_INFO, "send_auth_reply: send failed");
 			os_free(sta->sae_postponed_commit);
 			sta->sae_postponed_commit = NULL;
@@ -515,12 +515,16 @@ static int send_auth_reply(struct hostapd_data *hapd, struct sta_info *sta,
 		os_memcpy(ptr + 2, mic, mic_len);
 	}
 #endif /* CONFIF_IEEE8021X_AUTH */
-
-	if (hostapd_drv_send_mlme(hapd, reply, rlen, 0, NULL, 0, 0) < 0)
+	if (hostapd_drv_send_mgmt_resp(hapd, reply, rlen,
+				       0, NULL, 0, 0,
+				       sta ? sta->no_sta : false) < 0)
 		wpa_printf(MSG_INFO, "send_auth_reply: send failed");
 	else
 		reply_res = WLAN_STATUS_SUCCESS;
 
+	if (sta)
+		sta->no_sta = false;
+
 	os_free(buf);
 
 	return reply_res;
@@ -556,11 +560,12 @@ static void handle_auth_ft_finish(void *ctx, const u8 *dst,
 	struct sta_info *sta;
 	int reply_res;
 
-	reply_res = send_auth_reply(hapd, NULL, dst, WLAN_AUTH_FT,
+	sta = ap_get_sta(hapd, dst);
+	reply_res = send_auth_reply(hapd, NULL,
+				    sta ? sta->auth_reply_addr : dst,
+				    WLAN_AUTH_FT,
 				    auth_transaction, status, ies, ies_len,
 				    "auth-ft-finish");
-
-	sta = ap_get_sta(hapd, dst);
 	if (sta == NULL)
 		return;
 
@@ -1098,7 +1103,7 @@ static int auth_sae_send_commit(struct hostapd_data *hapd,
 		status = hapd->conf->sae_commit_status;
 	}
 #endif /* CONFIG_TESTING_OPTIONS */
-	reply_res = send_auth_reply(hapd, sta, sta->addr,
+	reply_res = send_auth_reply(hapd, sta, sta->auth_reply_addr,
 				    WLAN_AUTH_SAE, 1,
 				    status, wpabuf_head(data),
 				    wpabuf_len(data), "sae-send-commit");
@@ -1119,7 +1124,7 @@ static int auth_sae_send_confirm(struct hostapd_data *hapd,
 	if (data == NULL)
 		return WLAN_STATUS_UNSPECIFIED_FAILURE;
 
-	reply_res = send_auth_reply(hapd, sta, sta->addr,
+	reply_res = send_auth_reply(hapd, sta, sta->auth_reply_addr,
 				    WLAN_AUTH_SAE, 2,
 				    WLAN_STATUS_SUCCESS, wpabuf_head(data),
 				    wpabuf_len(data), "sae-send-confirm");
@@ -1754,6 +1759,10 @@ static void handle_auth_sae(struct hostapd_data *hapd, struct sta_info *sta,
 	const u8 *pos, *end;
 	int sta_removed = 0;
 	bool success_status;
+	const u8 *dst = sta->auth_reply_addr;
+
+	if (sta && sta->no_sta)
+		dst = mgmt->sa;
 
 	if (!groups) {
 		groups = default_groups;
@@ -1770,7 +1779,7 @@ static void handle_auth_sae(struct hostapd_data *hapd, struct sta_info *sta,
 		pos = mgmt->u.auth.variable;
 		end = ((const u8 *) mgmt) + len;
 		resp = status_code;
-		send_auth_reply(hapd, sta, sta->addr,
+		send_auth_reply(hapd, sta, dst,
 				WLAN_AUTH_SAE,
 				auth_transaction, resp, pos, end - pos,
 				"auth-sae-reflection-attack");
@@ -1780,7 +1789,7 @@ static void handle_auth_sae(struct hostapd_data *hapd, struct sta_info *sta,
 	if (hapd->conf->sae_commit_override &&
 	    auth_transaction == WLAN_AUTH_TR_SEQ_SAE_COMMIT) {
 		wpa_printf(MSG_DEBUG, "SAE: TESTING - commit override");
-		send_auth_reply(hapd, sta, sta->addr,
+		send_auth_reply(hapd, sta, dst,
 				WLAN_AUTH_SAE,
 				auth_transaction, resp,
 				wpabuf_head(hapd->conf->sae_commit_override),
@@ -2074,7 +2083,7 @@ reply:
 		    !data && end - pos >= 2)
 			data = wpabuf_alloc_copy(pos, 2);
 
-		send_auth_reply(hapd, sta, sta->addr,
+		send_auth_reply(hapd, sta, dst,
 				WLAN_AUTH_SAE,
 				auth_transaction, resp,
 				data ? wpabuf_head(data) : (u8 *) "",
@@ -2142,7 +2151,7 @@ void auth_sae_process_commit(void *eloop_ctx, void *user_ctx)
 		   "SAE: Process next available message from queue");
 	dl_list_del(&q->list);
 	handle_auth(hapd, (const struct ieee80211_mgmt *) q->msg, q->len,
-		    q->rssi, 1);
+		    q->rssi, 1, q->no_sta);
 	os_free(q);
 
 	if (eloop_is_timeout_registered(auth_sae_process_commit, hapd, NULL))
@@ -2155,7 +2164,7 @@ void auth_sae_process_commit(void *eloop_ctx, void *user_ctx)
 
 static void auth_sae_queue(struct hostapd_data *hapd,
 			   const struct ieee80211_mgmt *mgmt, size_t len,
-			   int rssi)
+			   int rssi, bool no_sta)
 {
 	struct hostapd_sae_commit_queue *q, *q2;
 	unsigned int queue_len;
@@ -2177,6 +2186,7 @@ static void auth_sae_queue(struct hostapd_data *hapd,
 		return;
 	q->rssi = rssi;
 	q->len = len;
+	q->no_sta = no_sta;
 	os_memcpy(q->msg, mgmt, len);
 
 	/* Check whether there is already a queued Authentication frame from the
@@ -3348,7 +3358,7 @@ static void handle_auth_fils_finish(struct hostapd_data *hapd,
 	auth_alg = (pub ||
 		    resp == WLAN_STATUS_FINITE_CYCLIC_GROUP_NOT_SUPPORTED) ?
 		WLAN_AUTH_FILS_SK_PFS : WLAN_AUTH_FILS_SK;
-	send_auth_reply(hapd, sta, sta->addr, auth_alg, 2, resp,
+	send_auth_reply(hapd, sta, sta->auth_reply_addr, auth_alg, 2, resp,
 			data ? wpabuf_head(data) : (u8 *) "",
 			data ? wpabuf_len(data) : 0, "auth-fils-finish");
 	wpabuf_free(data);
@@ -4223,7 +4233,7 @@ static void handle_auth_pasn(struct hostapd_data *hapd, struct sta_info *sta,
 
 static void handle_auth(struct hostapd_data *hapd,
 			const struct ieee80211_mgmt *mgmt, size_t len,
-			int rssi, int from_queue)
+			int rssi, int from_queue, bool frame_no_sta)
 {
 	u16 auth_alg, auth_transaction, status_code;
 	u16 resp = WLAN_STATUS_SUCCESS;
@@ -4236,6 +4246,7 @@ static void handle_auth(struct hostapd_data *hapd,
 	u16 seq_ctrl;
 	struct radius_sta rad_info;
 	const u8 *dst, *sa;
+	const u8 *mld_addr;
 	bool mld_sta = false;
 
 	if (len < IEEE80211_HDRLEN + sizeof(mgmt->u.auth)) {
@@ -4261,11 +4272,11 @@ static void handle_auth(struct hostapd_data *hapd,
 	 * the driver so that the station MLD MAC address would be used in both
 	 * hostapd and the driver.
 	 */
-	sa = hostapd_process_ml_auth(hapd, mgmt, len);
-	if (sa)
+	mld_addr = hostapd_process_ml_auth(hapd, mgmt, len);
+	if (mld_addr) {
 		mld_sta = true;
-	else
-		sa = mgmt->sa;
+		sa = mld_addr;
+	}
 #endif /* CONFIG_IEEE80211BE */
 
 	auth_alg = le_to_host16(mgmt->u.auth.auth_alg);
@@ -4282,12 +4293,12 @@ static void handle_auth(struct hostapd_data *hapd,
 
 	wpa_printf(MSG_DEBUG, "authentication: STA=" MACSTR " auth_alg=%d "
 		   "auth_transaction=%d status_code=%d protected=%d%s "
-		   "seq_ctrl=0x%x%s%s",
+		   "seq_ctrl=0x%x%s%s frame_no_sta=%d",
 		   MAC2STR(sa), auth_alg, auth_transaction,
 		   status_code, !!(fc & WLAN_FC_PROTECTED),
 		   challenge ? " challenge" : "",
 		   seq_ctrl, (fc & WLAN_FC_RETRY) ? " retry" : "",
-		   from_queue ? " (from queue)" : "");
+		   from_queue ? " (from queue)" : "", frame_no_sta);
 
 #ifdef CONFIG_NO_RC4
 	if (auth_alg == WLAN_AUTH_SHARED_KEY) {
@@ -4491,7 +4502,7 @@ static void handle_auth(struct hostapd_data *hapd,
 		 * be a queued commit message from the same peer. This is needed
 		 * to avoid reordering Authentication frames within the same
 		 * SAE exchange. */
-		auth_sae_queue(hapd, mgmt, len, rssi);
+		auth_sae_queue(hapd, mgmt, len, rssi, frame_no_sta);
 		return;
 	}
 #endif /* CONFIG_SAE */
@@ -4599,6 +4610,9 @@ static void handle_auth(struct hostapd_data *hapd,
 #ifdef CONFIG_MBO
 	sta->auth_rssi = rssi;
 #endif /* CONFIG_MBO */
+	os_memcpy(sta->auth_reply_addr,
+		  frame_no_sta ? mgmt->sa : sa, ETH_ALEN);
+	sta->no_sta = frame_no_sta;
 
 	res = ieee802_11_set_radius_info(hapd, sta, res, &rad_info);
 	if (res) {
@@ -4742,14 +4756,10 @@ static void handle_auth(struct hostapd_data *hapd,
 	}
 
  fail:
-	dst = mgmt->sa;
-
-#ifdef CONFIG_IEEE80211BE
-	if (ap_sta_is_mld(hapd, sta))
-		dst = sta->addr;
-#endif /* CONFIG_IEEE80211BE */
+	dst = frame_no_sta ? mgmt->sa : sa;
 
-	reply_res = send_auth_reply(hapd, sta, dst, auth_alg,
+	reply_res = send_auth_reply(hapd, sta, dst,
+				    auth_alg,
 				    auth_alg == WLAN_AUTH_SAE ?
 				    auth_transaction : auth_transaction + 1,
 				    resp, resp_ies, resp_ies_len,
@@ -8322,6 +8332,7 @@ int ieee802_11_mgmt(struct hostapd_data *hapd, const u8 *buf, size_t len,
 	int ret = 0;
 	unsigned int freq;
 	int ssi_signal = fi ? fi->ssi_signal : 0;
+	bool no_sta = fi ? fi->no_sta : false;
 
 	if (len < 24)
 		return 0;
@@ -8413,7 +8424,7 @@ int ieee802_11_mgmt(struct hostapd_data *hapd, const u8 *buf, size_t len,
 	switch (stype) {
 	case WLAN_FC_STYPE_AUTH:
 		wpa_printf(MSG_DEBUG, "mgmt::auth");
-		handle_auth(hapd, mgmt, len, ssi_signal, 0);
+		handle_auth(hapd, mgmt, len, ssi_signal, 0, no_sta);
 		ret = 1;
 		break;
 	case WLAN_FC_STYPE_ASSOC_REQ:
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index b721dcc8cfa4..baac3865b553 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -358,6 +358,10 @@ struct sta_info {
 	struct link_reconf_req_list *reconf_req;
 #endif /* CONFIG_IEEE80211BE */
 
+	u8 auth_reply_addr[ETH_ALEN];
+	/* Last received auth/assoc frame from driver had no_sta flag */
+	bool no_sta;
+
 	u16 max_idle_period; /* if nonzero, the granted BSS max idle period in
 			      * units of 1000 TUs */
 
diff --git a/src/drivers/driver.h b/src/drivers/driver.h
index 7284d863d2e4..0480664dad7e 100644
--- a/src/drivers/driver.h
+++ b/src/drivers/driver.h
@@ -3944,12 +3944,14 @@ struct wpa_driver_ops {
 	 *	(used only for testing purposes)
 	 * @wait: Time to wait off-channel for a response (in ms), or zero
 	 * @link_id: Link ID to use for TX, or -1 if not set
+	 * @no_sta: Valid when the kernel has no corresponding station entry
+	 *	    for the received frame
 	 * Returns: 0 on success, -1 on failure
 	 */
 	int (*send_mlme)(void *priv, const u8 *data, size_t data_len,
 			 int noack, unsigned int freq, const u16 *csa_offs,
 			 size_t csa_offs_len, int no_encrypt,
-			 unsigned int wait, int link_id);
+			 unsigned int wait, int link_id, bool no_sta);
 
 	/**
 	 * update_ft_ies - Update FT (IEEE 802.11r) IEs
diff --git a/src/drivers/driver_nl80211.c b/src/drivers/driver_nl80211.c
index 4b70a304fc3d..7b5325153689 100644
--- a/src/drivers/driver_nl80211.c
+++ b/src/drivers/driver_nl80211.c
@@ -173,7 +173,8 @@ static int nl80211_send_frame_cmd(struct i802_bss *bss,
 				  const u8 *buf, size_t buf_len,
 				  int save_cookie, int no_cck, int no_ack,
 				  int offchanok, const u16 *csa_offs,
-				  size_t csa_offs_len, int link_id);
+				  size_t csa_offs_len, int link_id,
+				  bool no_sta);
 static int wpa_driver_nl80211_probe_req_report(struct i802_bss *bss,
 					       int report);
 
@@ -4927,7 +4928,7 @@ static int wpa_driver_nl80211_send_mlme(struct i802_bss *bss, const u8 *data,
 					unsigned int wait_time,
 					const u16 *csa_offs,
 					size_t csa_offs_len, int no_encrypt,
-					int link_id)
+					int link_id, bool no_sta)
 {
 	struct wpa_driver_nl80211_data *drv = bss->drv;
 	struct ieee80211_mgmt *mgmt;
@@ -4943,10 +4944,11 @@ static int wpa_driver_nl80211_send_mlme(struct i802_bss *bss, const u8 *data,
 	fc = le_to_host16(mgmt->frame_control);
 	wpa_printf(MSG_DEBUG, "nl80211: send_mlme - da=" MACSTR " sa=" MACSTR
 		   " bssid=" MACSTR
-		   " noack=%d freq=%u no_cck=%d offchanok=%d wait_time=%u no_encrypt=%d fc=0x%x (%s) nlmode=%d",
+		   " noack=%d freq=%u no_cck=%d offchanok=%d wait_time=%u no_encrypt=%d fc=0x%x (%s) nlmode=%d"
+		   " no_sta=%d",
 		   MAC2STR(mgmt->da), MAC2STR(mgmt->sa), MAC2STR(mgmt->bssid),
 		   noack, freq, no_cck, offchanok, wait_time,
-		   no_encrypt, fc, fc2str(fc), drv->nlmode);
+		   no_encrypt, fc, fc2str(fc), drv->nlmode, no_sta);
 
 #ifdef CONFIG_PR
 	/* Route MLME TX via PD wdev if source address matches */
@@ -5059,7 +5061,7 @@ send_frame_cmd:
 	wpa_printf(MSG_DEBUG, "nl80211: send_mlme -> send_frame_cmd");
 	res = nl80211_send_frame_cmd(bss, freq, wait_time, data, data_len,
 				     use_cookie, no_cck, noack, offchanok,
-				     csa_offs, csa_offs_len, link_id);
+				     csa_offs, csa_offs_len, link_id, no_sta);
 	if (!res)
 		drv->send_frame_link_id = link_id;
 
@@ -9199,7 +9201,7 @@ static int i802_sta_deauth(void *priv, const u8 *own_addr, const u8 *addr,
 	return wpa_driver_nl80211_send_mlme(bss, (u8 *) &mgmt,
 					    IEEE80211_HDRLEN +
 					    sizeof(mgmt.u.deauth), 0, 0, 0, 0,
-					    0, NULL, 0, 0, link_id);
+					    0, NULL, 0, 0, link_id, false);
 }
 
 
@@ -9226,7 +9228,7 @@ static int i802_sta_disassoc(void *priv, const u8 *own_addr, const u8 *addr,
 	return wpa_driver_nl80211_send_mlme(bss, (u8 *) &mgmt,
 					    IEEE80211_HDRLEN +
 					    sizeof(mgmt.u.disassoc), 0, 0, 0, 0,
-					    0, NULL, 0, 0, link_id);
+					    0, NULL, 0, 0, link_id, false);
 }
 
 
@@ -9989,7 +9991,8 @@ static int nl80211_send_frame_cmd(struct i802_bss *bss,
 				  const u8 *buf, size_t buf_len,
 				  int save_cookie, int no_cck, int no_ack,
 				  int offchanok, const u16 *csa_offs,
-				  size_t csa_offs_len, int link_id)
+				  size_t csa_offs_len, int link_id,
+				  bool no_sta)
 {
 	struct wpa_driver_nl80211_data *drv = bss->drv;
 	struct nl_msg *msg;
@@ -10013,7 +10016,8 @@ static int nl80211_send_frame_cmd(struct i802_bss *bss,
 	    (no_ack && nla_put_flag(msg, NL80211_ATTR_DONT_WAIT_FOR_ACK)) ||
 	    (csa_offs && nla_put(msg, NL80211_ATTR_CSA_C_OFFSETS_TX,
 				 csa_offs_len * sizeof(u16), csa_offs)) ||
-	    nla_put(msg, NL80211_ATTR_FRAME, buf_len, buf))
+	    nla_put(msg, NL80211_ATTR_FRAME, buf_len, buf) ||
+	    (no_sta && nla_put_flag(msg, NL80211_ATTR_FRAME_CMD_NO_STA)))
 		goto fail;
 
 	cookie = 0;
@@ -10140,11 +10144,11 @@ static int wpa_driver_nl80211_send_action(struct i802_bss *bss,
 		ret = wpa_driver_nl80211_send_mlme(bss, buf, 24 + data_len,
 						   0, freq, no_cck, offchanok,
 						   wait_time, NULL, 0, 0,
-						   link_id);
+						   link_id, false);
 	else
 		ret = nl80211_send_frame_cmd(bss, freq, wait_time, buf,
 					     24 + data_len, 1, no_cck, 0,
-					     offchanok, NULL, 0, link_id);
+					     offchanok, NULL, 0, link_id, false);
 
 	os_free(buf);
 	return ret;
@@ -11424,7 +11428,7 @@ static void nl80211_send_null_frame(struct i802_bss *bss, const u8 *own_addr,
 	os_memcpy(nulldata.hdr.IEEE80211_SA_FROMDS, own_addr, ETH_ALEN);
 
 	if (wpa_driver_nl80211_send_mlme(bss, (u8 *) &nulldata, size, 0, 0, 0,
-					 0, 0, NULL, 0, 0, -1) < 0)
+					 0, 0, NULL, 0, 0, -1, false) < 0)
 		wpa_printf(MSG_DEBUG, "nl80211_send_null_frame: Failed to "
 			   "send poll frame");
 }
@@ -11924,12 +11928,13 @@ static int driver_nl80211_send_mlme(void *priv, const u8 *data,
 				    unsigned int freq,
 				    const u16 *csa_offs, size_t csa_offs_len,
 				    int no_encrypt, unsigned int wait,
-				    int link_id)
+				    int link_id, bool no_sta)
 {
 	struct i802_bss *bss = priv;
 	return wpa_driver_nl80211_send_mlme(bss, data, data_len, noack,
 					    freq, 0, 0, wait, csa_offs,
-					    csa_offs_len, no_encrypt, link_id);
+					    csa_offs_len, no_encrypt, link_id,
+					    no_sta);
 }
 
 
diff --git a/wpa_supplicant/driver_i.h b/wpa_supplicant/driver_i.h
index 7d8b806a4547..55fceacb1702 100644
--- a/wpa_supplicant/driver_i.h
+++ b/wpa_supplicant/driver_i.h
@@ -341,7 +341,7 @@ static inline int wpa_drv_send_mlme(struct wpa_supplicant *wpa_s,
 	if (wpa_s->driver->send_mlme)
 		return wpa_s->driver->send_mlme(wpa_s->drv_priv,
 						data, data_len, noack,
-						freq, NULL, 0, 0, wait, -1);
+						freq, NULL, 0, 0, wait, -1, false);
 	return -1;
 }
 
diff --git a/wpa_supplicant/mesh_mpm.c b/wpa_supplicant/mesh_mpm.c
index 66684c99c67d..2ed3b9abafe0 100644
--- a/wpa_supplicant/mesh_mpm.c
+++ b/wpa_supplicant/mesh_mpm.c
@@ -800,6 +800,7 @@ static struct sta_info * mesh_mpm_add_peer(struct wpa_supplicant *wpa_s,
 		return NULL;
 	}
 
+	os_memcpy(sta->auth_reply_addr, addr, ETH_ALEN);
 	/* insert into driver */
 	os_memset(&params, 0, sizeof(params));
 	params.supp_rates = sta->supported_rates;
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:45 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:45 +0530
Subject: [PATCH 4/6] hostapd: Allow STA association after SA Query timeout
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-5-ramasamy.kaliappan@oss.qualcomm.com>

When a PMF-protected non-AP MLD STA is already associated using M1 as
its MLD address and L1/L2 as link addresses, it may later send an
association request using the same M1 address but different link
addresses.

In this case, the kernel does not perform MLD address translation and
sets the no_sta flag. The AP MLD responds to the source address with a
temporary rejection (with no_sta set) and starts an SA Query
procedure.

When a subsequent association request is received after the SA Query
comeback time with no_sta set, hostapd removes the existing STA entry
from the driver and re-adds the STA based on the new capabilities and
MLD information from the association request.

The AP MLD then sends an association response with status SUCCESS to the
STA address with no_sta cleared, allowing the reassociation with the
new link addresses to complete.

Co-developed-by: Rameshkumar Sundaram <rameshkumar.sundaram at oss.qualcomm.com>
Signed-off-by: Rameshkumar Sundaram <rameshkumar.sundaram at oss.qualcomm.com>
Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 src/ap/ieee802_11.c | 70 ++++++++++++++++++++++++++++++++++++++++++++-
 src/ap/sta_info.c   |  4 +--
 src/ap/sta_info.h   |  2 ++
 3 files changed, 73 insertions(+), 3 deletions(-)

diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 45e1386d7a11..3b71b5edc311 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -6644,7 +6644,7 @@ static int add_associated_sta(struct hostapd_data *hapd,
 		   wpa_auth_sta_ft_tk_already_set(sta->wpa_sm),
 		   wpa_auth_sta_fils_tk_already_set(sta->wpa_sm));
 
-	if (!mld_link_sta && !sta->added_unassoc &&
+	if (!ap_sta_is_mld(hapd, sta) && !sta->added_unassoc &&
 	    (!(sta->flags & WLAN_STA_AUTHORIZED) ||
 	     (reassoc && sta->ft_over_ds && sta->auth_alg == WLAN_AUTH_FT) ||
 	     (!wpa_auth_sta_ft_tk_already_set(sta->wpa_sm) &&
@@ -7601,6 +7601,74 @@ static void handle_assoc(struct hostapd_data *hapd,
 	}
 #endif /* CONFIG_FILS */
 
+	if ((sta->flags & WLAN_STA_MFP) &&
+	     sta->sa_query_timed_out &&
+	     sta->mld_info.mld_sta) {
+		struct ieee802_11_elems elems;
+		u8 mld_addr[ETH_ALEN] = {0};
+		/*
+		 * Clean up the existing STA info
+		 * allowing association with new sta info based on
+		 * the association request after the SA Query timeout.
+		 */
+		wpa_auth_sta_deinit(sta->wpa_sm);
+		sta->wpa_sm = NULL;
+		clear_wpa_sm_for_each_partner_link(hapd, sta);
+		ap_sta_remove_link_sta(hapd, sta);
+		hostapd_drv_sta_remove(hapd, sta->addr);
+		sta->flags &= ~(WLAN_STA_ASSOC | WLAN_STA_AUTHORIZED);
+		ap_sta_set_sa_query_timeout(hapd, sta, 0);
+
+		if (ieee802_11_parse_elems(pos, left, &elems, 1) == ParseFailed) {
+			wpa_printf(MSG_DEBUG, "Failed to parse elements");
+			goto fail;
+		}
+
+		os_memset(&sta->mld_info, 0x00, sizeof(sta->mld_info));
+
+		if (!hostapd_process_ml_assoc_req_addr(hapd,
+						elems.basic_mle,
+						elems.basic_mle_len,
+						mld_addr)) {
+			u8 link_id = hapd->mld_link_id;
+
+			sta->mld_info.mld_sta = true;
+			sta->mld_assoc_link_id = link_id;
+
+			os_memcpy(sta->mld_info.common_info.mld_addr, mld_addr,
+					ETH_ALEN);
+			os_memcpy(sta->mld_info.links[link_id].peer_addr,
+					mgmt->sa, ETH_ALEN);
+			os_memcpy(sta->mld_info.links[link_id].local_addr,
+					hapd->own_addr, ETH_ALEN);
+		}
+
+		wpa_printf(MSG_DEBUG, "Allowing association of STA "
+				MACSTR " after timed out SA Query procedure",
+				MAC2STR(sta->addr));
+		if (hostapd_sta_add(hapd, sta->addr, 0, 0,
+				    sta->supported_rates,
+				    sta->supported_rates_len,
+				    0, NULL, NULL, NULL, 0, NULL, 0,
+				    NULL, 0, NULL,
+				    sta->flags, 0, 0, 0, 0,
+				    mgmt->sa, 0,
+				    0, 0)) {
+			hostapd_logger(hapd, sta->addr, HOSTAPD_MODULE_IEEE80211,
+				       HOSTAPD_LEVEL_NOTICE,
+				       "Could not add STA to kernel driver");
+			goto fail;
+		}
+		/*
+		 * The station has been added to the driver.
+		 * When sending the (Re)Association response frame,
+		 * reset the no_sta flag and send the response to sta->addr
+		 * instead of the source address.
+		 */
+		no_sta = false;
+		dst = sta->addr;
+	}
+
 	/* followed by SSID and Supported rates; and HT capabilities if 802.11n
 	 * is used */
 	resp = check_assoc_ies(hapd, sta, pos, left,
diff --git a/src/ap/sta_info.c b/src/ap/sta_info.c
index feb1282078ff..e9e33f040c76 100644
--- a/src/ap/sta_info.c
+++ b/src/ap/sta_info.c
@@ -2070,8 +2070,8 @@ int ap_sta_pending_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
 
 
 #ifdef CONFIG_IEEE80211BE
-static void ap_sta_remove_link_sta(struct hostapd_data *hapd,
-				   struct sta_info *sta)
+void ap_sta_remove_link_sta(struct hostapd_data *hapd,
+			    struct sta_info *sta)
 {
 	struct hostapd_data *tmp_hapd;
 
diff --git a/src/ap/sta_info.h b/src/ap/sta_info.h
index baac3865b553..42e4f7f16bce 100644
--- a/src/ap/sta_info.h
+++ b/src/ap/sta_info.h
@@ -469,6 +469,8 @@ void ap_sta_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
 					    unsigned timeout);
 int ap_sta_pending_delayed_1x_auth_fail_disconnect(struct hostapd_data *hapd,
 						   struct sta_info *sta);
+void ap_sta_remove_link_sta(struct hostapd_data *hapd,
+			    struct sta_info *sta);
 int ap_sta_re_add(struct hostapd_data *hapd, struct sta_info *sta);
 
 void ap_free_sta_pasn(struct hostapd_data *hapd, struct sta_info *sta);
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:46 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:46 +0530
Subject: [PATCH 5/6] hostapd: Add debug ack status for tx status
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-6-ramasamy.kaliappan@oss.qualcomm.com>

When an authentication or association response frame is sent to a peer
address with the no_sta flag set, the driver may have no station entry
for that peer even though ack may received from peer

Include the tx ack status in the authentication and association tx
callback function when the destination STA entry cannot be found.

Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 src/ap/ieee802_11.c | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/ap/ieee802_11.c b/src/ap/ieee802_11.c
index 3b71b5edc311..b5802fff2fba 100644
--- a/src/ap/ieee802_11.c
+++ b/src/ap/ieee802_11.c
@@ -8560,8 +8560,9 @@ static void handle_auth_cb(struct hostapd_data *hapd,
 	sta = ap_get_sta(hapd, mgmt->da);
 	if (!sta) {
 		wpa_printf(MSG_DEBUG, "handle_auth_cb: STA " MACSTR
-			   " not found",
-			   MAC2STR(mgmt->da));
+			   " not found"
+			   " [%s acknowledged for association response]",
+			   MAC2STR(mgmt->da), !ok ? "no" : "");
 		return;
 	}
 
@@ -8739,8 +8740,10 @@ static void handle_assoc_cb(struct hostapd_data *hapd,
 
 	sta = ap_get_sta(hapd, mgmt->da);
 	if (!sta) {
-		wpa_printf(MSG_INFO, "handle_assoc_cb: STA " MACSTR " not found",
-			   MAC2STR(mgmt->da));
+		wpa_printf(MSG_INFO, "handle_assoc_cb: STA " MACSTR " not found"
+			   " [%s acknowledged association response]",
+			   MAC2STR(mgmt->da),
+			   !ok ? "no" : "");
 		return;
 	}
 
-- 
2.34.1



From ramasamy.kaliappan at oss.qualcomm.com  Thu Aug  6 09:31:47 2026
From: ramasamy.kaliappan at oss.qualcomm.com (Ramasamy Kaliappan)
Date: Thu,  6 Aug 2026 22:01:47 +0530
Subject: [PATCH 6/6] hwsim: Add hwsim test case for no_sta handling
In-Reply-To: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
References: <20260806163147.808172-1-ramasamy.kaliappan@oss.qualcomm.com>
Message-ID: <20260806163147.808172-7-ramasamy.kaliappan@oss.qualcomm.com>

Add new hwsim test case(test_eht_mld_sae_two_links_sta_reconnect_no_sta)
to validate the no_sta handling in the authentication and association
frame.

The test configures a 3?link AP MLD and an MLD?capable STA, performs an
initial SAE-based MLO connection, and verifies expected MLD state and
link activity. It then triggers a reassociation without an explicit
disconnect to simulate a reconnect scenario where the AP may still hold
a station entry while receiving frames marked with no_sta.

A dedicated reconnect test verifies that an AP MLD preserves the STA
entry across reassociation while receiving no_sta-marked frames, and
that responses are sent to the correct destination after capability
updates. Traffic is validated across all links after reconnect.

These tests help to ensure correct infra-level behavior for frames
marked with NL80211_ATTR_FRAME_CMD_NO_STA, preventing regressions
in authentication, association, SA Query, and TX status processing
for multi-link setups.

Signed-off-by: Ramasamy Kaliappan <ramasamy.kaliappan at oss.qualcomm.com>
---
 tests/hwsim/test_eht.py | 63 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 63 insertions(+)

diff --git a/tests/hwsim/test_eht.py b/tests/hwsim/test_eht.py
index 4ad464ddb017..8cbc9487ee44 100644
--- a/tests/hwsim/test_eht.py
+++ b/tests/hwsim/test_eht.py
@@ -689,6 +689,69 @@ def test_eht_mld_sae_two_links(dev, apdev):
     """EHT MLD AP with MLD client SAE H2E connection using two links"""
     run_eht_mld_sae_two_links(dev, apdev)
 
+def test_eht_mld_sae_two_links_sta_reconnect_no_sta(dev, apdev):
+    """EHT AP MLD with MLD STA reconnecting while AP still has STA (no_sta handling)"""
+    with HWSimRadio(use_mlo=True) as (hapd_radio, hapd_iface), \
+        HWSimRadio(use_mlo=True) as (wpas_radio, wpas_iface):
+
+        wpas = WpaSupplicant(global_iface='/tmp/wpas-wlan5')
+        wpas.interface_add(wpas_iface)
+        check_sae_capab(wpas)
+
+        passphrase = 'qwertyuiop'
+        ssid = "mld_ap_sae_two_link_reconnect"
+
+        params = eht_mld_ap_wpa2_params(ssid, passphrase,
+                                        key_mgmt="SAE", mfp="2", pwe="1")
+
+        hapd0 = eht_mld_enable_ap(hapd_iface, 0, params)
+
+        params1 = dict(params)
+        params1['channel'] = '6'
+        hapd1 = eht_mld_enable_ap(hapd_iface, 1, params1)
+
+        try:
+            wpas.set("sae_pwe", "1")
+            wpas.set("sae_groups", "20 19")
+
+            # Initial MLO connection
+            wpas.connect(ssid, sae_password=passphrase,
+                         scan_freq="2412 2437",
+                         key_mgmt="SAE", ieee80211w="2", beacon_prot="1")
+
+            eht_verify_status(wpas, hapd0, 2412, 20, is_ht=True, mld=True,
+                              valid_links=3, active_links=3)
+            eht_verify_wifi_version(wpas)
+            if wpas.get_status_field('sae_group') != '19':
+                raise Exception("Initial SAE group not as expected")
+
+            sta = hapd0.get_sta(wpas.own_addr())
+            logger.info("MLD STA after first assoc at hapd0: " + str(sta))
+            if sta['addr'] == 'FAIL':
+                raise Exception("AP MLD did not create STA entry after first association")
+
+            traffic_test(wpas, hapd0)
+            traffic_test(wpas, hapd1)
+
+            # Reconnect (reassociate) without explicit disconnect from the test
+            time.sleep(1)
+            wpas.request("REASSOCIATE")
+            ev = wpas.wait_event(["CTRL-EVENT-CONNECTED"], timeout=10)
+            if ev is None:
+                raise Exception("Reassociation did not complete")
+
+            # AP should still have a STA entry for this MLD STA
+            sta2 = hapd0.get_sta(wpas.own_addr())
+            logger.info("MLD STA after reassoc at hapd0: " + str(sta2))
+            if sta2['addr'] == 'FAIL':
+                raise Exception("AP MLD lost STA entry on reassociation")
+
+            traffic_test(wpas, hapd0)
+            traffic_test(wpas, hapd1)
+        finally:
+            wpas.set("sae_pwe", "0")
+            wpas.set("sae_groups", "")
+
 def test_eht_mld_sae_two_links_no_beacon_prot(dev, apdev):
     """EHT MLD AP with MLD client SAE H2E connection using two links and no beacon protection"""
     run_eht_mld_sae_two_links(dev, apdev, beacon_prot="0")
-- 
2.34.1



From j at w1.fi  Thu Aug  6 13:21:39 2026
From: j at w1.fi (Jouni Malinen)
Date: Thu, 6 Aug 2026 23:21:39 +0300
Subject: [PATCH] PR: Fix PASN responder ROC duration, MAC filtering and
 termination notification
In-Reply-To: <20260806151349.1134998-1-xiayucheng@google.com>
References: <20260806151349.1134998-1-xiayucheng@google.com>
Message-ID: <anTs035ZI0HwTz08@w1.fi>

On Thu, Aug 06, 2026 at 03:13:49PM +0000, Emily Xia wrote:
> - Increase PR_PASN_RESPONDER_ROC_DURATION from 5s to 10s to allow
>   sufficient time for PASN responder authentication and ranging setup.
> - Notify upper layers with PR_SESSION_END_NEG_FAILED in
>   wpas_pr_pasn_roc_start_cb when driver remain-on-channel fails to start
>   for responder role.
> - Copy pr_responder_src_addr to radio work src_addr in
>   wpas_pr_schedule_responder_roc to preserve custom MAC filter address
>   when rescheduling subsequent ROC chunks.

Thanks, applied.
 
-- 
Jouni Malinen                                            PGP id EFC895FA


From j at w1.fi  Fri Aug  7 06:33:21 2026
From: j at w1.fi (Jouni Malinen)
Date: Fri, 7 Aug 2026 16:33:21 +0300
Subject: hostapd/wpa_supplicant - new release v2.12
Message-ID: <anXeofa7nlWxrpOg@w1.fi>

New versions of wpa_supplicant and hostapd were just
released and are now available from https://w1.fi/

This release is from the 2_12 branch and the main branch will continue
to be used for 2.13 development.

There has been quite a few new features and fixes since the 2.11
release. The following ChangeLog entries highlight some of the main
changes:

hostapd:
* support RSN overriding (e.g., WPA3-Personal Compatibility Mode)
* EHT/IEEE 802.11be/Wi-Fi 7
  - more complete support
  - fix message validation issues that could enable DoS attacks
  - fix group key rekeying
* enable SAE group 20 by default if SAE-EXT-KEY is enabled
* reject unexpected SAE password identifier to avoid DoS attack against
  a specific STA
* mandate use of SAE H2E when using password identifiers
* assign VLAN when using SAE with PMKSA caching
* support SPP A-MSDU negotiation
* support IEEE 802.11bi functionality
  - changing SAE password identifiers
  - EPPKE
  - IEEE 802.1X/EAP in Authentication frames
  - Association frame encryption
  - PMKID privacy
* remove the driver interface for now obsolete Host AP driver
* remove the driver interface for now obsolete Atheros WEXT interface
* move supported, basic, and Beacon TX rate configuration to be at BSS
  level instead of per-radio for all BSSs
* fix various issues in Multiple-BSSID functionality
* support OpenSSL 3.0 API changes
* EAP-TEAP: protocol changes based on RFC 9930; this is not compatible
  with previous versions
* support Automated Frequency Coordination (AFC) on the 6 GHz band
* improve GAS/ANQP processing to support larger ANQP responses
* a large number of other fixes, cleanup, and extensions

wpa_supplicant:
* support RSN overriding (e.g., WPA3-Personal Compatibility Mode)
* improve BSS transition management support
* EHT/IEEE 802.11be/Wi-Fi 7
  - more complete support
  - fix message validation issues that could enable DoS attacks
* support Wi-Fi Direct R2
* support Wi-Fi Aware (add synchronized NAN; extend USD support)
* support Proximity Ranging
* support SPP A-MSDU negotiation
* support IEEE 802.11bi functionality
  - changing SAE password identifiers
  - EPPKE
  - IEEE 802.1X/EAP in Authentication frames
  - Association frame encryption
  - PMKID privacy
* enable layer 2/Wi-Fi multicast filtering for all networks (not
  just some Passpoint networks which enabled this before)
* wpa_gui: port to Qt6
* support OpenSSL 3.0 API changes
* EAP-TEAP: protocol changes based on RFC 9930; this is not compatible
  with previous versions
* maintain configuration file permissions when writing updated
  configuration
* add option to validate PKCS#11/OpenSC engine and module paths
* fix PMKSA caching to enforce network context to avoid misuse of
  unexpected PMKSA cache entries
* fix a potential DoS attack in SAE processing of an unexpected element
* fix incomplete bounds checking of mesh AMPE messages that could have
  resulted in DoS attacks and memory corruption
* a large number of other fixes, cleanup, and extensions


git-shortlog for 2.11 -> 2.12:

There were 2377 commits, so the list would be a too long for this email.
Anyway, if you are interested in the details, they are available in the
hostap.git repository. diffstat has following to say about the changes:
 571 files changed, 107994 insertions(+), 36292 deletions(-)

-- 
Jouni Malinen                                            PGP id EFC895FA


From ksperling at apple.com  Fri Aug  7 17:20:39 2026
From: ksperling at apple.com (Karsten Sperling)
Date: Sat, 08 Aug 2026 12:20:39 +1200
Subject: Issue: OpenSSL tls_connection_set_params() replaces the context-wide
 trust store
Message-ID: <A3EE02F9-DEF3-4788-AEC8-F23A234696C3@apple.com>


tls_connection_ca_cert() in src/crypto/tls_openssl.c installs a brand-new, empty X509_STORE on the context-wide SSL_CTX before it has looked at any of its arguments.

Because tls_connection_set_params() calls it unconditionally, a per-connection tls_connection_set_params() call removes the trusted-CA set of every other connection sharing that tls_ctx. When params supplies no ca_cert, ca_cert_blob or ca_path, nothing reloads anything and the store is simply left empty.

Every other per-connection setting applied by tls_connection_set_params() is scoped to conn->ssl (e.g. SSL_use_certificate_ASN1(), SSL_use_PrivateKey_*(), SSL_set1_curves_list(), ...), so the CA handling is the odd one out.

One case where this is reachable today: src/radius/radius_client.c creates both auth_tls_conn and acct_tls_conn from a single radius->tls_ctx (:1892) and calls tls_connection_set_params() per connection with that server's own ca_cert (:1376-1389). If the auth connection's handshake is still in flight when the accounting connection is set up, the auth connection's certificate verification runs against the accounting server's trust store, and vice versa.

Checked against main at eaa647bb8: tls_connection_ca_cert() at src/crypto/tls_openssl.c:3119, store installed at :3137, called unconditionally from tls_connection_set_params() at :5809.

From viper at landau.one  Sun Aug  9 02:33:49 2026
From: viper at landau.one (Konstantin Menyaev)
Date: Sun, 9 Aug 2026 12:33:49 +0300
Subject: [PATCH 1/1] build: static libwpa_client supported
In-Reply-To: <20260801183907.1606878-1-viper@landau.one>
References: <20260801183907.1606878-1-viper@landau.one>
Message-ID: <de2c8f73-01b5-4134-8db1-7d5a22057470@landau.one>

On 8/1/26 21:39, viper at landau.one wrote:
> From: Konstantin Menyaev <viper at landau.one>
> 
> CONFIG_BUILD_WPA_CLIENT_STATIC option added to build the static
> libwpa_client library.
> 
> This is useful for applications that statically link against the
> wpa_supplicant client API.
> 
> Signed-off-by: Konstantin Menyaev <viper at landau.one>
> ---
>   wpa_supplicant/Makefile | 10 ++++++++++
>   1 file changed, 10 insertions(+)
> 
> diff --git a/wpa_supplicant/Makefile b/wpa_supplicant/Makefile
> index 2b82c42c8..4ab7ae512 100644
> --- a/wpa_supplicant/Makefile
> +++ b/wpa_supplicant/Makefile
> @@ -24,6 +24,12 @@ ifdef CONFIG_BUILD_WPA_CLIENT_SO
>   _all: libwpa_client.so
>   endif
>   
> +ifdef CONFIG_BUILD_WPA_CLIENT_STATIC
> +# add the dependency this way to allow CONFIG_BUILD_WPA_CLIENT_STATIC
> +# being set in the config which is read by build.rules
> +_all: libwpa_client.a
> +endif
> +
>   ifndef CONFIG_NO_WPA_PASSPHRASE
>   # add the dependency this way to allow CONFIG_NO_WPA_PASSPHRASE
>   # being set in the config which is read by build.rules
> @@ -91,6 +97,10 @@ endif
>   ifdef CONFIG_BUILD_WPA_CLIENT_SO
>   	install -m 0644 -D libwpa_client.so $(DESTDIR)/$(LIBDIR)/libwpa_client.so
>   	install -m 0644 -D ../src/common/wpa_ctrl.h $(DESTDIR)/$(INCDIR)/wpa_ctrl.h
> +endif
> +ifdef CONFIG_BUILD_WPA_CLIENT_STATIC
> +	install -m 0644 -D libwpa_client.a $(DESTDIR)/$(LIBDIR)/libwpa_client.a
> +	install -m 0644 -D ../src/common/wpa_ctrl.h $(DESTDIR)/$(INCDIR)/wpa_ctrl.h
>   endif
>   	if ls eap_*.so >/dev/null 2>&1; then \
>   		install -d $(DESTDIR)$(LIBDIR)/wpa_supplicant && \

kindly remind


From chung-hsien.hsu at infineon.com  Mon Aug 10 03:19:49 2026
From: chung-hsien.hsu at infineon.com (Chung-Hsien Hsu)
Date: Mon, 10 Aug 2026 18:19:49 +0800
Subject: [PATCH] wpa_supplicant: Cache PMK for 802.1X 4-way handshake
 offload
In-Reply-To: <anGnQsoOSSr-FmoF@w1.fi>
References: <20260603022228.3052-1-chung-hsien.hsu@infineon.com>
 <amcf9TX23Xvkdyco@w1.fi>
 <20260803104146.GA354007@ISCN5CG4472QHC.infineon.com>
 <anGnQsoOSSr-FmoF@w1.fi>
Message-ID: <20260810101949.GA3954@ISCN5CG4472QHC.infineon.com>

On Tue, Aug 04, 2026 at 10:48:02AM +0200, Jouni Malinen wrote:
> It would be good to state that more clearly in the commit message since
> that part about PMKSA caching not being available was not obvious since
> the focused more on storing the PMK rather than adding a PMKSA cache
> entry.

Agreed. I will update the commit message to make the missing PMKSA cache
entry and its impact on subsequent connections clearer.

> If the main point here is in adding that PMKSA cache entry, why the
> changes allow wpa_sm_set_pmk() to be called with auth_addr == NULL which
> is explicitly documented to not cache the PMK? I can understand the need
> to create a PMKSA cache entry, but what is the need, if any, to set
> sm->pmk?

The original change was intended to mirror the host-driven EAPOL-Key
path by storing the PMK in the WPA state machine as well as creating a
PMKSA cache entry where applicable.

After looking at this more closely, there is no specific need to update
sm->pmk for the driver-offloaded 4-way handshake itself. The actual
issue is the missing PMKSA cache entry.

I'll revise the patch so that wpa_sm_set_pmk() is called only when PMKSA
caching is applicable and a valid authenticator address is available,
and send a v2.

Chung-Hsien


From xiayucheng at google.com  Tue Aug 11 02:19:28 2026
From: xiayucheng at google.com (Emily Xia)
Date: Tue, 11 Aug 2026 09:19:28 +0000
Subject: [PATCH] PR: Update device capabilities for OOB discovery in PASN wrapper
Message-ID: <20260811091928.1295783-1-xiayucheng@google.com>

When processing the PASN ranging wrapper for peers discovered via out-of-band
(OOB) discovery, update dev->pr_caps, dev->ntb_caps, and dev->edca_caps with
the parsed capabilities after matching checks succeed.

Signed-off-by: Emily Xia <xiayucheng at google.com>
---
 src/common/proximity_ranging.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/common/proximity_ranging.c b/src/common/proximity_ranging.c
index a230043f5..21edca070 100644
--- a/src/common/proximity_ranging.c
+++ b/src/common/proximity_ranging.c
@@ -2261,6 +2261,8 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 	if (!pr_eq_ranging_capa_params(dev, &caps)) {
 		wpa_printf(MSG_INFO, "PR: Ranging capabilities not matching");
 		goto end;
+	} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+		dev->pr_caps = caps;
 	}
 
 	pr_get_ranging_capabilities(pr, &caps);
@@ -2285,6 +2287,8 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 			wpa_printf(MSG_INFO,
 				   "PR: NTB capabilities not matching");
 			goto end;
+		} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+			dev->ntb_caps = ntb;
 		}
 
 		if (dev->ntb_caps.ista_support)
@@ -2301,6 +2305,8 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 			wpa_printf(MSG_INFO,
 				   "PR: EDCA capabilities not matching");
 			goto end;
+		} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+			dev->edca_caps = edca;
 		}
 
 		if (dev->edca_caps.ista_support)
-- 
2.55.0.679.g6767b8d81c-goog



From chung-hsien.hsu at infineon.com  Tue Aug 11 19:13:22 2026
From: chung-hsien.hsu at infineon.com (Chung-Hsien Hsu)
Date: Wed, 12 Aug 2026 10:13:22 +0800
Subject: [PATCH v2] wpa_supplicant: Fix PMKSA caching with 802.1X 4-way handshake offload
Message-ID: <20260812021321.92205-1-chung-hsien.hsu@infineon.com>

When driver-based 4-way handshake offload is used with IEEE 802.1X,
wpa_supplicant_eapol_cb() retrieves the PMK from the EAPOL state machine
and configures it to the driver. Since the subsequent 4-way handshake is
handled by the driver, the normal host-side EAPOL-Key processing path
that creates a PMKSA cache entry is not executed.

As a result, a subsequent connection cannot use PMKSA caching and may
require a full EAP authentication again.

Create a PMKSA cache entry after successful IEEE 802.1X authentication
for the same non-FT and non-Suite-B RSN cases where the host-driven
EAPOL-Key processing path does so. Only call wpa_sm_set_pmk() when a
valid authenticator address is available.

Signed-off-by: Chung-Hsien Hsu <chung-hsien.hsu at infineon.com>
---
v2:
- Clarified that the issue being fixed is the missing PMKSA cache entry
  and its impact on subsequent connections.
- Removed the unconditional wpa_sm_set_pmk() call with auth_addr ==
  NULL.
- Limited wpa_sm_set_pmk() to cases where PMKSA caching is applicable
  and a valid authenticator address is available.
- Updated the subject and commit message to focus on PMKSA caching.
---
 wpa_supplicant/wpas_glue.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/wpa_supplicant/wpas_glue.c b/wpa_supplicant/wpas_glue.c
index e57114503228..52fc468452a6 100644
--- a/wpa_supplicant/wpas_glue.c
+++ b/wpa_supplicant/wpas_glue.c
@@ -292,6 +292,7 @@ static void wpa_supplicant_eapol_cb(struct eapol_sm *eapol,
 				    void *ctx)
 {
 	struct wpa_supplicant *wpa_s = ctx;
+	const u8 *auth_addr;
 	int res, pmk_len;
 	u8 pmk[PMK_LEN_MAX];
 
@@ -380,6 +381,15 @@ static void wpa_supplicant_eapol_cb(struct eapol_sm *eapol,
 		wpa_printf(MSG_DEBUG, "Failed to set PMK to the driver");
 	}
 
+	if (wpa_s->wpa_proto == WPA_PROTO_RSN &&
+	    !wpa_key_mgmt_suite_b(wpa_s->key_mgmt) &&
+	    !wpa_key_mgmt_ft(wpa_s->key_mgmt)) {
+		auth_addr = wpa_sm_get_auth_addr(wpa_s->wpa);
+		if (!is_zero_ether_addr(auth_addr))
+			wpa_sm_set_pmk(wpa_s->wpa, pmk, pmk_len, NULL,
+				       auth_addr);
+	}
+
 	wpa_supplicant_cancel_scan(wpa_s);
 	wpa_supplicant_cancel_auth_timeout(wpa_s);
 	wpa_supplicant_set_state(wpa_s, WPA_COMPLETED);
-- 
2.25.1



From xiayucheng at google.com  Tue Aug 11 20:00:06 2026
From: xiayucheng at google.com (Emily Xia)
Date: Wed, 12 Aug 2026 03:00:06 +0000
Subject: [PATCH v2] PR: Update device capabilities for OOB discovery in PASN wrapper
In-Reply-To: <20260811091928.1295783-1-xiayucheng@google.com>
References: <20260811091928.1295783-1-xiayucheng@google.com>
Message-ID: <20260812030006.2378038-1-xiayucheng@google.com>

Previously a peer device discovered by out-of-band (OOB) discovery has
empty capabilities in pr_caps, ntb_caps, and edca_caps. This caused the
final decision of format_bw in wpas_pr_ranging_params to evaluate to 0
because peer_format_bw was always 0.

When processing the PASN ranging wrapper for peers discovered via OOB
discovery, update dev->pr_caps, dev->ntb_caps, and dev->edca_caps with
the parsed capabilities in standalone statements after matching checks
succeed.

Signed-off-by: Emily Xia <xiayucheng at google.com>
---
v2:
- Improve coding style by separating OOB capability assignment statements
  from conditional error check blocks into standalone statements.

 src/common/proximity_ranging.c | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/src/common/proximity_ranging.c b/src/common/proximity_ranging.c
index 21edca070..a73db909b 100644
--- a/src/common/proximity_ranging.c
+++ b/src/common/proximity_ranging.c
@@ -2261,7 +2261,9 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 	if (!pr_eq_ranging_capa_params(dev, &caps)) {
 		wpa_printf(MSG_INFO, "PR: Ranging capabilities not matching");
 		goto end;
-	} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+	}
+
+	if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
 		dev->pr_caps = caps;
 	}
 
@@ -2287,7 +2289,9 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 			wpa_printf(MSG_INFO,
 				   "PR: NTB capabilities not matching");
 			goto end;
-		} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+		}
+
+		if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
 			dev->ntb_caps = ntb;
 		}
 
@@ -2305,7 +2309,9 @@ static int pr_process_pasn_ranging_wrapper(struct pr_data *pr,
 			wpa_printf(MSG_INFO,
 				   "PR: EDCA capabilities not matching");
 			goto end;
-		} else if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
+		}
+
+		if (dev->discovery_type == PR_DISCOVERY_TYPE_OOB) {
 			dev->edca_caps = edca;
 		}
 
-- 
2.55.0.679.g6767b8d81c-goog



