Index | Thread | Search

From:
Kirill A. Korinsky <kirill@korins.ky>
Subject:
The 7th and the last batch of qwz fixes
To:
Stefan Sperling <stsp@stsp.name>
Cc:
OpenBSD tech <tech@openbsd.org>
Date:
Thu, 01 Oct 2026 19:44:30 +0200

Download raw body.

Thread
  • Kirill A. Korinsky:

    The 7th and the last batch of qwz fixes

Stefan,

here the last batch of qwz fixes.

I had left only two commits which we skipt and releatvly trivial enabling
11ac, which, I think, make sense to discuss at end of the moth, after the
release.

Ok?

From 393700cf64cbc3d6558d5609c3a83384ef22c933 Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:56:24 +0200
Subject: [PATCH 1/6] sys/qwz: add background scan and ordered roaming

Based on sys/dev/ic/qwx.c,v 1.83 , sys/dev/ic/qwxvar.h,v 1.28 ,
sys/dev/pci/if_qwx_pci.c,v 1.25 , sys/dev/ic/qwx.c,v 1.126 ,
sys/dev/ic/qwxvar.h,v 1.37 and sys/dev/pci/if_qwx_pci.c,v 1.36

Add background scans and drain TX before switching APs. Tear down the
old peer and its keys while its address is still available. Keep
firmware running across interface down/up, bound key deletion waits,
and clear pending key arguments after running tasks drain during stop.
---
 sys/dev/ic/qwz.c         | 323 ++++++++++++++++++++++++++++++++++++---
 sys/dev/ic/qwzvar.h      |   9 ++
 sys/dev/pci/if_qwz_pci.c |   2 +
 3 files changed, 313 insertions(+), 21 deletions(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index 3e93b6bb40d..60b7242b342 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -177,7 +177,7 @@ int qwz_dp_peer_rx_pn_replay_config(struct qwz_softc *, struct qwz_vif *,
 void qwz_setkey_clear(struct qwz_softc *);
 void qwz_vif_purge(struct qwz_softc *);
 
-int qwz_scan(struct qwz_softc *);
+int qwz_scan(struct qwz_softc *, int);
 void qwz_scan_abort(struct qwz_softc *);
 int qwz_auth(struct qwz_softc *);
 int qwz_deauth(struct qwz_softc *);
@@ -397,6 +397,16 @@ qwz_del_task(struct qwz_softc *sc, struct taskq *taskq, struct task *task)
 		refcnt_rele(&sc->task_refs);
 }
 
+void
+qwz_del_task_all(struct qwz_softc *sc)
+{
+	qwz_del_task(sc, sc->sc_nswq, &sc->newstate_task);
+	qwz_del_task(sc, systq, &sc->setkey_task);
+	qwz_del_task(sc, systq, &sc->ba_task);
+	qwz_del_task(sc, systq, &sc->bgscan_task);
+	qwz_del_task(sc, systq, &sc->bgscan_done_task);
+}
+
 void
 qwz_stop(struct ifnet *ifp)
 {
@@ -413,11 +423,8 @@ qwz_stop(struct ifnet *ifp)
 
 	/* Cancel scheduled tasks and let any stale tasks finish up. */
 	task_del(systq, &sc->init_task);
-	qwz_del_task(sc, sc->sc_nswq, &sc->newstate_task);
-	qwz_del_task(sc, systq, &sc->setkey_task);
-	qwz_del_task(sc, systq, &sc->ba_task);
+	qwz_del_task_all(sc);
 	refcnt_finalize(&sc->task_refs, "qwzstop");
-
 	qwz_setkey_clear(sc);
 
 	ifp->if_timer = sc->sc_tx_timer = 0;
@@ -441,6 +448,10 @@ qwz_stop(struct ifnet *ifp)
 
 	sc->sc_newstate(ic, IEEE80211_S_INIT, -1);
 	sc->ns_nstate = IEEE80211_S_INIT;
+	clear_bit(QWZ_FLAG_ROAMING, sc->sc_flags);
+	free(sc->bgscan_unref_arg, M_DEVBUF, sc->bgscan_unref_arg_size);
+	sc->bgscan_unref_arg = NULL;
+	sc->bgscan_unref_arg_size = 0;
 	sc->scan.state = ATH12K_SCAN_IDLE;
 	sc->vdev_id_11d_scan = QWZ_11D_INVALID_VDEV_ID;
 
@@ -968,6 +979,80 @@ qwz_setkey_task(void *arg)
 	splx(s);
 }
 
+void
+qwz_clear_hwkeys(struct qwz_softc *sc, struct ath12k_peer *peer)
+{
+	struct qwz_vif *arvif = &sc->sc_vif;
+	uint8_t pdev_id = 0; /* TODO: derive pdev ID somehow? */
+	struct wmi_vdev_install_key_arg arg =  {
+		.vdev_id = arvif->vdev_id,
+		.key_len = 0,
+		.key_data = NULL,
+		.key_cipher = WMI_CIPHER_NONE,
+		.key_flags = 0,
+	};
+	int k_id = 0, ret;
+
+	if (test_bit(ATH12K_FLAG_HW_CRYPTO_DISABLED, sc->sc_flags))
+		return;
+
+	arg.macaddr = peer->addr;
+
+	for (k_id = 0; k_id <= WMI_MAX_KEY_INDEX; k_id++) {
+		arg.key_idx = k_id;
+
+		sc->install_key_done = 0;
+		ret = qwz_wmi_vdev_install_key(sc, &arg, pdev_id);
+		if (ret) {
+			printf("%s: delete key %d failed: error %d\n",
+			    sc->sc_dev.dv_xname, k_id, ret);
+			return;
+		}
+
+		while (!sc->install_key_done) {
+			ret = tsleep_nsec(&sc->install_key_done, 0,
+			    "qwzinstkey", SEC_TO_NSEC(1));
+			if (ret) {
+				printf("%s: delete key %d timeout\n",
+				    sc->sc_dev.dv_xname, k_id);
+				return;
+			}
+		}
+	}
+}
+
+void
+qwz_clear_pn_replay_config(struct qwz_softc *sc, struct ath12k_peer *peer)
+{
+	struct ath12k_hal_reo_cmd cmd = {0};
+	struct dp_rx_tid *rx_tid;
+	uint8_t tid;
+	int ret = 0;
+
+	cmd.flag |= HAL_REO_CMD_FLG_NEED_STATUS;
+	cmd.upd0 |= HAL_REO_CMD_UPD0_PN |
+		    HAL_REO_CMD_UPD0_PN_SIZE |
+		    HAL_REO_CMD_UPD0_PN_VALID |
+		    HAL_REO_CMD_UPD0_PN_CHECK |
+		    HAL_REO_CMD_UPD0_SVLD;
+
+	for (tid = 0; tid < IEEE80211_NUM_TID; tid++) {
+		rx_tid = &peer->rx_tid[tid];
+		if (!rx_tid->active)
+			continue;
+		cmd.addr_lo = rx_tid->paddr & 0xffffffff;
+		cmd.addr_hi = (rx_tid->paddr >> 32);
+		ret = qwz_dp_tx_send_reo_cmd(sc, rx_tid,
+		    HAL_REO_CMD_UPDATE_RX_QUEUE, &cmd, NULL);
+		if (ret) {
+			printf("%s: failed to configure rx tid %d queue "
+			    "for pn replay detection %d\n",
+			    sc->sc_dev.dv_xname, tid, ret);
+			break;
+		}
+	}
+}
+
 void
 qwz_setkey_clear(struct qwz_softc *sc)
 {
@@ -1010,9 +1095,8 @@ qwz_newstate(struct ieee80211com *ic, enum ieee80211_state nstate, int arg)
 		qwz_del_task(sc, systq, &sc->ba_task);
 		qwz_del_task(sc, systq, &sc->setkey_task);
 		qwz_setkey_clear(sc);
-#if 0
+		qwz_del_task(sc, systq, &sc->bgscan_task);
 		qwz_del_task(sc, systq, &sc->bgscan_done_task);
-#endif
 	}
 
 	sc->ns_nstate = nstate;
@@ -1055,6 +1139,11 @@ qwz_newstate_task(void *arg)
 	if (nstate <= ostate) {
 		switch (ostate) {
 		case IEEE80211_S_RUN:
+			if (test_bit(QWZ_FLAG_ROAMING, sc->sc_flags)) {
+				clear_bit(QWZ_FLAG_ROAMING, sc->sc_flags);
+				break;
+			}
+
 			err = qwz_run_stop(sc);
 			if (err)
 				goto out;
@@ -1091,21 +1180,15 @@ qwz_newstate_task(void *arg)
 
 	case IEEE80211_S_SCAN:
 next_scan:
-		err = qwz_scan(sc);
+		err = qwz_scan(sc, 0);
 		if (err)
 			break;
 		if (ifp->if_flags & IFF_DEBUG)
 			printf("%s: %s -> %s\n", ifp->if_xname,
 			    ieee80211_state_name[ic->ic_state],
 			    ieee80211_state_name[IEEE80211_S_SCAN]);
-#if 0
-		if ((sc->sc_flags & QWZ_FLAG_BGSCAN) == 0) {
-#endif
-			ieee80211_set_link_state(ic, LINK_STATE_DOWN);
-			ieee80211_node_cleanup(ic, ic->ic_bss);
-#if 0
-		}
-#endif
+		ieee80211_set_link_state(ic, LINK_STATE_DOWN);
+		ieee80211_node_cleanup(ic, ic->ic_bss);
 		ic->ic_state = IEEE80211_S_SCAN;
 		refcnt_rele_wake(&sc->task_refs);
 		splx(s);
@@ -12026,8 +12109,11 @@ qwz_wmi_process_mgmt_tx_comp(struct qwz_softc *sc,
 	ieee80211_release_node(ic, tx_data->ni);
 	tx_data->ni = NULL;
 
-	if (arvif->txmgmt.queued > 0)
+	if (arvif->txmgmt.queued > 0) {
 		arvif->txmgmt.queued--;
+		if (arvif->txmgmt.queued == 0)
+			wakeup(&arvif->txmgmt.queued);
+	}
 
 	if (tx_compl_param->status != 0)
 		ifp->if_oerrors++;
@@ -14430,8 +14516,11 @@ qwz_dp_tx_free_txbuf(struct qwz_softc *sc, int msdu_id,
 		m_freem(tx_data->m);
 		tx_data->m = NULL;
 
-		if (tx_ring->queued > 0)
+		if (tx_ring->queued > 0) {
 			tx_ring->queued--;
+			if (tx_ring->queued == 0)
+				wakeup(&tx_ring->queued);
+		}
 	}
 
 	if (tx_data->ni) {
@@ -14580,8 +14669,11 @@ qwz_dp_tx_complete_msdu(struct qwz_softc *sc, struct dp_tx_ring *tx_ring,
 		m_freem(tx_data->m);
 		tx_data->m = NULL;
 
-		if (tx_ring->queued > 0)
+		if (tx_ring->queued > 0) {
 			tx_ring->queued--;
+			if (tx_ring->queued == 0)
+				wakeup(&tx_ring->queued);
+		}
 	}
 
 	if (tx_data->ni == NULL)
@@ -24092,7 +24184,7 @@ qwz_start_scan(struct qwz_softc *sc, struct scan_req_params *arg)
 #define ATH12K_MAC_SCAN_CMD_EVT_OVERHEAD		200 /* in msecs */
 
 int
-qwz_scan(struct qwz_softc *sc)
+qwz_scan(struct qwz_softc *sc, int bgscan)
 {
 	struct ieee80211com *ic = &sc->sc_ic;
 	struct qwz_vif *arvif = &sc->sc_vif;
@@ -24226,7 +24318,7 @@ qwz_scan(struct qwz_softc *sc)
 #ifdef notyet
 		spin_unlock_bh(&ar->data_lock);
 #endif
-	} else {
+	} else if (!bgscan) {
 		/*
 		 * The current mode might have been fixed during association.
 		 * Ensure all channels get scanned.
@@ -24294,6 +24386,189 @@ qwz_scan_abort(struct qwz_softc *sc)
 #endif
 }
 
+void
+qwz_bgscan_task(void *arg)
+{
+	struct qwz_softc *sc = arg;
+	struct ieee80211com *ic = &sc->sc_ic;
+	int s = splnet();
+
+	if ((ic->ic_if.if_flags & IFF_RUNNING) &&
+	    ic->ic_state == IEEE80211_S_RUN &&
+	    sc->scan.state == ATH12K_SCAN_IDLE &&
+	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+		qwz_scan(sc, 1);
+
+	refcnt_rele_wake(&sc->task_refs);
+	splx(s);
+}
+
+int
+qwz_bgscan(struct ieee80211com *ic)
+{
+	struct ifnet *ifp = &ic->ic_if;
+	struct qwz_softc *sc = ifp->if_softc;
+
+	qwz_add_task(sc, systq, &sc->bgscan_task);
+
+	return 0;
+}
+
+void
+qwz_bgscan_done_task(void *arg)
+{
+	struct qwz_softc *sc = arg;
+	struct qwz_dp *dp = &sc->dp;
+	struct qwz_vif *arvif = &sc->sc_vif;
+	struct ieee80211com *ic = &sc->sc_ic;
+	struct ifnet *ifp = &ic->ic_if;
+	struct ieee80211_node *ni = ic->ic_bss;
+	int err = 0, s, i;
+
+	s = splnet();
+
+	/* Prevent races with ifconfig commands. */
+	if (rw_enter(&sc->ioctl_rwl, RW_WRITE | RW_NOSLEEP) != 0) {
+		refcnt_rele_wake(&sc->task_refs);
+		splx(s);
+		return;
+	}
+
+	/* Ensure that we start in expected state. */
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    (ifp->if_flags & IFF_RUNNING) == 0 ||
+	    (ic->ic_flags & IEEE80211_F_BGSCAN) == 0 ||
+	    (ic->ic_xflags & IEEE80211_F_TX_MGMT_ONLY) == 0 ||
+	    test_bit(QWZ_FLAG_ROAMING, sc->sc_flags) ||
+	    ic->ic_state != IEEE80211_S_RUN) {
+		/* Don't touch the device, just return. */
+		rw_exit(&sc->ioctl_rwl);
+		refcnt_rele_wake(&sc->task_refs);
+		splx(s);
+		return;
+	}
+
+	/* Send a DEAUTH frame to our old AP. */
+	err = IEEE80211_SEND_MGMT(ic, ni, IEEE80211_FC0_SUBTYPE_DEAUTH,
+	    IEEE80211_REASON_AUTH_LEAVE);
+	if (err)
+		goto done;
+
+	/* Prevent state changes due to received frames. */
+	ifp->if_flags &= ~IFF_RUNNING;
+
+	/* Disallow new tasks. */
+	set_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags);
+
+	qwz_del_task_all(sc);
+	qwz_setkey_clear(sc);
+
+	/* Wait for Tx queues to drain. */
+	for (i = 0; i < sc->hw_params.max_tx_ring; i++) {
+		struct dp_tx_ring *tx_ring = &dp->tx_ring[i];
+
+		while (tx_ring->queued > 0) {
+			err = tsleep_nsec(&tx_ring->queued, 0, "qwztxdr",
+			    SEC_TO_NSEC(1));
+			if (err) {
+				if (tx_ring->queued == 0) {
+					err = 0;
+					break;
+				}
+				DPRINTF("%s: Tx ring %d has %d frames queued\n",
+				    __func__, i, tx_ring->queued);
+				goto done;
+			}
+		}
+	}
+	while (arvif->txmgmt.queued > 0) {
+		err = tsleep_nsec(&arvif->txmgmt.queued, 0, "qwztxdr",
+		    MSEC_TO_NSEC(500));
+		if (err) {
+			if (arvif->txmgmt.queued == 0) {
+				err = 0;
+				break;
+			}
+			DPRINTF("%s: %d management frames still queued\n",
+			    __func__, arvif->txmgmt.queued);
+			goto done;
+		}
+	}
+
+	/*
+	 * Remove installed crypto keys while we still have access to them.
+	 * Once qwz_newstate() is entered ic_bss will already contain
+	 * information about our next AP.
+	 */
+	if (ic->ic_flags & IEEE80211_F_RSNON) {
+		struct ieee80211_key *k;
+
+		if (ni->ni_pairwise_key.k_flags & IEEE80211_KEY_SWCRYPTO)
+			ieee80211_delete_key(ic, ni, &ni->ni_pairwise_key);
+		for (i = 0; i < nitems(ic->ic_nw_keys); i++) {
+			k = &ic->ic_nw_keys[i];
+			if (k->k_flags & IEEE80211_KEY_SWCRYPTO)
+				ieee80211_delete_key(ic, ni, k);
+		}
+
+		ni->ni_port_valid = 0;
+		ni->ni_flags &= ~IEEE80211_NODE_TXRXPROT;
+		ni->ni_flags &= ~IEEE80211_NODE_TXMGMTPROT;
+		ni->ni_flags &= ~IEEE80211_NODE_RXMGMTPROT;
+		ni->ni_rsn_supp_state = RSNA_SUPP_INITIALIZE;
+	}
+
+	/*
+	 * XXX: This needs to be unset for vdev shutdown to work.
+	 * Perhaps we need a separate flag?
+	 */
+	clear_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags);
+
+	/* Clear association to our old AP in firmware. */
+	err = qwz_run_stop(sc);
+	if (err)
+		goto done;
+
+	err = qwz_deauth(sc);
+	if (err)
+		goto done;
+
+	/* Allow roaming to proceed. */
+	set_bit(QWZ_FLAG_ROAMING, sc->sc_flags);
+	ifp->if_flags |= IFF_RUNNING;
+	ni->ni_unref_arg = sc->bgscan_unref_arg;
+	ni->ni_unref_arg_size = sc->bgscan_unref_arg_size;
+	sc->bgscan_unref_arg = NULL;
+	sc->bgscan_unref_arg_size = 0;
+	ieee80211_node_switch_bss(ic, ni);
+done:
+	if (err) {
+		clear_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags);
+		free(sc->bgscan_unref_arg, M_DEVBUF, sc->bgscan_unref_arg_size);
+		sc->bgscan_unref_arg = NULL;
+		sc->bgscan_unref_arg_size = 0;
+
+		ifp->if_flags |= IFF_RUNNING;
+		task_add(systq, &sc->init_task);
+	}
+
+	rw_exit(&sc->ioctl_rwl);
+	refcnt_rele_wake(&sc->task_refs);
+	splx(s);
+}
+
+void
+qwz_bgscan_done(struct ieee80211com *ic,
+    struct ieee80211_node_switch_bss_arg *arg, size_t arg_size)
+{
+	struct qwz_softc *sc = ic->ic_softc;
+
+	free(sc->bgscan_unref_arg, M_DEVBUF, sc->bgscan_unref_arg_size);
+	sc->bgscan_unref_arg = arg;
+	sc->bgscan_unref_arg_size = arg_size;
+	qwz_add_task(sc, systq, &sc->bgscan_done_task);
+}
+
 /*
  * Find a pdev which corresponds to a given channel.
  * This doesn't exactly match the semantics of the Linux driver
@@ -24447,6 +24722,10 @@ qwz_deauth(struct qwz_softc *sc)
 	if (peer == NULL)
 		return 0;
 
+	if (!peer->delete_pending) {
+		qwz_clear_pn_replay_config(sc, peer);
+		qwz_clear_hwkeys(sc, peer);
+	}
 
 	ret = qwz_mac_station_remove(sc, arvif, pdev_id, peer);
 	if (ret)
@@ -25077,6 +25356,8 @@ qwz_attach(struct qwz_softc *sc)
 	task_set(&sc->newstate_task, qwz_newstate_task, sc);
 	task_set(&sc->setkey_task, qwz_setkey_task, sc);
 	task_set(&sc->ba_task, qwz_ba_task, sc);
+	task_set(&sc->bgscan_task, qwz_bgscan_task, sc);
+	task_set(&sc->bgscan_done_task, qwz_bgscan_done_task, sc);
 	timeout_set_proc(&sc->scan.timeout, qwz_scan_timeout, sc);
 #if NBPFILTER > 0
 	qwz_radiotap_attach(sc);
diff --git a/sys/dev/ic/qwzvar.h b/sys/dev/ic/qwzvar.h
index 05984dd8697..cbc4b377424 100644
--- a/sys/dev/ic/qwzvar.h
+++ b/sys/dev/ic/qwzvar.h
@@ -421,6 +421,7 @@ enum ath12k_dev_flags {
 	ATH12K_FLAG_FIXED_MEM_RGN,
 	ATH12K_FLAG_DEVICE_INIT_DONE,
 	ATH12K_FLAG_MULTI_MSI_VECTORS,
+	QWZ_FLAG_ROAMING,
 };
 
 enum ath12k_scan_state {
@@ -2009,6 +2010,11 @@ struct qwz_softc {
 	struct task		ba_task;
 	struct qwz_ba_task_data	ba_rx;
 
+	struct task		bgscan_task;
+	struct task		bgscan_done_task;
+	struct ieee80211_node_switch_bss_arg *bgscan_unref_arg;
+	size_t			bgscan_unref_arg_size;
+
 	enum ath12k_11d_state	state_11d;
 	int			completed_11d_scan;
 	uint32_t		vdev_id_11d_scan;
@@ -2180,6 +2186,9 @@ void	qwz_watchdog(struct ifnet *);
 void	qwz_init_task(void *);
 int	qwz_newstate(struct ieee80211com *, enum ieee80211_state, int);
 void	qwz_newstate_task(void *);
+int	qwz_bgscan(struct ieee80211com *);
+void	qwz_bgscan_done(struct ieee80211com *,
+    struct ieee80211_node_switch_bss_arg *, size_t);
 
 struct qwz_node {
 	struct ieee80211_node ni;
diff --git a/sys/dev/pci/if_qwz_pci.c b/sys/dev/pci/if_qwz_pci.c
index e1997e2671c..7c98b79f8ad 100644
--- a/sys/dev/pci/if_qwz_pci.c
+++ b/sys/dev/pci/if_qwz_pci.c
@@ -1015,6 +1015,8 @@ qwz_pci_attach(struct device *parent, struct device *self, void *aux)
 	ic->ic_ampdu_rx_stop = qwz_ampdu_rx_stop;
 	ic->ic_ampdu_tx_start = qwz_ampdu_tx_start;
 	ic->ic_ampdu_tx_stop = NULL;
+	ic->ic_bgscan_start = qwz_bgscan;
+	ic->ic_bgscan_done = qwz_bgscan_done;
 	/*
 	 * We cannot read the MAC address without loading the
 	 * firmware from disk. Postpone until mountroot is done.
-- 
2.55.0


From 6a6981b006e04fcedf43631b9c802a7c9dfe60b2 Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:56:17 +0200
Subject: [PATCH 2/6] sys/qwz: update negotiated HT channel width

Based sys/dev/ic/qwx.c,v 1.118 , sys/dev/ic/qwxvar.h,v 1.35 and
 sys/dev/pci/if_qwx_pci.c,v 1.35

Apply AP width changes with the required PHY mode and channel width
command ordering, using initialized peer arguments. Run updates on
the state task queue because WMI sends can sleep. Cancel pending work
during teardown and drain active state tasks before switching peers.
---
 sys/dev/ic/qwz.c         | 128 +++++++++++++++++++++++++++++++++++++++
 sys/dev/ic/qwzvar.h      |   3 +
 sys/dev/pci/if_qwz_pci.c |   2 +-
 3 files changed, 132 insertions(+), 1 deletion(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index 60b7242b342..244337d974f 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -401,6 +401,7 @@ void
 qwz_del_task_all(struct qwz_softc *sc)
 {
 	qwz_del_task(sc, sc->sc_nswq, &sc->newstate_task);
+	qwz_del_task(sc, sc->sc_nswq, &sc->updatechan_task);
 	qwz_del_task(sc, systq, &sc->setkey_task);
 	qwz_del_task(sc, systq, &sc->ba_task);
 	qwz_del_task(sc, systq, &sc->bgscan_task);
@@ -1092,6 +1093,7 @@ qwz_newstate(struct ieee80211com *ic, enum ieee80211_state nstate, int arg)
 	    nstate != IEEE80211_S_AUTH)
 		return 0;
 	if (ic->ic_state == IEEE80211_S_RUN) {
+		qwz_del_task(sc, sc->sc_nswq, &sc->updatechan_task);
 		qwz_del_task(sc, systq, &sc->ba_task);
 		qwz_del_task(sc, systq, &sc->setkey_task);
 		qwz_setkey_clear(sc);
@@ -24461,6 +24463,8 @@ qwz_bgscan_done_task(void *arg)
 	set_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags);
 
 	qwz_del_task_all(sc);
+	/* State and channel tasks can sleep with the old peer in use. */
+	taskq_barrier(sc->sc_nswq);
 	qwz_setkey_clear(sc);
 
 	/* Wait for Tx queues to drain. */
@@ -24994,6 +24998,124 @@ qwz_peer_assoc_prepare(struct qwz_softc *sc, struct qwz_vif *arvif,
 	/* TODO: amsdu_disable req? */
 }
 
+void
+qwz_updatechan_task(void *arg)
+{
+	struct qwz_softc *sc = arg;
+	struct ieee80211com *ic = &sc->sc_ic;
+	struct ifnet *ifp = &ic->ic_if;
+	struct qwz_vif *arvif = &sc->sc_vif;
+	struct ieee80211_node *ni = ic->ic_bss;
+	struct qwz_node *nq = (struct qwz_node *)ic->ic_bss;
+	int pdev_id = 0; /* TODO: derive pdev ID somehow? */
+	struct peer_assoc_params peer_arg = {0};
+	enum wmi_peer_chwidth chwidth;
+	int ret, s = splnet();
+
+	if ((ifp->if_flags & IFF_RUNNING) == 0 ||
+	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    ic->ic_state != IEEE80211_S_RUN ||
+	    sc->ns_nstate != IEEE80211_S_RUN)
+		goto out;
+
+	qwz_peer_assoc_h_phymode(sc, ni, &peer_arg);
+	qwz_peer_assoc_h_ht(sc, ni, &peer_arg);
+
+	if (peer_arg.bw_40)
+		chwidth = WMI_PEER_CHWIDTH_40MHZ;
+	else
+		chwidth = WMI_PEER_CHWIDTH_20MHZ;
+
+	if (nq->chwidth == chwidth)
+		goto out;
+
+	if (nq->chwidth < chwidth) {
+		/*
+		 * BW is upgraded. In this case we send WMI_PEER_PHYMODE
+		 * followed by WMI_PEER_CHWIDTH.
+		 */
+		ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr,
+		    arvif->vdev_id, pdev_id, WMI_PEER_PHYMODE,
+		    peer_arg.peer_phymode);
+		if (ret) {
+			printf("%s: failed to update phymode for peer %s "
+			    "vdev_id %d\n",
+			    sc->sc_dev.dv_xname,
+			    ether_sprintf(ni->ni_macaddr),
+			    arvif->vdev_id);
+			goto out;
+		}
+
+		if ((ifp->if_flags & IFF_RUNNING) == 0 ||
+		    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    sc->ns_nstate != IEEE80211_S_RUN)
+			goto out;
+
+		ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr,
+		    arvif->vdev_id, pdev_id, WMI_PEER_CHWIDTH, chwidth);
+		if (ret) {
+			printf("%s: failed to update channel width for peer %s "
+			    "vdev_id %d\n",
+			    sc->sc_dev.dv_xname,
+			    ether_sprintf(ni->ni_macaddr),
+			    arvif->vdev_id);
+			goto out;
+		}
+	} else {
+		/*
+		 * BW is downgraded. In this case we send WMI_PEER_CHWIDTH
+		 * followed by WMI_PEER_PHYMODE.
+		 */
+		ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr,
+		    arvif->vdev_id, pdev_id, WMI_PEER_CHWIDTH, chwidth);
+		if (ret) {
+			printf("%s: failed to update channel width for peer %s "
+			    "vdev_id %d\n",
+			    sc->sc_dev.dv_xname,
+			    ether_sprintf(ni->ni_macaddr),
+			    arvif->vdev_id);
+			goto out;
+		}
+
+		if ((ifp->if_flags & IFF_RUNNING) == 0 ||
+		    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    sc->ns_nstate != IEEE80211_S_RUN)
+			goto out;
+
+		ret = qwz_wmi_set_peer_param(sc, ni->ni_macaddr,
+		    arvif->vdev_id, pdev_id, WMI_PEER_PHYMODE,
+		    peer_arg.peer_phymode);
+		if (ret) {
+			printf("%s: failed to update phymode for peer %s "
+			    "vdev_id %d\n",
+			    sc->sc_dev.dv_xname,
+			    ether_sprintf(ni->ni_macaddr),
+			    arvif->vdev_id);
+			goto out;
+		}
+
+	}
+
+	nq->chwidth = chwidth;
+out:
+	refcnt_rele_wake(&sc->task_refs);
+	splx(s);
+}
+
+void
+qwz_updatechan(struct ieee80211com *ic)
+{
+	struct qwz_softc *sc = ic->ic_softc;
+
+	if ((ic->ic_if.if_flags & IFF_RUNNING) == 0 ||
+	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    ic->ic_state != IEEE80211_S_RUN ||
+	    sc->ns_nstate != IEEE80211_S_RUN)
+		return;
+
+	qwz_add_task(sc, sc->sc_nswq, &sc->updatechan_task);
+}
+
 void
 qwz_rx_agg_start(struct qwz_softc *sc, struct ieee80211_node *ni, uint8_t tid,
     uint16_t ssn, uint16_t winsize)
@@ -25217,6 +25339,7 @@ qwz_run(struct qwz_softc *sc)
 {
 	struct ieee80211com *ic = &sc->sc_ic;
 	struct ieee80211_node *ni = ic->ic_bss;
+	struct qwz_node *nq = (struct qwz_node *)ni;
 	struct qwz_vif *arvif = &sc->sc_vif;
 	uint8_t pdev_id = 0; /* TODO: derive pdev ID somehow? */
 	struct peer_assoc_params peer_arg;
@@ -25282,6 +25405,10 @@ qwz_run(struct qwz_softc *sc)
 	}
 
 	arvif->is_up = 1;
+	if (peer_arg.bw_40)
+		nq->chwidth = WMI_PEER_CHWIDTH_40MHZ;
+	else
+		nq->chwidth = WMI_PEER_CHWIDTH_20MHZ;
 #if 0
 	arvif->rekey_data.enable_offload = 0;
 #endif
@@ -25354,6 +25481,7 @@ qwz_attach(struct qwz_softc *sc)
 
 	task_set(&sc->init_task, qwz_init_task, sc);
 	task_set(&sc->newstate_task, qwz_newstate_task, sc);
+	task_set(&sc->updatechan_task, qwz_updatechan_task, sc);
 	task_set(&sc->setkey_task, qwz_setkey_task, sc);
 	task_set(&sc->ba_task, qwz_ba_task, sc);
 	task_set(&sc->bgscan_task, qwz_bgscan_task, sc);
diff --git a/sys/dev/ic/qwzvar.h b/sys/dev/ic/qwzvar.h
index cbc4b377424..732786c3e91 100644
--- a/sys/dev/ic/qwzvar.h
+++ b/sys/dev/ic/qwzvar.h
@@ -1986,6 +1986,7 @@ struct qwz_softc {
 	struct refcnt		task_refs;
 	struct taskq		*sc_nswq;
 	struct task		newstate_task;
+	struct task		updatechan_task;
 	enum ieee80211_state	ns_nstate;
 	int			ns_arg;
 
@@ -2189,10 +2190,12 @@ void	qwz_newstate_task(void *);
 int	qwz_bgscan(struct ieee80211com *);
 void	qwz_bgscan_done(struct ieee80211com *,
     struct ieee80211_node_switch_bss_arg *, size_t);
+void	qwz_updatechan(struct ieee80211com *);
 
 struct qwz_node {
 	struct ieee80211_node ni;
 	uint16_t peer_id;
+	enum wmi_peer_chwidth chwidth;
 	unsigned int flags;
 #define QWZ_NODE_FLAG_HAVE_PAIRWISE_KEY	0x01
 #define QWZ_NODE_FLAG_HAVE_GROUP_KEY	0x02
diff --git a/sys/dev/pci/if_qwz_pci.c b/sys/dev/pci/if_qwz_pci.c
index 7c98b79f8ad..9b8efbc7a15 100644
--- a/sys/dev/pci/if_qwz_pci.c
+++ b/sys/dev/pci/if_qwz_pci.c
@@ -1004,8 +1004,8 @@ qwz_pci_attach(struct device *parent, struct device *self, void *aux)
 	ic->ic_newstate = qwz_newstate;
 	ic->ic_set_key = qwz_set_key;
 	ic->ic_delete_key = qwz_delete_key;
-#if 0
 	ic->ic_updatechan = qwz_updatechan;
+#if 0
 	ic->ic_updateprot = qwz_updateprot;
 	ic->ic_updateslot = qwz_updateslot;
 	ic->ic_updateedca = qwz_updateedca;
-- 
2.55.0


From ca636d00a6b296588066c48bcea3aa0f5f03554f Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:55:25 +0200
Subject: [PATCH 3/6] sys/qwz: protect management frames

Based sys/dev/ic/qwx.c,v 1.96 , sys/dev/ic/qwxvar.h,v 1.32 ,
 sys/dev/pci/if_qwx_pci.c,v 1.30 , sys/dev/ic/qwx.c,v 1.109 ,
 sys/dev/ic/qwx.c,v 1.110 and sys/dev/ic/qwx.c,v 1.111

Enable protected management RX/TX and software BIP. Send protected
deauthentication before teardown and on other departures from RUN,
only after the WPA handshake and without duplicating an AP deauth.
Synchronize management TX buffers before submission.
---
 sys/dev/ic/qwz.c         | 144 ++++++++++++++++++++++++++++++---------
 sys/dev/ic/qwzvar.h      |   2 +
 sys/dev/pci/if_qwz_pci.c |   3 +-
 3 files changed, 114 insertions(+), 35 deletions(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index 244337d974f..a8474eee2ee 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -397,6 +397,49 @@ qwz_del_task(struct qwz_softc *sc, struct taskq *taskq, struct task *task)
 		refcnt_rele(&sc->task_refs);
 }
 
+void
+qwz_mfp_leave_done(struct ieee80211com *ic, struct ieee80211_node *ni)
+{
+	struct qwz_softc *sc = ic->ic_softc;
+	struct ifnet *ifp = &ic->ic_if;
+
+	if ((ifp->if_flags & IFF_RUNNING) &&
+	    ic->ic_state == IEEE80211_S_RUN &&
+	    (ni->ni_flags & IEEE80211_NODE_MFP)) {
+		sc->deauth_sent = 1;
+		wakeup(&sc->deauth_sent);
+	}
+}
+
+void
+qwz_mfp_leave(struct qwz_softc *sc)
+{
+	struct ieee80211com *ic = &sc->sc_ic;
+	struct ieee80211_node *ni = (void *)ic->ic_bss;
+
+	ic->ic_xflags |= IEEE80211_F_TX_MGMT_ONLY;
+	sc->deauth_sent = 0;
+
+	ni->ni_unref_cb = qwz_mfp_leave_done;
+	ni->ni_unref_arg = NULL;
+	ni->ni_unref_arg_size = 0;
+
+	/*
+	 * Send an authenticated deauth frame in order to let our AP know we
+	 * are leaving. This allows our AP to tear down MFP state cleanly.
+	 * Otherwise we would remain locked out of this AP until a timeout
+	 * of stale MFP state occurs at the AP, which might take a while.
+	 */
+	if (IEEE80211_SEND_MGMT(ic, ni, IEEE80211_FC0_SUBTYPE_DEAUTH,
+	    IEEE80211_REASON_AUTH_LEAVE) != 0) {
+		ni->ni_unref_cb = NULL;
+		return;
+	}
+
+	if (tsleep_nsec(&sc->deauth_sent, 0, "qwzlv", MSEC_TO_NSEC(500)) != 0)
+		ni->ni_unref_cb = NULL;
+}
+
 void
 qwz_del_task_all(struct qwz_softc *sc)
 {
@@ -417,6 +460,12 @@ qwz_stop(struct ifnet *ifp)
 
 	rw_assert_wrlock(&sc->ioctl_rwl);
 
+	if (ic->ic_opmode == IEEE80211_M_STA &&
+	    ic->ic_state == IEEE80211_S_RUN &&
+	    (ic->ic_bss->ni_flags & IEEE80211_NODE_MFP) &&
+	    ic->ic_bss->ni_port_valid)
+		qwz_mfp_leave(sc);
+
 	timeout_del(&sc->mon_reap_timer);
 
 	/* Disallow new tasks. */
@@ -695,7 +744,8 @@ qwz_set_key(struct ieee80211com *ic, struct ieee80211_node *ni,
 
 	if (test_bit(ATH12K_FLAG_HW_CRYPTO_DISABLED, sc->sc_flags) ||
 	    k->k_cipher == IEEE80211_CIPHER_WEP40 ||
-	    k->k_cipher == IEEE80211_CIPHER_WEP104)
+	    k->k_cipher == IEEE80211_CIPHER_WEP104 ||
+	    k->k_cipher == IEEE80211_CIPHER_BIP)
 		return ieee80211_set_key(ic, ni, k);
 
 	return qwz_queue_setkey_cmd(ic, ni, k, QWZ_ADD_KEY);
@@ -709,7 +759,8 @@ qwz_delete_key(struct ieee80211com *ic, struct ieee80211_node *ni,
 
 	if (test_bit(ATH12K_FLAG_HW_CRYPTO_DISABLED, sc->sc_flags) ||
 	    k->k_cipher == IEEE80211_CIPHER_WEP40 ||
-	    k->k_cipher == IEEE80211_CIPHER_WEP104) {
+	    k->k_cipher == IEEE80211_CIPHER_WEP104 ||
+	    k->k_cipher == IEEE80211_CIPHER_BIP) {
 		ieee80211_delete_key(ic, ni, k);
 		return;
 	}
@@ -1146,6 +1197,14 @@ qwz_newstate_task(void *arg)
 				break;
 			}
 
+			if (ic->ic_opmode == IEEE80211_M_STA &&
+			    (ifp->if_flags & IFF_RUNNING) &&
+			    (ic->ic_bss->ni_flags & IEEE80211_NODE_MFP) &&
+			    ic->ic_bss->ni_port_valid &&
+			    (nstate != IEEE80211_S_AUTH ||
+			     sc->ns_arg != IEEE80211_FC0_SUBTYPE_DEAUTH))
+				qwz_mfp_leave(sc);
+
 			err = qwz_run_stop(sc);
 			if (err)
 				goto out;
@@ -11984,27 +12043,15 @@ qwz_mgmt_rx_event(struct qwz_softc *sc, struct mbuf *m)
 
 	wh = mtod(m, struct ieee80211_frame *);
 	ni = ieee80211_find_rxnode(ic, wh);
-#if 0
-	/* In case of PMF, FW delivers decrypted frames with Protected Bit set.
-	 * Don't clear that. Also, FW delivers broadcast management frames
-	 * (ex: group privacy action frames in mesh) as encrypted payload.
-	 */
-	if (ieee80211_has_protected(hdr->frame_control) &&
-	    !is_multicast_ether_addr(ieee80211_get_DA(hdr))) {
-		status->flag |= RX_FLAG_DECRYPTED;
-
-		if (!ieee80211_is_robust_mgmt_frame(skb)) {
-			status->flag |= RX_FLAG_IV_STRIPPED |
-					RX_FLAG_MMIC_STRIPPED;
-			hdr->frame_control = __cpu_to_le16(fc &
-					     ~IEEE80211_FCTL_PROTECTED);
-		}
-	}
+	/* Firmware retains the protected bit on decrypted unicast frames. */
+	if ((wh->i_fc[1] & IEEE80211_FC1_PROTECTED) &&
+	    !IEEE80211_IS_MULTICAST(wh->i_addr1))
+		rxi.rxi_flags |= IEEE80211_RXI_HWDEC;
 
+#if 0
 	if (ieee80211_is_beacon(hdr->frame_control))
 		ath12k_mac_handle_beacon(ar, skb);
 #endif
-
 	DNPRINTF(QWZ_D_MGMT,
 	    "%s: event mgmt rx skb %p len %d ftype %02x stype %02x\n",
 	    __func__, m, m->m_pkthdr.len,
@@ -18381,7 +18428,7 @@ qwz_wmi_mgmt_send(struct qwz_softc *sc, struct qwz_vif *arvif, uint8_t pdev_id,
 	frame_tlv->header = FIELD_PREP(WMI_TLV_TAG, WMI_TAG_ARRAY_BYTE) |
 	    FIELD_PREP(WMI_TLV_LEN, buf_len_aligned);
 
-	memcpy(frame_tlv->value, mtod(frame, void *), buf_len);
+	m_copydata(frame, 0, buf_len, frame_tlv->value);
 #if 0 /* Not needed on OpenBSD? */
 	ath12k_ce_byte_swap(frame_tlv->value, buf_len);
 #endif
@@ -23872,34 +23919,63 @@ int
 qwz_mac_mgmt_tx_wmi(struct qwz_softc *sc, struct qwz_vif *arvif,
     uint8_t pdev_id, struct ieee80211_node *ni, struct mbuf *m)
 {
+	struct ieee80211com *ic = &sc->sc_ic;
 	struct qwz_txmgmt_queue *txmgmt = &arvif->txmgmt;
 	struct qwz_tx_data *tx_data;
+	struct ieee80211_frame *wh;
 	int buf_id;
 	int ret;
+	uint8_t subtype;
 
 	buf_id = txmgmt->cur;
 
 	DNPRINTF(QWZ_D_MAC, "%s: tx mgmt frame, buf id %d\n", __func__, buf_id);
 
-	if (txmgmt->queued >= nitems(txmgmt->data))
+	if (txmgmt->queued >= nitems(txmgmt->data)) {
+		m_freem(m);
 		return ENOSPC;
+	}
 
 	tx_data = &txmgmt->data[buf_id];
-#if 0
-	if (!(info->flags & IEEE80211_TX_CTL_HW_80211_ENCAP)) {
-		if ((ieee80211_is_action(hdr->frame_control) ||
-		     ieee80211_is_deauth(hdr->frame_control) ||
-		     ieee80211_is_disassoc(hdr->frame_control)) &&
-		     ieee80211_has_protected(hdr->frame_control)) {
-			skb_put(skb, IEEE80211_CCMP_MIC_LEN);
+
+	wh = mtod(m, struct ieee80211_frame *);
+	subtype = wh->i_fc[0] & IEEE80211_FC0_SUBTYPE_MASK;
+
+	if ((ni->ni_flags & IEEE80211_NODE_MFP) &&
+	    (wh->i_fc[1] & IEEE80211_FC1_PROTECTED) &&
+	    (subtype == IEEE80211_FC0_SUBTYPE_DISASSOC ||
+	     subtype == IEEE80211_FC0_SUBTYPE_DEAUTH ||
+	     subtype == IEEE80211_FC0_SUBTYPE_ACTION)) {
+		if (IEEE80211_IS_MULTICAST(wh->i_addr1)) {
+			struct ieee80211_key *k;
+
+			/* BIP needs to be done in software crypto. */
+			k = ieee80211_get_txkey(ic, wh, ni);
+			if ((m = ieee80211_encrypt(ic, m, k)) == NULL)
+				return ENOBUFS;
+		} else {
+			int off;
+
+			if (m_makespace(m, ieee80211_get_hdrlen(wh),
+			    IEEE80211_CCMP_HDRLEN, &off) == NULL) {
+				m_freem(m);
+				return ENOMEM;
+			}
+
+			if (m_makespace(m, m->m_pkthdr.len,
+			    IEEE80211_CCMP_MICLEN, &off) == NULL) {
+				m_freem(m);
+				return ENOMEM;
+			}
 		}
 	}
-#endif
+
 	ret = bus_dmamap_load_mbuf(sc->sc_dmat, tx_data->map,
 	    m, BUS_DMA_WRITE | BUS_DMA_NOWAIT);
 	if (ret && ret != EFBIG) {
 		printf("%s: failed to map mgmt Tx buffer: %d\n",
 		    sc->sc_dev.dv_xname, ret);
+		m_freem(m);
 		return ret;
 	}
 	if (ret) {
@@ -23918,10 +23994,14 @@ qwz_mac_mgmt_tx_wmi(struct qwz_softc *sc, struct qwz_vif *arvif,
 		}
 	}
 
+	bus_dmamap_sync(sc->sc_dmat, tx_data->map, 0,
+	    tx_data->map->dm_mapsize, BUS_DMASYNC_PREWRITE);
+
 	ret = qwz_wmi_mgmt_send(sc, arvif, pdev_id, buf_id, m, tx_data);
 	if (ret) {
 		printf("%s: failed to send mgmt frame: %d\n",
 		    sc->sc_dev.dv_xname, ret);
+		m_freem(m);
 		goto err_unmap_buf;
 	}
 	tx_data->ni = ni;
@@ -24788,12 +24868,8 @@ qwz_peer_assoc_h_crypto(struct qwz_softc *sc, struct qwz_vif *arvif,
 		if (ni->ni_rsnprotos == IEEE80211_PROTO_WPA)
 			arg->need_gtk_2_way = 1;
 	}
-#if 0
-	if (sta->mfp) {
-		/* TODO: Need to check if FW supports PMF? */
+	if (ni->ni_flags & IEEE80211_NODE_MFP)
 		arg->is_pmf_enabled = true;
-	}
-#endif
 }
 
 int
diff --git a/sys/dev/ic/qwzvar.h b/sys/dev/ic/qwzvar.h
index 732786c3e91..8b1730eab26 100644
--- a/sys/dev/ic/qwzvar.h
+++ b/sys/dev/ic/qwzvar.h
@@ -1990,6 +1990,8 @@ struct qwz_softc {
 	enum ieee80211_state	ns_nstate;
 	int			ns_arg;
 
+	int			deauth_sent;
+
 	/* Task for setting encryption keys and its arguments. */
 	struct task		setkey_task;
 	/*
diff --git a/sys/dev/pci/if_qwz_pci.c b/sys/dev/pci/if_qwz_pci.c
index 9b8efbc7a15..d5327eaa47c 100644
--- a/sys/dev/pci/if_qwz_pci.c
+++ b/sys/dev/pci/if_qwz_pci.c
@@ -967,7 +967,8 @@ qwz_pci_attach(struct device *parent, struct device *self, void *aux)
 	    IEEE80211_C_MONITOR |	/* monitor mode supported */
 #endif
 	    IEEE80211_C_SHSLOT |	/* short slot time supported */
-	    IEEE80211_C_SHPREAMBLE;	/* short preamble supported */
+	    IEEE80211_C_SHPREAMBLE |	/* short preamble supported */
+	    IEEE80211_C_MFP;		/* management frame protection */
 
 	ic->ic_sup_rates[IEEE80211_MODE_11A] = ieee80211_std_rateset_11a;
 	ic->ic_sup_rates[IEEE80211_MODE_11B] = ieee80211_std_rateset_11b;
-- 
2.55.0


From 6a6aee11347868cc230112836fb20ca3853ff685 Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:57:27 +0200
Subject: [PATCH 4/6] sys/qwz: block task admission before leaving an AP

Block new tasks before sending departure frames while allowing CE
transmission. Drain state work before teardown, and reopen admission
after successful initialization or when switching to the selected AP.
---
 sys/dev/ic/qwz.c    | 61 ++++++++++++++++++++++++++++++++++-----------
 sys/dev/ic/qwzvar.h |  1 +
 2 files changed, 48 insertions(+), 14 deletions(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index a8474eee2ee..4f111ba33a0 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -252,6 +252,7 @@ qwz_init(struct ifnet *ifp)
 			refcnt_init(&sc->task_refs);
 			ifq_clr_oactive(&ifp->if_snd);
 			ifp->if_flags |= IFF_RUNNING;
+			clear_bit(QWZ_FLAG_STOPPING, sc->sc_flags);
 			sc->ops.irq_enable(sc);
 			ieee80211_begin_scan(ifp);
 		}
@@ -360,6 +361,7 @@ qwz_init(struct ifnet *ifp)
 		ifq_clr_oactive(&ifp->if_snd);
 		ifp->if_flags |= IFF_RUNNING;
 
+		clear_bit(QWZ_FLAG_STOPPING, sc->sc_flags);
 		sc->ops.irq_enable(sc);
 		ieee80211_begin_scan(ifp);
 	}
@@ -379,7 +381,8 @@ qwz_add_task(struct qwz_softc *sc, struct taskq *taskq, struct task *task)
 {
 	int s = splnet();
 
-	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags)) {
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags)) {
 		splx(s);
 		return;
 	}
@@ -459,8 +462,13 @@ qwz_stop(struct ifnet *ifp)
 	int s = splnet();
 
 	rw_assert_wrlock(&sc->ioctl_rwl);
+	set_bit(QWZ_FLAG_STOPPING, sc->sc_flags);
+	qwz_del_task(sc, sc->sc_nswq, &sc->newstate_task);
+	qwz_del_task(sc, sc->sc_nswq, &sc->updatechan_task);
+	taskq_barrier(sc->sc_nswq);
 
-	if (ic->ic_opmode == IEEE80211_M_STA &&
+	if (sc->sc_vif.is_up &&
+	    ic->ic_opmode == IEEE80211_M_STA &&
 	    ic->ic_state == IEEE80211_S_RUN &&
 	    (ic->ic_bss->ni_flags & IEEE80211_NODE_MFP) &&
 	    ic->ic_bss->ni_port_valid)
@@ -489,7 +497,7 @@ qwz_stop(struct ifnet *ifp)
 
 	/* Tear down firmware-side association so we can re-associate. */
 	if (sc->num_created_vdevs != 0) {
-		if (ic->ic_state == IEEE80211_S_RUN)
+		if (sc->sc_vif.is_up)
 			qwz_run_stop(sc);
 		if (ic->ic_state >= IEEE80211_S_AUTH ||
 		    sc->num_started_vdevs > 0 || !TAILQ_EMPTY(&sc->peers))
@@ -722,6 +730,10 @@ qwz_queue_setkey_cmd(struct ieee80211com *ic, struct ieee80211_node *ni,
 	struct qwz_softc *sc = ic->ic_softc;
 	struct qwz_setkey_task_arg *a;
 
+	if (test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
+	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+		return ESHUTDOWN;
+
 	if (sc->setkey_nkeys >= nitems(sc->setkey_arg) ||
 	    k->k_id > WMI_MAX_KEY_INDEX)
 		return ENOSPC;
@@ -917,6 +929,7 @@ qwz_add_sta_key(struct qwz_softc *sc, struct ieee80211_node *ni,
 	}
 
 	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    (ic->ic_if.if_flags & IFF_RUNNING) == 0 ||
 	    ic->ic_state != IEEE80211_S_RUN ||
 	    sc->ns_nstate != IEEE80211_S_RUN)
@@ -947,6 +960,7 @@ qwz_add_sta_key(struct qwz_softc *sc, struct ieee80211_node *ni,
 		}
 
 		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 		    (ic->ic_if.if_flags & IFF_RUNNING) == 0 ||
 		    ic->ic_state != IEEE80211_S_RUN ||
 		    sc->ns_nstate != IEEE80211_S_RUN)
@@ -976,6 +990,7 @@ qwz_del_sta_key(struct qwz_softc *sc, struct ieee80211_node *ni,
 	}
 
 	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    (ic->ic_if.if_flags & IFF_RUNNING) == 0 ||
 	    ic->ic_state != IEEE80211_S_RUN ||
 	    sc->ns_nstate != IEEE80211_S_RUN)
@@ -1006,7 +1021,8 @@ qwz_setkey_task(void *arg)
 	int err = 0, s = splnet();
 
 	while (sc->setkey_nkeys > 0) {
-		if (err || test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+		if (err || test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags))
 			break;
 		a = sc->setkey_arg[sc->setkey_tail];
 		memset(&sc->setkey_arg[sc->setkey_tail], 0,
@@ -1131,6 +1147,7 @@ qwz_newstate(struct ieee80211com *ic, enum ieee80211_state nstate, int arg)
 
 	/* We may get triggered by received frames during qwz_stop(). */
 	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    !(ifp->if_flags & IFF_RUNNING))
 		return 0;
 
@@ -1170,7 +1187,8 @@ qwz_newstate_task(void *arg)
 	enum ieee80211_state ostate = ic->ic_state;
 	int err = 0, s = splnet();
 
-	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags)) {
+	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags)) {
 		/* qwz_stop() is waiting for us. */
 		refcnt_rele_wake(&sc->task_refs);
 		splx(s);
@@ -1228,7 +1246,8 @@ qwz_newstate_task(void *arg)
 		}
 
 		/* Die now if qwz_stop() was called while we were sleeping. */
-		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags)) {
+		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags)) {
 			refcnt_rele_wake(&sc->task_refs);
 			splx(s);
 			return;
@@ -1244,6 +1263,9 @@ next_scan:
 		err = qwz_scan(sc, 0);
 		if (err)
 			break;
+		if (test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
+		    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+			goto out;
 		if (ifp->if_flags & IFF_DEBUG)
 			printf("%s: %s -> %s\n", ifp->if_xname,
 			    ieee80211_state_name[ic->ic_state],
@@ -1269,6 +1291,7 @@ next_scan:
 	}
 out:
 	if (!test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) &&
+	    !test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) &&
 	    (ifp->if_flags & IFF_RUNNING)) {
 		if (err)
 			task_add(systq, &sc->init_task);
@@ -24478,7 +24501,8 @@ qwz_bgscan_task(void *arg)
 	if ((ic->ic_if.if_flags & IFF_RUNNING) &&
 	    ic->ic_state == IEEE80211_S_RUN &&
 	    sc->scan.state == ATH12K_SCAN_IDLE &&
-	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) &&
+	    !test_bit(QWZ_FLAG_STOPPING, sc->sc_flags))
 		qwz_scan(sc, 1);
 
 	refcnt_rele_wake(&sc->task_refs);
@@ -24518,11 +24542,13 @@ qwz_bgscan_done_task(void *arg)
 
 	/* Ensure that we start in expected state. */
 	if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    (ifp->if_flags & IFF_RUNNING) == 0 ||
 	    (ic->ic_flags & IEEE80211_F_BGSCAN) == 0 ||
 	    (ic->ic_xflags & IEEE80211_F_TX_MGMT_ONLY) == 0 ||
 	    test_bit(QWZ_FLAG_ROAMING, sc->sc_flags) ||
-	    ic->ic_state != IEEE80211_S_RUN) {
+	    ic->ic_state != IEEE80211_S_RUN ||
+	    sc->ns_nstate != IEEE80211_S_RUN) {
 		/* Don't touch the device, just return. */
 		rw_exit(&sc->ioctl_rwl);
 		refcnt_rele_wake(&sc->task_refs);
@@ -24530,6 +24556,12 @@ qwz_bgscan_done_task(void *arg)
 		return;
 	}
 
+	set_bit(QWZ_FLAG_STOPPING, sc->sc_flags);
+	qwz_del_task_all(sc);
+	/* State and channel tasks can sleep with the old peer in use. */
+	taskq_barrier(sc->sc_nswq);
+	qwz_setkey_clear(sc);
+
 	/* Send a DEAUTH frame to our old AP. */
 	err = IEEE80211_SEND_MGMT(ic, ni, IEEE80211_FC0_SUBTYPE_DEAUTH,
 	    IEEE80211_REASON_AUTH_LEAVE);
@@ -24542,11 +24574,6 @@ qwz_bgscan_done_task(void *arg)
 	/* Disallow new tasks. */
 	set_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags);
 
-	qwz_del_task_all(sc);
-	/* State and channel tasks can sleep with the old peer in use. */
-	taskq_barrier(sc->sc_nswq);
-	qwz_setkey_clear(sc);
-
 	/* Wait for Tx queues to drain. */
 	for (i = 0; i < sc->hw_params.max_tx_ring; i++) {
 		struct dp_tx_ring *tx_ring = &dp->tx_ring[i];
@@ -24619,6 +24646,7 @@ qwz_bgscan_done_task(void *arg)
 
 	/* Allow roaming to proceed. */
 	set_bit(QWZ_FLAG_ROAMING, sc->sc_flags);
+	clear_bit(QWZ_FLAG_STOPPING, sc->sc_flags);
 	ifp->if_flags |= IFF_RUNNING;
 	ni->ni_unref_arg = sc->bgscan_unref_arg;
 	ni->ni_unref_arg_size = sc->bgscan_unref_arg_size;
@@ -25090,6 +25118,7 @@ qwz_updatechan_task(void *arg)
 
 	if ((ifp->if_flags & IFF_RUNNING) == 0 ||
 	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    ic->ic_state != IEEE80211_S_RUN ||
 	    sc->ns_nstate != IEEE80211_S_RUN)
 		goto out;
@@ -25124,6 +25153,7 @@ qwz_updatechan_task(void *arg)
 
 		if ((ifp->if_flags & IFF_RUNNING) == 0 ||
 		    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 		    sc->ns_nstate != IEEE80211_S_RUN)
 			goto out;
 
@@ -25155,6 +25185,7 @@ qwz_updatechan_task(void *arg)
 
 		if ((ifp->if_flags & IFF_RUNNING) == 0 ||
 		    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 		    sc->ns_nstate != IEEE80211_S_RUN)
 			goto out;
 
@@ -25185,6 +25216,7 @@ qwz_updatechan(struct ieee80211com *ic)
 
 	if ((ic->ic_if.if_flags & IFF_RUNNING) == 0 ||
 	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
 	    ic->ic_state != IEEE80211_S_RUN ||
 	    sc->ns_nstate != IEEE80211_S_RUN)
 		return;
@@ -25260,7 +25292,8 @@ qwz_ba_task(void *arg)
 	int tid;
 
 	for (tid = 0; tid < IEEE80211_NUM_TID; tid++) {
-		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags))
+		if (test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+		    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags))
 			break;
 		if (sc->ba_rx.start_tidmask & (1 << tid)) {
 			struct ieee80211_rx_ba *ba = &ni->ni_rx_ba[tid];
diff --git a/sys/dev/ic/qwzvar.h b/sys/dev/ic/qwzvar.h
index 8b1730eab26..9d1073d6d87 100644
--- a/sys/dev/ic/qwzvar.h
+++ b/sys/dev/ic/qwzvar.h
@@ -422,6 +422,7 @@ enum ath12k_dev_flags {
 	ATH12K_FLAG_DEVICE_INIT_DONE,
 	ATH12K_FLAG_MULTI_MSI_VECTORS,
 	QWZ_FLAG_ROAMING,
+	QWZ_FLAG_STOPPING,
 };
 
 enum ath12k_scan_state {
-- 
2.55.0


From feabfc5bad34a79b19b1fee7d81817d70ded2b4f Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:57:42 +0200
Subject: [PATCH 5/6] sys/qwz: cancel blocked roaming

Cancel pending roaming when lock contention or interface state prevents
the handoff. Release its argument and resume data only when the current
association remains active, preserving pauses owned by other
transitions.
---
 sys/dev/ic/qwz.c | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index 4f111ba33a0..fe84ca12d4d 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -24520,6 +24520,31 @@ qwz_bgscan(struct ieee80211com *ic)
 	return 0;
 }
 
+void
+qwz_bgscan_cancel(struct qwz_softc *sc)
+{
+	struct ieee80211com *ic = &sc->sc_ic;
+	struct ifnet *ifp = &ic->ic_if;
+
+	if (sc->bgscan_unref_arg == NULL)
+		return;
+
+	free(sc->bgscan_unref_arg, M_DEVBUF, sc->bgscan_unref_arg_size);
+	sc->bgscan_unref_arg = NULL;
+	sc->bgscan_unref_arg_size = 0;
+	ic->ic_flags &= ~IEEE80211_F_BGSCAN;
+
+	if ((ifp->if_flags & IFF_RUNNING) &&
+	    ic->ic_state == IEEE80211_S_RUN &&
+	    sc->ns_nstate == IEEE80211_S_RUN &&
+	    !test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) &&
+	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) &&
+	    !test_bit(QWZ_FLAG_ROAMING, sc->sc_flags)) {
+		ic->ic_xflags &= ~IEEE80211_F_TX_MGMT_ONLY;
+		(*ifp->if_start)(ifp);
+	}
+}
+
 void
 qwz_bgscan_done_task(void *arg)
 {
@@ -24535,6 +24560,7 @@ qwz_bgscan_done_task(void *arg)
 
 	/* Prevent races with ifconfig commands. */
 	if (rw_enter(&sc->ioctl_rwl, RW_WRITE | RW_NOSLEEP) != 0) {
+		qwz_bgscan_cancel(sc);
 		refcnt_rele_wake(&sc->task_refs);
 		splx(s);
 		return;
@@ -24549,7 +24575,7 @@ qwz_bgscan_done_task(void *arg)
 	    test_bit(QWZ_FLAG_ROAMING, sc->sc_flags) ||
 	    ic->ic_state != IEEE80211_S_RUN ||
 	    sc->ns_nstate != IEEE80211_S_RUN) {
-		/* Don't touch the device, just return. */
+		qwz_bgscan_cancel(sc);
 		rw_exit(&sc->ioctl_rwl);
 		refcnt_rele_wake(&sc->task_refs);
 		splx(s);
-- 
2.55.0


From 54ff99c2d1dedc649391fa3f6a1ff7898740510f Mon Sep 17 00:00:00 2001
From: "Kirill A. Korinsky" <kirill@korins.ky>
Date: Sun, 27 Sep 2026 12:57:45 +0200
Subject: [PATCH 6/6] sys/qwz: clear failed scan state

Clear background scan state when queued work is skipped or fails,
allowing later scans to proceed. Preserve a handoff already queued by
scan completion.
---
 sys/dev/ic/qwz.c | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/sys/dev/ic/qwz.c b/sys/dev/ic/qwz.c
index fe84ca12d4d..1cec75c1c20 100644
--- a/sys/dev/ic/qwz.c
+++ b/sys/dev/ic/qwz.c
@@ -24345,6 +24345,7 @@ qwz_scan(struct qwz_softc *sc, int bgscan)
 	arg = malloc(sizeof(*arg), M_DEVBUF, M_ZERO | M_NOWAIT);
 	if (!arg) {
 		ret = ENOMEM;
+		sc->scan.state = ATH12K_SCAN_IDLE;
 		goto exit;
 	}
 
@@ -24385,6 +24386,7 @@ qwz_scan(struct qwz_softc *sc, int bgscan)
 
 		if (!arg->chan_list) {
 			ret = ENOMEM;
+			sc->scan.state = ATH12K_SCAN_IDLE;
 			goto exit;
 		}
 
@@ -24496,14 +24498,20 @@ qwz_bgscan_task(void *arg)
 {
 	struct qwz_softc *sc = arg;
 	struct ieee80211com *ic = &sc->sc_ic;
-	int s = splnet();
+	int ret = EBUSY, s = splnet();
 
 	if ((ic->ic_if.if_flags & IFF_RUNNING) &&
 	    ic->ic_state == IEEE80211_S_RUN &&
+	    sc->ns_nstate == IEEE80211_S_RUN &&
+	    (ic->ic_flags & IEEE80211_F_BGSCAN) &&
 	    sc->scan.state == ATH12K_SCAN_IDLE &&
 	    !test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) &&
 	    !test_bit(QWZ_FLAG_STOPPING, sc->sc_flags))
-		qwz_scan(sc, 1);
+		ret = qwz_scan(sc, 1);
+
+	/* Scan completion may already have handed ownership to roaming. */
+	if (ret != 0 && sc->bgscan_unref_arg == NULL)
+		ic->ic_flags &= ~IEEE80211_F_BGSCAN;
 
 	refcnt_rele_wake(&sc->task_refs);
 	splx(s);
@@ -24514,10 +24522,23 @@ qwz_bgscan(struct ieee80211com *ic)
 {
 	struct ifnet *ifp = &ic->ic_if;
 	struct qwz_softc *sc = ifp->if_softc;
+	int ret = EBUSY, s = splnet();
+
+	if ((ifp->if_flags & IFF_RUNNING) == 0 ||
+	    ic->ic_state != IEEE80211_S_RUN ||
+	    sc->ns_nstate != IEEE80211_S_RUN ||
+	    sc->scan.state != ATH12K_SCAN_IDLE ||
+	    test_bit(ATH12K_FLAG_CRASH_FLUSH, sc->sc_flags) ||
+	    test_bit(QWZ_FLAG_STOPPING, sc->sc_flags) ||
+	    task_pending(&sc->bgscan_task))
+		goto out;
 
 	qwz_add_task(sc, systq, &sc->bgscan_task);
+	ret = 0;
 
-	return 0;
+out:
+	splx(s);
+	return ret;
 }
 
 void
-- 
2.55.0



-- 
wbr, Kirill