Linux wireless drivers development
 help / color / mirror / Atom feed
* Re: [PATCH 2/3] wifi: mt76: mt792x: Add TX page pool support for IOMMU-enabled systems
From: Lorenzo Bianconi @ 2026-07-09  8:27 UTC (permalink / raw)
  To: Eason Lai
  Cc: nbd, linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai
In-Reply-To: <20260709075558.1654164-3-eason.lai@mediatek.com>

[-- Attachment #1: Type: text/plain, Size: 15545 bytes --]

On Jul 09, Eason Lai wrote:
> From: Eason Lai <Eason.Lai@mediatek.com>
> 
> Add support for TX page pool allocation in mt792x devices when IOMMU is
> enabled. This optimization reduces DMA mapping overhead by
> pre-allocating and reusing page pool buffers for TX operations.

Can you please provide some test results here? In particular regarding the
resulting throughput.

> 
> Key changes:
> - Export DMA helper functions and ops for mt792x usage
>   - Add tx_prealloc_enabled flag to track page pool state
>     - Implement mt792x-specific DMA queue operations with TX page pool
>       - Create page pools per TX queue when IOMMU is detected
>         - Handle page pool buffer cleanup in both success and error
>           paths
> - Add proper skip_unmap flag handling for page pool buffers
> 
> The page pool path is used for linear skbs without fragments, falling
> back to standard DMA mapping for complex skb structures.
> 
> Signed-off-by: Eason Lai <Eason.Lai@mediatek.com>
> ---

[...]

>  
> -static void
> +void
>  mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q)
>  {
>  	wmb();
> @@ -409,6 +417,7 @@ mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q)
>  	else
>  		Q_WRITE(q, cpu_idx, q->head);
>  }
> +EXPORT_SYMBOL_GPL(mt76_dma_kick_queue);

this is already available in mt76_queue_ops struct.

>  
>  static void
>  mt76_dma_tx_cleanup(struct mt76_dev *dev, struct mt76_queue *q, bool flush)
> @@ -1152,7 +1161,7 @@ mt76_dma_init(struct mt76_dev *dev,
>  	return 0;
>  }
>  
> -static const struct mt76_queue_ops mt76_dma_ops = {
> +const struct mt76_queue_ops mt76_dma_ops = {
>  	.init = mt76_dma_init,
>  	.alloc = mt76_dma_alloc_queue,
>  	.reset_q = mt76_dma_queue_reset,
> @@ -1164,6 +1173,7 @@ static const struct mt76_queue_ops mt76_dma_ops = {
>  	.rx_reset = mt76_dma_rx_reset,
>  	.kick = mt76_dma_kick_queue,
>  };
> +EXPORT_SYMBOL_GPL(mt76_dma_ops);
>  
>  void mt76_dma_attach(struct mt76_dev *dev)
>  {
> diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
> index 927c21536f4e..25e24fa36eca 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt76.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt76.h
> @@ -957,6 +957,8 @@ struct mt76_dev {
>  	int tx_dma_idx[4];
>  	enum mt76_hwrro_mode hwrro_mode;
>  
> +	bool tx_prealloc_enabled;
> +
>  	struct mt76_worker tx_worker;
>  	struct napi_struct tx_napi;
>  
> @@ -1785,7 +1787,16 @@ mt76_tx_status_get_hw(struct mt76_dev *dev, struct sk_buff *skb)
>  
>  void mt76_put_txwi(struct mt76_dev *dev, struct mt76_txwi_cache *t);
>  void mt76_put_rxwi(struct mt76_dev *dev, struct mt76_txwi_cache *t);
> +struct mt76_txwi_cache *mt76_get_txwi(struct mt76_dev *dev);
>  struct mt76_txwi_cache *mt76_get_rxwi(struct mt76_dev *dev);
> +int mt76_dma_add_buf(struct mt76_dev *dev, struct mt76_queue *q,
> +		     struct mt76_queue_buf *buf, int nbufs, u32 info,
> +		     struct sk_buff *skb, void *txwi);
> +void mt76_dma_tx_cleanup_idx(struct mt76_dev *dev, struct mt76_queue *q, int idx,
> +			     struct mt76_queue_entry *prev_e);
> +void mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q);
> +void mt76_dma_sync_idx(struct mt76_dev *dev, struct mt76_queue *q);
> +extern const struct mt76_queue_ops mt76_dma_ops;
>  void mt76_free_pending_rxwi(struct mt76_dev *dev);
>  void mt76_rx_complete(struct mt76_dev *dev, struct sk_buff_head *frames,
>  		      struct napi_struct *napi);
> diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c b/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
> index 0339e2e7ab60..d4b36b0832b8 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
> @@ -250,6 +250,12 @@ void mt76_connac_txp_skb_unmap(struct mt76_dev *dev,
>  {
>  	struct mt76_connac_txp_common *txp;
>  
> +	if (t->page_pool_buf) {
> +		mt76_put_page_pool_buf(t->page_pool_buf, false);
> +		t->page_pool_buf = NULL;
> +		return;
> +	}
> +
>  	txp = mt76_connac_txwi_to_txp(dev, t);
>  	if (is_mt76_fw_txp(dev))
>  		mt76_connac_txp_skb_unmap_fw(dev, &txp->fw);
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> index c4161754c01d..3d69d8c67dea 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
> @@ -214,7 +214,7 @@ static int mt7925_dma_init(struct mt792x_dev *dev)
>  {
>  	int ret;
>  
> -	mt76_dma_attach(&dev->mt76);
> +	mt792x_dma_attach(&dev->mt76);
>  
>  	ret = mt792x_dma_disable(dev, true);
>  	if (ret)
> @@ -263,6 +263,8 @@ static int mt7925_dma_init(struct mt792x_dev *dev)
>  			  mt792x_poll_tx);
>  	napi_enable(&dev->mt76.tx_napi);
>  
> +	mt792x_dma_tx_page_pool_init(dev);
> +
>  	return mt792x_dma_enable(dev);
>  }
>  
> diff --git a/drivers/net/wireless/mediatek/mt76/mt792x.h b/drivers/net/wireless/mediatek/mt76/mt792x.h
> index 4ff93f2cd624..7ac0318dc249 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt792x.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt792x.h
> @@ -418,8 +418,10 @@ void mt792x_sta_statistics(struct ieee80211_hw *hw,
>  			   struct station_info *sinfo);
>  void mt792x_set_coverage_class(struct ieee80211_hw *hw, int radio_idx,
>  			       s16 coverage_class);
> +void mt792x_dma_attach(struct mt76_dev *dev);
>  void mt792x_dma_cleanup(struct mt792x_dev *dev);
>  int mt792x_dma_enable(struct mt792x_dev *dev);
> +int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev);
>  int mt792x_wpdma_reset(struct mt792x_dev *dev, bool force);
>  int mt792x_wpdma_reinit_cond(struct mt792x_dev *dev);
>  int mt792x_dma_disable(struct mt792x_dev *dev, bool force);
> diff --git a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
> index 002aece857b2..b341f1cb3ce0 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
> @@ -3,6 +3,9 @@
>  
>  #include <linux/module.h>
>  #include <linux/firmware.h>
> +#include <linux/iommu.h>
> +#include <net/page_pool/helpers.h>
> +#include <linux/of.h>
>  
>  #include "mt792x.h"
>  #include "dma.h"
> @@ -266,6 +269,323 @@ int mt792x_wpdma_reinit_cond(struct mt792x_dev *dev)
>  }
>  EXPORT_SYMBOL_GPL(mt792x_wpdma_reinit_cond);
>  
> +static int mt792x_create_tx_page_pool(struct mt76_dev *dev, struct mt76_queue *q)
> +{
> +	struct page_pool_params pp_params = {
> +		.order = 0,
> +		.flags = 0,
> +		.nid = NUMA_NO_NODE,
> +		.dev = dev->dma_dev,
> +	};
> +
> +	if (!q || !dev->tx_prealloc_enabled)
> +		return 0;
> +
> +	if (!mt76_is_mmio(dev))
> +		return 0;

do you really need this check? you already check it in
mt792x_dma_tx_page_pool_init().

> +
> +	pp_params.pool_size = 256;
> +	pp_params.flags |= PP_FLAG_DMA_MAP;
> +	pp_params.dma_dir = DMA_BIDIRECTIONAL;
> +	pp_params.max_len = PAGE_SIZE;
> +	pp_params.offset = 0;
> +
> +	q->page_pool = page_pool_create(&pp_params);
> +	if (IS_ERR(q->page_pool)) {
> +		int err = PTR_ERR(q->page_pool);
> +
> +		q->page_pool = NULL;
> +		dev_warn(dev->dev, "Failed to create TX page pool for queue %d (err=%d)\n",
> +			 q->hw_idx, err);
> +		return 0;
> +	}
> +
> +	return 0;
> +}
> +
> +int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev)
> +{
> +	struct mt76_dev *mdev = &dev->mt76;
> +	int i, ret, pool_count = 0;
> +
> +	if (!iommu_get_domain_for_dev(mdev->dma_dev))
> +		return 0;
> +
> +	if (!mt76_is_mmio(mdev))
> +		return 0;
> +
> +	mdev->tx_prealloc_enabled = true;
> +
> +	for (i = 0; i < ARRAY_SIZE(mdev->phy.q_tx); i++) {
> +		struct mt76_queue *q = mdev->phy.q_tx[i];
> +
> +		if (!q)
> +			continue;
> +
> +		ret = mt792x_create_tx_page_pool(mdev, q);
> +		if (ret)
> +			return ret;
> +
> +		if (q->page_pool)
> +			pool_count++;
> +	}
> +
> +	if (pool_count > 0)
> +		dev_info(mdev->dev,
> +			 "IOMMU enabled, created %d TX page pools\n", pool_count);
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(mt792x_dma_tx_page_pool_init);
> +
> +static void mt792x_dma_tx_page_pool_cleanup(struct mt792x_dev *dev)
> +{
> +	struct mt76_dev *mdev = &dev->mt76;
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(mdev->phy.q_tx); i++) {
> +		struct mt76_queue *q = mdev->phy.q_tx[i];
> +
> +		if (!q || !q->page_pool)
> +			continue;
> +
> +		page_pool_destroy(q->page_pool);
> +		q->page_pool = NULL;
> +	}
> +
> +	mdev->tx_prealloc_enabled = false;
> +}
> +
> +static void *
> +mt792x_dma_tx_alloc_page_pool_buf(struct mt76_dev *dev, struct mt76_queue *q,
> +				  struct sk_buff *skb, dma_addr_t *dma_addr,
> +				  int *buf_len)
> +{
> +	struct page *page;
> +	void *buf;
> +	int len;
> +	u32 offset;
> +
> +	if (!q->page_pool || !dev->tx_prealloc_enabled)
> +		return NULL;
> +
> +	len = skb_headlen(skb);
> +	if (len > PAGE_SIZE)
> +		return NULL;
> +
> +	buf = mt76_get_page_pool_buf(q, &offset, len);
> +	if (!buf)
> +		return NULL;
> +
> +	page = virt_to_head_page(buf);
> +	*dma_addr = page_pool_get_dma_addr(page) + offset;
> +	if (unlikely(!*dma_addr)) {
> +		dev_warn_ratelimited(dev->dev, "Page pool returned NULL DMA address\n");
> +		mt76_put_page_pool_buf(buf, false);
> +		return NULL;
> +	}
> +
> +	*buf_len = len;
> +
> +	dma_sync_single_for_cpu(dev->dma_dev, *dma_addr, len, DMA_TO_DEVICE);
> +	skb_copy_from_linear_data(skb, buf, len);

I would like to see some results here since usually it is not a good idea to
copy all the packets (even if it is just the linear part).

> +	dma_sync_single_for_device(dev->dma_dev, *dma_addr, len, DMA_TO_DEVICE);
> +
> +	return buf;
> +}
> +
> +static int
> +mt792x_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
> +			enum mt76_txq_id qid, struct sk_buff *skb,
> +			struct mt76_wcid *wcid, struct ieee80211_sta *sta)
> +{
> +	struct ieee80211_tx_status status = {
> +		.sta = sta,
> +	};
> +	struct mt76_tx_info tx_info = {
> +		.skb = skb,
> +	};
> +	struct mt76_dev *dev = phy->dev;
> +	struct ieee80211_hw *hw;
> +	int len, n = 0, ret = -ENOMEM;
> +	struct mt76_txwi_cache *t;
> +	struct sk_buff *iter;
> +	dma_addr_t addr;
> +	u8 *txwi;
> +
> +	if (test_bit(MT76_RESET, &phy->state))
> +		goto free_skb;
> +
> +	t = mt76_get_txwi(dev);
> +	if (!t)
> +		goto free_skb;
> +
> +	txwi = mt76_get_txwi_ptr(dev, t);
> +
> +	skb->prev = NULL;
> +	skb->next = NULL;
> +	if (dev->drv->drv_flags & MT_DRV_TX_ALIGNED4_SKBS)
> +		mt76_insert_hdr_pad(skb);
> +
> +	len = skb_headlen(skb);
> +
> +	if (dev->tx_prealloc_enabled && q->page_pool &&
> +	    !skb_has_frag_list(skb) && !skb_shinfo(skb)->nr_frags) {
> +		void *buf;
> +		int pp_len;
> +
> +		buf = mt792x_dma_tx_alloc_page_pool_buf(dev, q, skb, &addr, &pp_len);
> +		if (buf) {
> +			t->page_pool_buf = buf;
> +			len = pp_len;
> +
> +			tx_info.buf[n].addr = t->dma_addr;
> +			tx_info.buf[n++].len = dev->drv->txwi_size;
> +			tx_info.buf[n].addr = addr;
> +			tx_info.buf[n].len = len;
> +			tx_info.buf[n].skip_unmap = true;
> +			n++;
> +
> +			goto skip_dma_map;
> +		}
> +	}
> +
> +	addr = dma_map_single(dev->dma_dev, skb->data, len, DMA_TO_DEVICE);
> +	if (unlikely(dma_mapping_error(dev->dma_dev, addr)))
> +		goto free;
> +
> +	t->page_pool_buf = NULL;
> +
> +	tx_info.buf[n].addr = t->dma_addr;
> +	tx_info.buf[n++].len = dev->drv->txwi_size;
> +	tx_info.buf[n].addr = addr;
> +	tx_info.buf[n++].len = len;
> +
> +skip_dma_map:
> +	skb_walk_frags(skb, iter) {
> +		if (n == ARRAY_SIZE(tx_info.buf))
> +			goto unmap;
> +
> +		addr = dma_map_single(dev->dma_dev, iter->data, iter->len,
> +				      DMA_TO_DEVICE);
> +		if (unlikely(dma_mapping_error(dev->dma_dev, addr)))
> +			goto unmap;
> +
> +		tx_info.buf[n].addr = addr;
> +		tx_info.buf[n].skip_unmap = false;
> +		tx_info.buf[n++].len = iter->len;
> +	}
> +	tx_info.nbuf = n;
> +
> +	if (q->queued + (tx_info.nbuf + 1) / 2 >= q->ndesc - 1) {
> +		ret = -ENOMEM;
> +		goto unmap;
> +	}
> +
> +	dma_sync_single_for_cpu(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
> +				DMA_TO_DEVICE);
> +	ret = dev->drv->tx_prepare_skb(dev, txwi, qid, wcid, sta, &tx_info);
> +	dma_sync_single_for_device(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
> +				   DMA_TO_DEVICE);
> +	if (ret < 0)
> +		goto unmap;
> +
> +	return mt76_dma_add_buf(dev, q, tx_info.buf, tx_info.nbuf,
> +				tx_info.info, tx_info.skb, t);
> +
> +unmap:
> +	for (n--; n > 0; n--) {
> +		if (!tx_info.buf[n].skip_unmap)
> +			dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
> +					 tx_info.buf[n].len, DMA_TO_DEVICE);
> +	}
> +
> +	if (t->page_pool_buf) {
> +		mt76_put_page_pool_buf(t->page_pool_buf, false);
> +		t->page_pool_buf = NULL;
> +	}
> +
> +free:
> +#ifdef CONFIG_NL80211_TESTMODE
> +	if (mt76_is_testmode_skb(dev, skb, &hw)) {
> +		struct mt76_phy *phy = hw->priv;
> +
> +		if (tx_info.skb == phy->test.tx_skb)
> +			phy->test.tx_done--;
> +	}
> +#endif
> +
> +	mt76_put_txwi(dev, t);
> +
> +free_skb:
> +	status.skb = tx_info.skb;
> +	hw = mt76_tx_status_get_hw(dev, tx_info.skb);
> +	spin_lock_bh(&dev->rx_lock);
> +	ieee80211_tx_status_ext(hw, &status);
> +	spin_unlock_bh(&dev->rx_lock);
> +
> +	return ret;
> +}
> +
> +static void
> +mt792x_dma_tx_cleanup(struct mt76_dev *dev, struct mt76_queue *q, bool flush)

this is almost a copy of mt76_dma_tx_cleanup(). Can we do something better
here?

> +{
> +	struct mt76_queue_entry entry;
> +	int last;
> +
> +	if (!q || !q->ndesc)
> +		return;
> +
> +	spin_lock_bh(&q->cleanup_lock);
> +	if (flush)
> +		last = -1;
> +	else
> +		last = Q_READ(q, dma_idx);
> +
> +	while (q->queued > 0 && q->tail != last) {
> +		mt76_dma_tx_cleanup_idx(dev, q, q->tail, &entry);
> +		mt76_npu_txdesc_cleanup(q, q->tail);
> +		mt76_queue_tx_complete(dev, q, &entry);
> +
> +		if (entry.txwi) {
> +			if (entry.skb && entry.txwi->page_pool_buf) {
> +				mt76_put_page_pool_buf(entry.txwi->page_pool_buf, false);
> +				entry.txwi->page_pool_buf = NULL;
> +			}
> +
> +			if (!(dev->drv->drv_flags & MT_DRV_TXWI_NO_FREE))
> +				mt76_put_txwi(dev, entry.txwi);
> +		}
> +
> +		if (!flush && q->tail == last)
> +			last = Q_READ(q, dma_idx);
> +	}
> +	spin_unlock_bh(&q->cleanup_lock);
> +
> +	if (flush) {
> +		spin_lock_bh(&q->lock);
> +		mt76_dma_sync_idx(dev, q);
> +		mt76_dma_kick_queue(dev, q);
> +		spin_unlock_bh(&q->lock);
> +	}
> +
> +	if (!q->queued)
> +		wake_up(&dev->tx_wait);
> +}
> +
> +static struct mt76_queue_ops mt792x_queue_ops;
> +
> +void mt792x_dma_attach(struct mt76_dev *dev)
> +{
> +	mt792x_queue_ops = mt76_dma_ops;

can you please take a look to mt7615_mmio_probe()?

Regards,
Lorenzo

> +
> +	mt792x_queue_ops.tx_queue_skb = mt792x_dma_tx_queue_skb;
> +	mt792x_queue_ops.tx_cleanup = mt792x_dma_tx_cleanup;
> +
> +	dev->queue_ops = &mt792x_queue_ops;
> +}
> +EXPORT_SYMBOL_GPL(mt792x_dma_attach);
> +
>  int mt792x_dma_disable(struct mt792x_dev *dev, bool force)
>  {
>  	/* disable WFDMA0 */
> @@ -326,6 +646,8 @@ void mt792x_dma_cleanup(struct mt792x_dev *dev)
>  		 MT_WFDMA0_RST_LOGIC_RST);
>  
>  	mt76_dma_cleanup(&dev->mt76);
> +
> +	mt792x_dma_tx_page_pool_cleanup(dev);
>  }
>  EXPORT_SYMBOL_GPL(mt792x_dma_cleanup);
>  
> -- 
> 2.45.2
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply

* Re: [PATCH 1/3] wifi: mt76: Separate skb and page_pool_buf pointers in mt76_txwi_cache
From: Lorenzo Bianconi @ 2026-07-09  8:19 UTC (permalink / raw)
  To: Eason Lai
  Cc: nbd, linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai
In-Reply-To: <20260709075558.1654164-2-eason.lai@mediatek.com>

[-- Attachment #1: Type: text/plain, Size: 6697 bytes --]

> From: Eason Lai <Eason.Lai@mediatek.com>
> 
> Refactor mt76_txwi_cache structure to use separate skb and page_pool_buf
> fields instead of a union with ptr. This improves type safety and makes
> the code more explicit about which pointer type is being used in
> different contexts.
> 
> Also add skip_unmap flag to tx_info.buf to handle cases where DMA
> unmapping should be skipped.
> 
> Signed-off-by: Eason Lai <Eason.Lai@mediatek.com>
> ---
>  drivers/net/wireless/mediatek/mt76/dma.c      | 29 ++++++++++++-------
>  drivers/net/wireless/mediatek/mt76/mt76.h     |  4 +--
>  .../net/wireless/mediatek/mt76/mt7996/mac.c   | 10 +++----
>  drivers/net/wireless/mediatek/mt76/tx.c       |  2 +-
>  drivers/net/wireless/mediatek/mt76/wed.c      |  6 ++--
>  5 files changed, 28 insertions(+), 23 deletions(-)
> 
> diff --git a/drivers/net/wireless/mediatek/mt76/dma.c b/drivers/net/wireless/mediatek/mt76/dma.c
> index f8c2fe5f2f58..2716278788bd 100644
> --- a/drivers/net/wireless/mediatek/mt76/dma.c
> +++ b/drivers/net/wireless/mediatek/mt76/dma.c
> @@ -43,7 +43,8 @@ mt76_alloc_rxwi(struct mt76_dev *dev)
>  	if (!t)
>  		return NULL;
>  
> -	t->ptr = NULL;
> +	t->skb = NULL;
> +	t->page_pool_buf = NULL;
>  	return t;
>  }
>  
> @@ -84,8 +85,11 @@ mt76_get_txwi(struct mt76_dev *dev)
>  {
>  	struct mt76_txwi_cache *t = __mt76_get_txwi(dev);
>  
> -	if (t)
> +	if (t) {
> +		t->skb = NULL;
> +		t->page_pool_buf = NULL;
>  		return t;
> +	}
>  
>  	return mt76_alloc_txwi(dev);
>  }
> @@ -147,8 +151,8 @@ mt76_free_pending_rxwi(struct mt76_dev *dev)
>  
>  	local_bh_disable();
>  	while ((t = __mt76_get_rxwi(dev)) != NULL) {
> -		if (t->ptr)
> -			mt76_put_page_pool_buf(t->ptr, false);
> +		if (t->page_pool_buf)
> +			mt76_put_page_pool_buf(t->page_pool_buf, false);
>  		kfree(t);
>  	}
>  	local_bh_enable();
> @@ -475,14 +479,14 @@ mt76_dma_get_rxdmad_c_buf(struct mt76_dev *dev, struct mt76_queue *q,
>  	if (more)
>  		*more = !FIELD_GET(RRO_RXDMAD_DATA1_LS_MASK, data1);
>  
> -	buf = t->ptr;
> +	buf = t->page_pool_buf;
>  	ind_reason = FIELD_GET(RRO_RXDMAD_DATA2_IND_REASON_MASK, data2);
>  	if (ind_reason == MT_DMA_WED_IND_REASON_REPEAT ||
>  	    ind_reason == MT_DMA_WED_IND_REASON_OLDPKT) {
>  		mt76_put_page_pool_buf(buf, false);
>  		buf = ERR_PTR(-EAGAIN);
>  	}
> -	t->ptr = NULL;
> +	t->page_pool_buf = NULL;
>  	t->dma_addr = 0;
>  
>  	mt76_put_rxwi(dev, t);
> @@ -529,9 +533,9 @@ mt76_dma_get_buf(struct mt76_dev *dev, struct mt76_queue *q, int idx,
>  				SKB_WITH_OVERHEAD(q->buf_size),
>  				page_pool_get_dma_dir(q->page_pool));
>  
> -		buf = t->ptr;
> +		buf = t->page_pool_buf;
>  		t->dma_addr = 0;
> -		t->ptr = NULL;
> +		t->page_pool_buf = NULL;
>  
>  		mt76_put_rxwi(dev, t);
>  		if (drop)
> @@ -694,6 +698,7 @@ mt76_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
>  			goto unmap;
>  
>  		tx_info.buf[n].addr = addr;
> +		tx_info.buf[n].skip_unmap = false;
>  		tx_info.buf[n++].len = iter->len;
>  	}
>  	tx_info.nbuf = n;
> @@ -718,9 +723,11 @@ mt76_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
>  				tx_info.info, tx_info.skb, t);
>  
>  unmap:
> -	for (n--; n > 0; n--)
> -		dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
> -				 tx_info.buf[n].len, DMA_TO_DEVICE);
> +	for (n--; n > 0; n--) {
> +		if (!tx_info.buf[n].skip_unmap)
> +			dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
> +					 tx_info.buf[n].len, DMA_TO_DEVICE);
> +	}
>  
>  free:
>  #ifdef CONFIG_NL80211_TESTMODE
> diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
> index 527bef97e122..927c21536f4e 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt76.h
> +++ b/drivers/net/wireless/mediatek/mt76/mt76.h
> @@ -445,10 +445,8 @@ struct mt76_txwi_cache {
>  	struct list_head list;
>  	dma_addr_t dma_addr;
>  
> -	union {
>  		struct sk_buff *skb;
> -		void *ptr;
> -	};
> +	void *page_pool_buf;

if you do not rename the pointer here, the patch would be much less intrusive.

Regards,
Lorenzo

>  
>  	u8 qid;
>  	u8 phy_idx;
> diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
> index e2a83da3a09c..924b0dc0ff1e 100644
> --- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
> +++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
> @@ -1775,12 +1775,12 @@ static void mt7996_rx_token_put(struct mt7996_dev *dev)
>  		struct mt76_txwi_cache *t;
>  
>  		t = mt76_rx_token_release(&dev->mt76, i);
> -		if (!t || !t->ptr)
> +		if (!t || !t->page_pool_buf)
>  			continue;
>  
> -		mt76_put_page_pool_buf(t->ptr, false);
> +		mt76_put_page_pool_buf(t->page_pool_buf, false);
>  		t->dma_addr = 0;
> -		t->ptr = NULL;
> +		t->page_pool_buf = NULL;
>  
>  		mt76_put_rxwi(&dev->mt76, t);
>  	}
> @@ -1928,14 +1928,14 @@ void mt7996_rro_rx_process(struct mt76_dev *mdev, void *data)
>  				goto next_page;
>  
>  			qid = t->qid;
> -			buf = t->ptr;
> +			buf = t->page_pool_buf;
>  			q = &mdev->q_rx[qid];
>  			dma_sync_single_for_cpu(mdev->dma_dev, t->dma_addr,
>  						SKB_WITH_OVERHEAD(q->buf_size),
>  						page_pool_get_dma_dir(q->page_pool));
>  
>  			t->dma_addr = 0;
> -			t->ptr = NULL;
> +			t->page_pool_buf = NULL;
>  			mt76_put_rxwi(mdev, t);
>  			if (!buf)
>  				goto next_page;
> diff --git a/drivers/net/wireless/mediatek/mt76/tx.c b/drivers/net/wireless/mediatek/mt76/tx.c
> index 22f9690634c9..665156a7ea65 100644
> --- a/drivers/net/wireless/mediatek/mt76/tx.c
> +++ b/drivers/net/wireless/mediatek/mt76/tx.c
> @@ -899,7 +899,7 @@ int mt76_rx_token_consume(struct mt76_dev *dev, void *ptr,
>  	token = idr_alloc(&dev->rx_token, t, 0, dev->rx_token_size,
>  			  GFP_ATOMIC);
>  	if (token >= 0) {
> -		t->ptr = ptr;
> +		t->page_pool_buf = ptr;
>  		t->dma_addr = phys;
>  	}
>  	spin_unlock_bh(&dev->rx_token_lock);
> diff --git a/drivers/net/wireless/mediatek/mt76/wed.c b/drivers/net/wireless/mediatek/mt76/wed.c
> index ed657d952de2..e1cf81d722b8 100644
> --- a/drivers/net/wireless/mediatek/mt76/wed.c
> +++ b/drivers/net/wireless/mediatek/mt76/wed.c
> @@ -15,11 +15,11 @@ void mt76_wed_release_rx_buf(struct mtk_wed_device *wed)
>  		struct mt76_txwi_cache *t;
>  
>  		t = mt76_rx_token_release(dev, i);
> -		if (!t || !t->ptr)
> +		if (!t || !t->page_pool_buf)
>  			continue;
>  
> -		mt76_put_page_pool_buf(t->ptr, false);
> -		t->ptr = NULL;
> +		mt76_put_page_pool_buf(t->page_pool_buf, false);
> +		t->page_pool_buf = NULL;
>  
>  		mt76_put_rxwi(dev, t);
>  	}
> -- 
> 2.45.2
> 

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply

* [PATCH 3/3] wifi: mt76: mt792x: Restrict TX page pool to MT8196 platform
From: Eason Lai @ 2026-07-09  7:55 UTC (permalink / raw)
  To: nbd, lorenzo
  Cc: linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai,
	eason.lai, Eason Lai
In-Reply-To: <20260709075558.1654164-1-eason.lai@mediatek.com>

From: Eason Lai <Eason.Lai@mediatek.com>

The TX page pool optimization significantly improves performance on
MT8196 when IOMMU is enabled, but this benefit is platform-specific.
Restrict this feature to MT8196 where it has been tested and validated.

On MT8196 with IOMMU enabled, DMA mapping overhead increases
dramatically compared to IOMMU-disabled configurations:

dma_unmap_single() in mt76_connac_txp_skb_unmap_hw() (NAPI context):
  - IOMMU disabled: 181.25 ns (avg over 20,000 calls)
      - IOMMU enabled:  5216.19 ns (avg over 21,000 calls)

dma_map_single() in mt76_dma_tx_queue_skb() (workqueue context):
  - IOMMU disabled: 880.20 ns (avg over 20,000 calls)
      - IOMMU enabled:  2106.65 ns (avg over 20,000 calls)

The TX page pool mitigates this overhead by reusing DMA mappings,
but should only be enabled on platforms where it has been verified
to work correctly.

Signed-off-by: Eason Lai <Eason.Lai@mediatek.com>
---
 drivers/net/wireless/mediatek/mt76/mt792x_dma.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
index b341f1cb3ce0..6d5725a5b10f 100644
--- a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
+++ b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
@@ -307,6 +307,7 @@ int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev)
 {
 	struct mt76_dev *mdev = &dev->mt76;
 	int i, ret, pool_count = 0;
+	bool is_mt8196;
 
 	if (!iommu_get_domain_for_dev(mdev->dma_dev))
 		return 0;
@@ -314,6 +315,13 @@ int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev)
 	if (!mt76_is_mmio(mdev))
 		return 0;
 
+	is_mt8196 = of_machine_is_compatible("mediatek,mt8196");
+	if (!is_mt8196) {
+		dev_info(mdev->dev, "Not MT8196 platform, TX page pool optimization disabled\n");
+		return 0;
+	}
+
+	dev_info(mdev->dev, "MT8196 platform detected, enabling TX page pool optimization\n");
 	mdev->tx_prealloc_enabled = true;
 
 	for (i = 0; i < ARRAY_SIZE(mdev->phy.q_tx); i++) {
-- 
2.45.2


^ permalink raw reply related

* [PATCH 1/3] wifi: mt76: Separate skb and page_pool_buf pointers in mt76_txwi_cache
From: Eason Lai @ 2026-07-09  7:55 UTC (permalink / raw)
  To: nbd, lorenzo
  Cc: linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai,
	eason.lai, Eason Lai
In-Reply-To: <20260709075558.1654164-1-eason.lai@mediatek.com>

From: Eason Lai <Eason.Lai@mediatek.com>

Refactor mt76_txwi_cache structure to use separate skb and page_pool_buf
fields instead of a union with ptr. This improves type safety and makes
the code more explicit about which pointer type is being used in
different contexts.

Also add skip_unmap flag to tx_info.buf to handle cases where DMA
unmapping should be skipped.

Signed-off-by: Eason Lai <Eason.Lai@mediatek.com>
---
 drivers/net/wireless/mediatek/mt76/dma.c      | 29 ++++++++++++-------
 drivers/net/wireless/mediatek/mt76/mt76.h     |  4 +--
 .../net/wireless/mediatek/mt76/mt7996/mac.c   | 10 +++----
 drivers/net/wireless/mediatek/mt76/tx.c       |  2 +-
 drivers/net/wireless/mediatek/mt76/wed.c      |  6 ++--
 5 files changed, 28 insertions(+), 23 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/dma.c b/drivers/net/wireless/mediatek/mt76/dma.c
index f8c2fe5f2f58..2716278788bd 100644
--- a/drivers/net/wireless/mediatek/mt76/dma.c
+++ b/drivers/net/wireless/mediatek/mt76/dma.c
@@ -43,7 +43,8 @@ mt76_alloc_rxwi(struct mt76_dev *dev)
 	if (!t)
 		return NULL;
 
-	t->ptr = NULL;
+	t->skb = NULL;
+	t->page_pool_buf = NULL;
 	return t;
 }
 
@@ -84,8 +85,11 @@ mt76_get_txwi(struct mt76_dev *dev)
 {
 	struct mt76_txwi_cache *t = __mt76_get_txwi(dev);
 
-	if (t)
+	if (t) {
+		t->skb = NULL;
+		t->page_pool_buf = NULL;
 		return t;
+	}
 
 	return mt76_alloc_txwi(dev);
 }
@@ -147,8 +151,8 @@ mt76_free_pending_rxwi(struct mt76_dev *dev)
 
 	local_bh_disable();
 	while ((t = __mt76_get_rxwi(dev)) != NULL) {
-		if (t->ptr)
-			mt76_put_page_pool_buf(t->ptr, false);
+		if (t->page_pool_buf)
+			mt76_put_page_pool_buf(t->page_pool_buf, false);
 		kfree(t);
 	}
 	local_bh_enable();
@@ -475,14 +479,14 @@ mt76_dma_get_rxdmad_c_buf(struct mt76_dev *dev, struct mt76_queue *q,
 	if (more)
 		*more = !FIELD_GET(RRO_RXDMAD_DATA1_LS_MASK, data1);
 
-	buf = t->ptr;
+	buf = t->page_pool_buf;
 	ind_reason = FIELD_GET(RRO_RXDMAD_DATA2_IND_REASON_MASK, data2);
 	if (ind_reason == MT_DMA_WED_IND_REASON_REPEAT ||
 	    ind_reason == MT_DMA_WED_IND_REASON_OLDPKT) {
 		mt76_put_page_pool_buf(buf, false);
 		buf = ERR_PTR(-EAGAIN);
 	}
-	t->ptr = NULL;
+	t->page_pool_buf = NULL;
 	t->dma_addr = 0;
 
 	mt76_put_rxwi(dev, t);
@@ -529,9 +533,9 @@ mt76_dma_get_buf(struct mt76_dev *dev, struct mt76_queue *q, int idx,
 				SKB_WITH_OVERHEAD(q->buf_size),
 				page_pool_get_dma_dir(q->page_pool));
 
-		buf = t->ptr;
+		buf = t->page_pool_buf;
 		t->dma_addr = 0;
-		t->ptr = NULL;
+		t->page_pool_buf = NULL;
 
 		mt76_put_rxwi(dev, t);
 		if (drop)
@@ -694,6 +698,7 @@ mt76_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
 			goto unmap;
 
 		tx_info.buf[n].addr = addr;
+		tx_info.buf[n].skip_unmap = false;
 		tx_info.buf[n++].len = iter->len;
 	}
 	tx_info.nbuf = n;
@@ -718,9 +723,11 @@ mt76_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
 				tx_info.info, tx_info.skb, t);
 
 unmap:
-	for (n--; n > 0; n--)
-		dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
-				 tx_info.buf[n].len, DMA_TO_DEVICE);
+	for (n--; n > 0; n--) {
+		if (!tx_info.buf[n].skip_unmap)
+			dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
+					 tx_info.buf[n].len, DMA_TO_DEVICE);
+	}
 
 free:
 #ifdef CONFIG_NL80211_TESTMODE
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 527bef97e122..927c21536f4e 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -445,10 +445,8 @@ struct mt76_txwi_cache {
 	struct list_head list;
 	dma_addr_t dma_addr;
 
-	union {
 		struct sk_buff *skb;
-		void *ptr;
-	};
+	void *page_pool_buf;
 
 	u8 qid;
 	u8 phy_idx;
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
index e2a83da3a09c..924b0dc0ff1e 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mac.c
@@ -1775,12 +1775,12 @@ static void mt7996_rx_token_put(struct mt7996_dev *dev)
 		struct mt76_txwi_cache *t;
 
 		t = mt76_rx_token_release(&dev->mt76, i);
-		if (!t || !t->ptr)
+		if (!t || !t->page_pool_buf)
 			continue;
 
-		mt76_put_page_pool_buf(t->ptr, false);
+		mt76_put_page_pool_buf(t->page_pool_buf, false);
 		t->dma_addr = 0;
-		t->ptr = NULL;
+		t->page_pool_buf = NULL;
 
 		mt76_put_rxwi(&dev->mt76, t);
 	}
@@ -1928,14 +1928,14 @@ void mt7996_rro_rx_process(struct mt76_dev *mdev, void *data)
 				goto next_page;
 
 			qid = t->qid;
-			buf = t->ptr;
+			buf = t->page_pool_buf;
 			q = &mdev->q_rx[qid];
 			dma_sync_single_for_cpu(mdev->dma_dev, t->dma_addr,
 						SKB_WITH_OVERHEAD(q->buf_size),
 						page_pool_get_dma_dir(q->page_pool));
 
 			t->dma_addr = 0;
-			t->ptr = NULL;
+			t->page_pool_buf = NULL;
 			mt76_put_rxwi(mdev, t);
 			if (!buf)
 				goto next_page;
diff --git a/drivers/net/wireless/mediatek/mt76/tx.c b/drivers/net/wireless/mediatek/mt76/tx.c
index 22f9690634c9..665156a7ea65 100644
--- a/drivers/net/wireless/mediatek/mt76/tx.c
+++ b/drivers/net/wireless/mediatek/mt76/tx.c
@@ -899,7 +899,7 @@ int mt76_rx_token_consume(struct mt76_dev *dev, void *ptr,
 	token = idr_alloc(&dev->rx_token, t, 0, dev->rx_token_size,
 			  GFP_ATOMIC);
 	if (token >= 0) {
-		t->ptr = ptr;
+		t->page_pool_buf = ptr;
 		t->dma_addr = phys;
 	}
 	spin_unlock_bh(&dev->rx_token_lock);
diff --git a/drivers/net/wireless/mediatek/mt76/wed.c b/drivers/net/wireless/mediatek/mt76/wed.c
index ed657d952de2..e1cf81d722b8 100644
--- a/drivers/net/wireless/mediatek/mt76/wed.c
+++ b/drivers/net/wireless/mediatek/mt76/wed.c
@@ -15,11 +15,11 @@ void mt76_wed_release_rx_buf(struct mtk_wed_device *wed)
 		struct mt76_txwi_cache *t;
 
 		t = mt76_rx_token_release(dev, i);
-		if (!t || !t->ptr)
+		if (!t || !t->page_pool_buf)
 			continue;
 
-		mt76_put_page_pool_buf(t->ptr, false);
-		t->ptr = NULL;
+		mt76_put_page_pool_buf(t->page_pool_buf, false);
+		t->page_pool_buf = NULL;
 
 		mt76_put_rxwi(dev, t);
 	}
-- 
2.45.2


^ permalink raw reply related

* [PATCH 0/3] Fix TX performance degradation when IOMMU is enabled
From: Eason Lai @ 2026-07-09  7:55 UTC (permalink / raw)
  To: nbd, lorenzo
  Cc: linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai,
	eason.lai, Eason Lai

From: Eason Lai <Eason.Lai@mediatek.com>

This series adds a TX preallocated buffer mechanism to fix the TX
performance degradation observed after enabling IOMMU.

Currently this feature is only enabled on MT8196, where it has been
validated.

Eason Lai (3):
  wifi: mt76: Separate skb and page_pool_buf pointers in mt76_txwi_cache
  wifi: mt76: mt792x: Add TX page pool support for IOMMU-enabled systems
  wifi: mt76: mt792x: Restrict TX page pool to MT8196 platform

 drivers/net/wireless/mediatek/mt76/dma.c      |  55 ++-
 drivers/net/wireless/mediatek/mt76/mt76.h     |  15 +-
 .../wireless/mediatek/mt76/mt76_connac_mac.c  |   6 +
 .../net/wireless/mediatek/mt76/mt7925/pci.c   |   4 +-
 drivers/net/wireless/mediatek/mt76/mt792x.h   |   2 +
 .../net/wireless/mediatek/mt76/mt792x_dma.c   | 330 ++++++++++++++++++
 .../net/wireless/mediatek/mt76/mt7996/mac.c   |  10 +-
 drivers/net/wireless/mediatek/mt76/tx.c       |   2 +-
 drivers/net/wireless/mediatek/mt76/wed.c      |   6 +-
 9 files changed, 398 insertions(+), 32 deletions(-)

-- 
2.45.2


^ permalink raw reply

* [PATCH 2/3] wifi: mt76: mt792x: Add TX page pool support for IOMMU-enabled systems
From: Eason Lai @ 2026-07-09  7:55 UTC (permalink / raw)
  To: nbd, lorenzo
  Cc: linux-wireless, linux-mediatek, Yf.Luo, kun.wu, deren.wu,
	sean.wang, quan.zhou, ryder.lee, leon.yen, litien.chang, jb.tsai,
	eason.lai, Eason Lai
In-Reply-To: <20260709075558.1654164-1-eason.lai@mediatek.com>

From: Eason Lai <Eason.Lai@mediatek.com>

Add support for TX page pool allocation in mt792x devices when IOMMU is
enabled. This optimization reduces DMA mapping overhead by
pre-allocating and reusing page pool buffers for TX operations.

Key changes:
- Export DMA helper functions and ops for mt792x usage
  - Add tx_prealloc_enabled flag to track page pool state
    - Implement mt792x-specific DMA queue operations with TX page pool
      - Create page pools per TX queue when IOMMU is detected
        - Handle page pool buffer cleanup in both success and error
          paths
- Add proper skip_unmap flag handling for page pool buffers

The page pool path is used for linear skbs without fragments, falling
back to standard DMA mapping for complex skb structures.

Signed-off-by: Eason Lai <Eason.Lai@mediatek.com>
---
 drivers/net/wireless/mediatek/mt76/dma.c      |  26 +-
 drivers/net/wireless/mediatek/mt76/mt76.h     |  11 +
 .../wireless/mediatek/mt76/mt76_connac_mac.c  |   6 +
 .../net/wireless/mediatek/mt76/mt7925/pci.c   |   4 +-
 drivers/net/wireless/mediatek/mt76/mt792x.h   |   2 +
 .../net/wireless/mediatek/mt76/mt792x_dma.c   | 322 ++++++++++++++++++
 6 files changed, 362 insertions(+), 9 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/dma.c b/drivers/net/wireless/mediatek/mt76/dma.c
index 2716278788bd..df89c0d1d526 100644
--- a/drivers/net/wireless/mediatek/mt76/dma.c
+++ b/drivers/net/wireless/mediatek/mt76/dma.c
@@ -4,6 +4,8 @@
  */
 
 #include <linux/dma-mapping.h>
+#include <linux/iommu.h>
+#include <net/page_pool/helpers.h>
 #include "mt76.h"
 #include "dma.h"
 #include "mt76_connac.h"
@@ -80,7 +82,7 @@ __mt76_get_rxwi(struct mt76_dev *dev)
 	return t;
 }
 
-static struct mt76_txwi_cache *
+struct mt76_txwi_cache *
 mt76_get_txwi(struct mt76_dev *dev)
 {
 	struct mt76_txwi_cache *t = __mt76_get_txwi(dev);
@@ -93,6 +95,7 @@ mt76_get_txwi(struct mt76_dev *dev)
 
 	return mt76_alloc_txwi(dev);
 }
+EXPORT_SYMBOL_GPL(mt76_get_txwi);
 
 struct mt76_txwi_cache *
 mt76_get_rxwi(struct mt76_dev *dev)
@@ -137,8 +140,10 @@ mt76_free_pending_txwi(struct mt76_dev *dev)
 
 	local_bh_disable();
 	while ((t = __mt76_get_txwi(dev)) != NULL) {
-		dma_unmap_single(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
-				 DMA_TO_DEVICE);
+		if (dev->dma_dev) {
+			dma_unmap_single(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
+					 DMA_TO_DEVICE);
+		}
 		kfree(mt76_get_txwi_ptr(dev, t));
 	}
 	local_bh_enable();
@@ -190,7 +195,7 @@ mt76_dma_queue_magic_cnt_init(struct mt76_dev *dev, struct mt76_queue *q)
 	}
 }
 
-static void
+void
 mt76_dma_sync_idx(struct mt76_dev *dev, struct mt76_queue *q)
 {
 	if ((q->flags & MT_QFLAG_WED_RRO_EN) &&
@@ -208,6 +213,7 @@ mt76_dma_sync_idx(struct mt76_dev *dev, struct mt76_queue *q)
 	q->head = Q_READ(q, dma_idx);
 	q->tail = q->head;
 }
+EXPORT_SYMBOL_GPL(mt76_dma_sync_idx);
 
 void mt76_dma_queue_reset(struct mt76_dev *dev, struct mt76_queue *q,
 			  bool reset_idx)
@@ -310,7 +316,7 @@ mt76_dma_add_rx_buf(struct mt76_dev *dev, struct mt76_queue *q,
 	return idx;
 }
 
-static int
+int
 mt76_dma_add_buf(struct mt76_dev *dev, struct mt76_queue *q,
 		 struct mt76_queue_buf *buf, int nbufs, u32 info,
 		 struct sk_buff *skb, void *txwi)
@@ -378,8 +384,9 @@ mt76_dma_add_buf(struct mt76_dev *dev, struct mt76_queue *q,
 
 	return idx;
 }
+EXPORT_SYMBOL_GPL(mt76_dma_add_buf);
 
-static void
+void
 mt76_dma_tx_cleanup_idx(struct mt76_dev *dev, struct mt76_queue *q, int idx,
 			struct mt76_queue_entry *prev_e)
 {
@@ -399,8 +406,9 @@ mt76_dma_tx_cleanup_idx(struct mt76_dev *dev, struct mt76_queue *q, int idx,
 	*prev_e = *e;
 	memset(e, 0, sizeof(*e));
 }
+EXPORT_SYMBOL_GPL(mt76_dma_tx_cleanup_idx);
 
-static void
+void
 mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q)
 {
 	wmb();
@@ -409,6 +417,7 @@ mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q)
 	else
 		Q_WRITE(q, cpu_idx, q->head);
 }
+EXPORT_SYMBOL_GPL(mt76_dma_kick_queue);
 
 static void
 mt76_dma_tx_cleanup(struct mt76_dev *dev, struct mt76_queue *q, bool flush)
@@ -1152,7 +1161,7 @@ mt76_dma_init(struct mt76_dev *dev,
 	return 0;
 }
 
-static const struct mt76_queue_ops mt76_dma_ops = {
+const struct mt76_queue_ops mt76_dma_ops = {
 	.init = mt76_dma_init,
 	.alloc = mt76_dma_alloc_queue,
 	.reset_q = mt76_dma_queue_reset,
@@ -1164,6 +1173,7 @@ static const struct mt76_queue_ops mt76_dma_ops = {
 	.rx_reset = mt76_dma_rx_reset,
 	.kick = mt76_dma_kick_queue,
 };
+EXPORT_SYMBOL_GPL(mt76_dma_ops);
 
 void mt76_dma_attach(struct mt76_dev *dev)
 {
diff --git a/drivers/net/wireless/mediatek/mt76/mt76.h b/drivers/net/wireless/mediatek/mt76/mt76.h
index 927c21536f4e..25e24fa36eca 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76.h
+++ b/drivers/net/wireless/mediatek/mt76/mt76.h
@@ -957,6 +957,8 @@ struct mt76_dev {
 	int tx_dma_idx[4];
 	enum mt76_hwrro_mode hwrro_mode;
 
+	bool tx_prealloc_enabled;
+
 	struct mt76_worker tx_worker;
 	struct napi_struct tx_napi;
 
@@ -1785,7 +1787,16 @@ mt76_tx_status_get_hw(struct mt76_dev *dev, struct sk_buff *skb)
 
 void mt76_put_txwi(struct mt76_dev *dev, struct mt76_txwi_cache *t);
 void mt76_put_rxwi(struct mt76_dev *dev, struct mt76_txwi_cache *t);
+struct mt76_txwi_cache *mt76_get_txwi(struct mt76_dev *dev);
 struct mt76_txwi_cache *mt76_get_rxwi(struct mt76_dev *dev);
+int mt76_dma_add_buf(struct mt76_dev *dev, struct mt76_queue *q,
+		     struct mt76_queue_buf *buf, int nbufs, u32 info,
+		     struct sk_buff *skb, void *txwi);
+void mt76_dma_tx_cleanup_idx(struct mt76_dev *dev, struct mt76_queue *q, int idx,
+			     struct mt76_queue_entry *prev_e);
+void mt76_dma_kick_queue(struct mt76_dev *dev, struct mt76_queue *q);
+void mt76_dma_sync_idx(struct mt76_dev *dev, struct mt76_queue *q);
+extern const struct mt76_queue_ops mt76_dma_ops;
 void mt76_free_pending_rxwi(struct mt76_dev *dev);
 void mt76_rx_complete(struct mt76_dev *dev, struct sk_buff_head *frames,
 		      struct napi_struct *napi);
diff --git a/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c b/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
index 0339e2e7ab60..d4b36b0832b8 100644
--- a/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt76_connac_mac.c
@@ -250,6 +250,12 @@ void mt76_connac_txp_skb_unmap(struct mt76_dev *dev,
 {
 	struct mt76_connac_txp_common *txp;
 
+	if (t->page_pool_buf) {
+		mt76_put_page_pool_buf(t->page_pool_buf, false);
+		t->page_pool_buf = NULL;
+		return;
+	}
+
 	txp = mt76_connac_txwi_to_txp(dev, t);
 	if (is_mt76_fw_txp(dev))
 		mt76_connac_txp_skb_unmap_fw(dev, &txp->fw);
diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
index c4161754c01d..3d69d8c67dea 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/pci.c
@@ -214,7 +214,7 @@ static int mt7925_dma_init(struct mt792x_dev *dev)
 {
 	int ret;
 
-	mt76_dma_attach(&dev->mt76);
+	mt792x_dma_attach(&dev->mt76);
 
 	ret = mt792x_dma_disable(dev, true);
 	if (ret)
@@ -263,6 +263,8 @@ static int mt7925_dma_init(struct mt792x_dev *dev)
 			  mt792x_poll_tx);
 	napi_enable(&dev->mt76.tx_napi);
 
+	mt792x_dma_tx_page_pool_init(dev);
+
 	return mt792x_dma_enable(dev);
 }
 
diff --git a/drivers/net/wireless/mediatek/mt76/mt792x.h b/drivers/net/wireless/mediatek/mt76/mt792x.h
index 4ff93f2cd624..7ac0318dc249 100644
--- a/drivers/net/wireless/mediatek/mt76/mt792x.h
+++ b/drivers/net/wireless/mediatek/mt76/mt792x.h
@@ -418,8 +418,10 @@ void mt792x_sta_statistics(struct ieee80211_hw *hw,
 			   struct station_info *sinfo);
 void mt792x_set_coverage_class(struct ieee80211_hw *hw, int radio_idx,
 			       s16 coverage_class);
+void mt792x_dma_attach(struct mt76_dev *dev);
 void mt792x_dma_cleanup(struct mt792x_dev *dev);
 int mt792x_dma_enable(struct mt792x_dev *dev);
+int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev);
 int mt792x_wpdma_reset(struct mt792x_dev *dev, bool force);
 int mt792x_wpdma_reinit_cond(struct mt792x_dev *dev);
 int mt792x_dma_disable(struct mt792x_dev *dev, bool force);
diff --git a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
index 002aece857b2..b341f1cb3ce0 100644
--- a/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
+++ b/drivers/net/wireless/mediatek/mt76/mt792x_dma.c
@@ -3,6 +3,9 @@
 
 #include <linux/module.h>
 #include <linux/firmware.h>
+#include <linux/iommu.h>
+#include <net/page_pool/helpers.h>
+#include <linux/of.h>
 
 #include "mt792x.h"
 #include "dma.h"
@@ -266,6 +269,323 @@ int mt792x_wpdma_reinit_cond(struct mt792x_dev *dev)
 }
 EXPORT_SYMBOL_GPL(mt792x_wpdma_reinit_cond);
 
+static int mt792x_create_tx_page_pool(struct mt76_dev *dev, struct mt76_queue *q)
+{
+	struct page_pool_params pp_params = {
+		.order = 0,
+		.flags = 0,
+		.nid = NUMA_NO_NODE,
+		.dev = dev->dma_dev,
+	};
+
+	if (!q || !dev->tx_prealloc_enabled)
+		return 0;
+
+	if (!mt76_is_mmio(dev))
+		return 0;
+
+	pp_params.pool_size = 256;
+	pp_params.flags |= PP_FLAG_DMA_MAP;
+	pp_params.dma_dir = DMA_BIDIRECTIONAL;
+	pp_params.max_len = PAGE_SIZE;
+	pp_params.offset = 0;
+
+	q->page_pool = page_pool_create(&pp_params);
+	if (IS_ERR(q->page_pool)) {
+		int err = PTR_ERR(q->page_pool);
+
+		q->page_pool = NULL;
+		dev_warn(dev->dev, "Failed to create TX page pool for queue %d (err=%d)\n",
+			 q->hw_idx, err);
+		return 0;
+	}
+
+	return 0;
+}
+
+int mt792x_dma_tx_page_pool_init(struct mt792x_dev *dev)
+{
+	struct mt76_dev *mdev = &dev->mt76;
+	int i, ret, pool_count = 0;
+
+	if (!iommu_get_domain_for_dev(mdev->dma_dev))
+		return 0;
+
+	if (!mt76_is_mmio(mdev))
+		return 0;
+
+	mdev->tx_prealloc_enabled = true;
+
+	for (i = 0; i < ARRAY_SIZE(mdev->phy.q_tx); i++) {
+		struct mt76_queue *q = mdev->phy.q_tx[i];
+
+		if (!q)
+			continue;
+
+		ret = mt792x_create_tx_page_pool(mdev, q);
+		if (ret)
+			return ret;
+
+		if (q->page_pool)
+			pool_count++;
+	}
+
+	if (pool_count > 0)
+		dev_info(mdev->dev,
+			 "IOMMU enabled, created %d TX page pools\n", pool_count);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(mt792x_dma_tx_page_pool_init);
+
+static void mt792x_dma_tx_page_pool_cleanup(struct mt792x_dev *dev)
+{
+	struct mt76_dev *mdev = &dev->mt76;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(mdev->phy.q_tx); i++) {
+		struct mt76_queue *q = mdev->phy.q_tx[i];
+
+		if (!q || !q->page_pool)
+			continue;
+
+		page_pool_destroy(q->page_pool);
+		q->page_pool = NULL;
+	}
+
+	mdev->tx_prealloc_enabled = false;
+}
+
+static void *
+mt792x_dma_tx_alloc_page_pool_buf(struct mt76_dev *dev, struct mt76_queue *q,
+				  struct sk_buff *skb, dma_addr_t *dma_addr,
+				  int *buf_len)
+{
+	struct page *page;
+	void *buf;
+	int len;
+	u32 offset;
+
+	if (!q->page_pool || !dev->tx_prealloc_enabled)
+		return NULL;
+
+	len = skb_headlen(skb);
+	if (len > PAGE_SIZE)
+		return NULL;
+
+	buf = mt76_get_page_pool_buf(q, &offset, len);
+	if (!buf)
+		return NULL;
+
+	page = virt_to_head_page(buf);
+	*dma_addr = page_pool_get_dma_addr(page) + offset;
+	if (unlikely(!*dma_addr)) {
+		dev_warn_ratelimited(dev->dev, "Page pool returned NULL DMA address\n");
+		mt76_put_page_pool_buf(buf, false);
+		return NULL;
+	}
+
+	*buf_len = len;
+
+	dma_sync_single_for_cpu(dev->dma_dev, *dma_addr, len, DMA_TO_DEVICE);
+	skb_copy_from_linear_data(skb, buf, len);
+	dma_sync_single_for_device(dev->dma_dev, *dma_addr, len, DMA_TO_DEVICE);
+
+	return buf;
+}
+
+static int
+mt792x_dma_tx_queue_skb(struct mt76_phy *phy, struct mt76_queue *q,
+			enum mt76_txq_id qid, struct sk_buff *skb,
+			struct mt76_wcid *wcid, struct ieee80211_sta *sta)
+{
+	struct ieee80211_tx_status status = {
+		.sta = sta,
+	};
+	struct mt76_tx_info tx_info = {
+		.skb = skb,
+	};
+	struct mt76_dev *dev = phy->dev;
+	struct ieee80211_hw *hw;
+	int len, n = 0, ret = -ENOMEM;
+	struct mt76_txwi_cache *t;
+	struct sk_buff *iter;
+	dma_addr_t addr;
+	u8 *txwi;
+
+	if (test_bit(MT76_RESET, &phy->state))
+		goto free_skb;
+
+	t = mt76_get_txwi(dev);
+	if (!t)
+		goto free_skb;
+
+	txwi = mt76_get_txwi_ptr(dev, t);
+
+	skb->prev = NULL;
+	skb->next = NULL;
+	if (dev->drv->drv_flags & MT_DRV_TX_ALIGNED4_SKBS)
+		mt76_insert_hdr_pad(skb);
+
+	len = skb_headlen(skb);
+
+	if (dev->tx_prealloc_enabled && q->page_pool &&
+	    !skb_has_frag_list(skb) && !skb_shinfo(skb)->nr_frags) {
+		void *buf;
+		int pp_len;
+
+		buf = mt792x_dma_tx_alloc_page_pool_buf(dev, q, skb, &addr, &pp_len);
+		if (buf) {
+			t->page_pool_buf = buf;
+			len = pp_len;
+
+			tx_info.buf[n].addr = t->dma_addr;
+			tx_info.buf[n++].len = dev->drv->txwi_size;
+			tx_info.buf[n].addr = addr;
+			tx_info.buf[n].len = len;
+			tx_info.buf[n].skip_unmap = true;
+			n++;
+
+			goto skip_dma_map;
+		}
+	}
+
+	addr = dma_map_single(dev->dma_dev, skb->data, len, DMA_TO_DEVICE);
+	if (unlikely(dma_mapping_error(dev->dma_dev, addr)))
+		goto free;
+
+	t->page_pool_buf = NULL;
+
+	tx_info.buf[n].addr = t->dma_addr;
+	tx_info.buf[n++].len = dev->drv->txwi_size;
+	tx_info.buf[n].addr = addr;
+	tx_info.buf[n++].len = len;
+
+skip_dma_map:
+	skb_walk_frags(skb, iter) {
+		if (n == ARRAY_SIZE(tx_info.buf))
+			goto unmap;
+
+		addr = dma_map_single(dev->dma_dev, iter->data, iter->len,
+				      DMA_TO_DEVICE);
+		if (unlikely(dma_mapping_error(dev->dma_dev, addr)))
+			goto unmap;
+
+		tx_info.buf[n].addr = addr;
+		tx_info.buf[n].skip_unmap = false;
+		tx_info.buf[n++].len = iter->len;
+	}
+	tx_info.nbuf = n;
+
+	if (q->queued + (tx_info.nbuf + 1) / 2 >= q->ndesc - 1) {
+		ret = -ENOMEM;
+		goto unmap;
+	}
+
+	dma_sync_single_for_cpu(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
+				DMA_TO_DEVICE);
+	ret = dev->drv->tx_prepare_skb(dev, txwi, qid, wcid, sta, &tx_info);
+	dma_sync_single_for_device(dev->dma_dev, t->dma_addr, dev->drv->txwi_size,
+				   DMA_TO_DEVICE);
+	if (ret < 0)
+		goto unmap;
+
+	return mt76_dma_add_buf(dev, q, tx_info.buf, tx_info.nbuf,
+				tx_info.info, tx_info.skb, t);
+
+unmap:
+	for (n--; n > 0; n--) {
+		if (!tx_info.buf[n].skip_unmap)
+			dma_unmap_single(dev->dma_dev, tx_info.buf[n].addr,
+					 tx_info.buf[n].len, DMA_TO_DEVICE);
+	}
+
+	if (t->page_pool_buf) {
+		mt76_put_page_pool_buf(t->page_pool_buf, false);
+		t->page_pool_buf = NULL;
+	}
+
+free:
+#ifdef CONFIG_NL80211_TESTMODE
+	if (mt76_is_testmode_skb(dev, skb, &hw)) {
+		struct mt76_phy *phy = hw->priv;
+
+		if (tx_info.skb == phy->test.tx_skb)
+			phy->test.tx_done--;
+	}
+#endif
+
+	mt76_put_txwi(dev, t);
+
+free_skb:
+	status.skb = tx_info.skb;
+	hw = mt76_tx_status_get_hw(dev, tx_info.skb);
+	spin_lock_bh(&dev->rx_lock);
+	ieee80211_tx_status_ext(hw, &status);
+	spin_unlock_bh(&dev->rx_lock);
+
+	return ret;
+}
+
+static void
+mt792x_dma_tx_cleanup(struct mt76_dev *dev, struct mt76_queue *q, bool flush)
+{
+	struct mt76_queue_entry entry;
+	int last;
+
+	if (!q || !q->ndesc)
+		return;
+
+	spin_lock_bh(&q->cleanup_lock);
+	if (flush)
+		last = -1;
+	else
+		last = Q_READ(q, dma_idx);
+
+	while (q->queued > 0 && q->tail != last) {
+		mt76_dma_tx_cleanup_idx(dev, q, q->tail, &entry);
+		mt76_npu_txdesc_cleanup(q, q->tail);
+		mt76_queue_tx_complete(dev, q, &entry);
+
+		if (entry.txwi) {
+			if (entry.skb && entry.txwi->page_pool_buf) {
+				mt76_put_page_pool_buf(entry.txwi->page_pool_buf, false);
+				entry.txwi->page_pool_buf = NULL;
+			}
+
+			if (!(dev->drv->drv_flags & MT_DRV_TXWI_NO_FREE))
+				mt76_put_txwi(dev, entry.txwi);
+		}
+
+		if (!flush && q->tail == last)
+			last = Q_READ(q, dma_idx);
+	}
+	spin_unlock_bh(&q->cleanup_lock);
+
+	if (flush) {
+		spin_lock_bh(&q->lock);
+		mt76_dma_sync_idx(dev, q);
+		mt76_dma_kick_queue(dev, q);
+		spin_unlock_bh(&q->lock);
+	}
+
+	if (!q->queued)
+		wake_up(&dev->tx_wait);
+}
+
+static struct mt76_queue_ops mt792x_queue_ops;
+
+void mt792x_dma_attach(struct mt76_dev *dev)
+{
+	mt792x_queue_ops = mt76_dma_ops;
+
+	mt792x_queue_ops.tx_queue_skb = mt792x_dma_tx_queue_skb;
+	mt792x_queue_ops.tx_cleanup = mt792x_dma_tx_cleanup;
+
+	dev->queue_ops = &mt792x_queue_ops;
+}
+EXPORT_SYMBOL_GPL(mt792x_dma_attach);
+
 int mt792x_dma_disable(struct mt792x_dev *dev, bool force)
 {
 	/* disable WFDMA0 */
@@ -326,6 +646,8 @@ void mt792x_dma_cleanup(struct mt792x_dev *dev)
 		 MT_WFDMA0_RST_LOGIC_RST);
 
 	mt76_dma_cleanup(&dev->mt76);
+
+	mt792x_dma_tx_page_pool_cleanup(dev);
 }
 EXPORT_SYMBOL_GPL(mt792x_dma_cleanup);
 
-- 
2.45.2


^ permalink raw reply related

* Re: [PATCH] wifi: mac80211: Fix cryptographic MAC comparison to be constant-time
From: Johannes Berg @ 2026-07-09  6:45 UTC (permalink / raw)
  To: Eric Biggers, linux-wireless, Jouni Malinen
  Cc: linux-crypto, linux-kernel, stable
In-Reply-To: <20260709024443.58132-1-ebiggers@kernel.org>

On Wed, 2026-07-08 at 22:44 -0400, Eric Biggers wrote:
> To prevent timing attacks, the comparison of cryptographic message
> authentication codes (MACs) needs to have data-independent timing.
> Replace the memcmp() with the correct function, crypto_memneq().
> 
> Fixes: 39404feee691 ("mac80211: FILS AEAD protection for station mode association frames")
> Cc: stable@vger.kernel.org

I guess I'll apply (a variant of) this patch for -next, but that commit
log really makes it sound like something is actually broken and needs
fixing, and I don't think that's true in this specific context.

What happens is that the frame is validated and then we associate
successfully (upon success) or drop the frame (upon failure). Only the
failure case is relevant for the timing issue, but in that case we
simply drop the frame and there isn't really an observable signal -
nothing else happens, at least not immediately, we may retry the request
later after a timer.

So sure, it looks better to have a crypto_memneq() in AES-SIV related
code, but in practice I don't see how it would make a difference now,
and it's even unlikely this code will ever matter for anything else in
the future, given that things are moving more and more towards full
frame encryption, including association request/response now.

I saw you originally had this in the "use libraries" patch [1], I'm also
good with you just keeping the change there. This might even be better
if you're planning to have this in -next soon, where it would otherwise
conflict if I keep this to -next.

[1] https://lore.kernel.org/linux-crypto/20260707053503.209874-24-ebiggers@kernel.org/

(The whole feature is also fairly much unused anyway in practice as far
as I can tell.)

johannes

^ permalink raw reply

* [PATCH] wifi: mac80211: Fix cryptographic MAC comparison to be constant-time
From: Eric Biggers @ 2026-07-09  2:44 UTC (permalink / raw)
  To: linux-wireless, Johannes Berg, Jouni Malinen
  Cc: linux-crypto, linux-kernel, Eric Biggers, stable

To prevent timing attacks, the comparison of cryptographic message
authentication codes (MACs) needs to have data-independent timing.
Replace the memcmp() with the correct function, crypto_memneq().

Fixes: 39404feee691 ("mac80211: FILS AEAD protection for station mode association frames")
Cc: stable@vger.kernel.org
Signed-off-by: Eric Biggers <ebiggers@kernel.org>
---
 net/mac80211/fils_aead.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/net/mac80211/fils_aead.c b/net/mac80211/fils_aead.c
index d2f4a17eab99..00b91e6ed994 100644
--- a/net/mac80211/fils_aead.c
+++ b/net/mac80211/fils_aead.c
@@ -195,7 +195,7 @@ static int aes_siv_decrypt(const u8 *key, size_t key_len,
 	res = aes_s2v(key /* K1 */, key_len, num_elem, addr, len, check);
 	if (res)
 		return res;
-	if (memcmp(check, frame_iv, AES_BLOCK_SIZE) != 0)
+	if (crypto_memneq(check, frame_iv, AES_BLOCK_SIZE))
 		return -EINVAL;
 	return 0;
 }

base-commit: 8cdeaa50eae8dad34885515f62559ee83e7e8dda
-- 
2.55.0


^ permalink raw reply related

* [PATCH 8/8] wifi: nl80211: clean up color-change beacon data on errors
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

nl80211_color_change() calls nl80211_parse_beacon() for the beacon_next
template, which can allocate params.beacon_next.mbssid_ies and .rnr_ies.
The out: label frees those, but two error paths after that parse
returned directly instead of jumping to it: nl80211_parse_beacon()
itself failing, and the kzalloc_objs() of the attribute table failing.
Both leaked whatever mbssid_ies/rnr_ies had been allocated in
beacon_next.

Initialize tb to NULL and route both error paths through the out: label
so the parsed beacon data is freed.

Fixes: dc1e3cb8da8b ("nl80211: MBSSID and EMA support in AP mode")
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 net/wireless/nl80211.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/net/wireless/nl80211.c b/net/wireless/nl80211.c
index 5adcb6bd0fc56..7e796d7c1a661 100644
--- a/net/wireless/nl80211.c
+++ b/net/wireless/nl80211.c
@@ -18724,7 +18724,7 @@ static int nl80211_color_change(struct sk_buff *skb, struct genl_info *info)
 	struct cfg80211_color_change_settings params = {};
 	struct net_device *dev = info->user_ptr[1];
 	struct wireless_dev *wdev = dev->ieee80211_ptr;
-	struct nlattr **tb;
+	struct nlattr **tb = NULL;
 	u16 offset;
 	int err;
 
@@ -18754,11 +18754,13 @@ static int nl80211_color_change(struct sk_buff *skb, struct genl_info *info)
 				   wdev->links[params.link_id].ap.chandef.chan,
 				   info->extack);
 	if (err)
-		return err;
+		goto out;
 
 	tb = kzalloc_objs(*tb, NL80211_ATTR_MAX + 1);
-	if (!tb)
-		return -ENOMEM;
+	if (!tb) {
+		err = -ENOMEM;
+		goto out;
+	}
 
 	err = nla_parse_nested(tb, NL80211_ATTR_MAX,
 			       info->attrs[NL80211_ATTR_COLOR_CHANGE_ELEMS],
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 7/8] wifi: mwifiex: validate action frame fixed fields
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li, stable
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

mwifiex_process_mgmt_packet() accepts an rx_pkt_length as small as a
4-address struct ieee80211_hdr plus the 2-byte firmware length prefix.
After the prefix is stripped, mwifiex_parse_mgmt_packet() can be called
with len equal to sizeof(struct ieee80211_hdr).

For an action frame the parser then reads the category byte just past
that header, and for a public action frame the action code at the next
byte, without checking that len covers them. A minimal-length action
frame therefore causes a one- or two-byte read past the end of the RX
buffer.

Reject frames shorter than the header, and require the category and
(for public action frames) action-code bytes to be present before
reading them.

Suggested-by: Johannes Berg <johannes@sipsolutions.net>
Fixes: 72e5aa8d2a6d ("mwifiex: support for parsing TDLS discovery frames")
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/66f148d83eb9f0970b9abbccc85d1b61244e54ad.camel@sipsolutions.net/
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 drivers/net/wireless/marvell/mwifiex/util.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/drivers/net/wireless/marvell/mwifiex/util.c b/drivers/net/wireless/marvell/mwifiex/util.c
index 7d3631d212236..e2d76c2d9b05b 100644
--- a/drivers/net/wireless/marvell/mwifiex/util.c
+++ b/drivers/net/wireless/marvell/mwifiex/util.c
@@ -313,13 +313,22 @@ mwifiex_parse_mgmt_packet(struct mwifiex_private *priv, u8 *payload, u16 len,
 	u8 category, action_code, *addr2;
 	struct ieee80211_hdr *ieee_hdr = (void *)payload;
 
+	if (len < sizeof(*ieee_hdr))
+		return -1;
+
 	stype = (le16_to_cpu(ieee_hdr->frame_control) & IEEE80211_FCTL_STYPE);
 
 	switch (stype) {
 	case IEEE80211_STYPE_ACTION:
+		if (len < sizeof(*ieee_hdr) + 1)
+			return -1;
+
 		category = *(payload + sizeof(struct ieee80211_hdr));
 		switch (category) {
 		case WLAN_CATEGORY_PUBLIC:
+			if (len < sizeof(*ieee_hdr) + 2)
+				return -1;
+
 			action_code = *(payload + sizeof(struct ieee80211_hdr)
 					+ 1);
 			if (action_code == WLAN_PUB_ACTION_TDLS_DISCOVER_RES) {
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 6/8] wifi: mac80211: validate S1G TWT params before driver setup
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li, stable
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

A received S1G TWT setup frame is length-checked in
ieee80211_process_rx_twt_action() before it is queued: it requires

  skb->len >= IEEE80211_MIN_ACTION_SIZE + sizeof(twt_setup) + 2

and then skb->len >= IEEE80211_MIN_ACTION_SIZE + 3 + twt->length, where
twt->length is attacker-controlled. twt->length can be as small as 3
(the control byte plus the 2-byte req_type) and the frame still passes,
so only those bytes are guaranteed present.

For an individual (non-broadcast) agreement, ieee80211_s1g_rx_twt_setup()
calls drv_add_twt_setup(), and both trace_drv_add_twt_setup() and the
driver ->add_twt_setup() callback read the full struct
ieee80211_twt_params via twt->params (req_type, twt, min_twt_dur,
mantissa, channel). That needs twt->length >= sizeof(twt->control) +
sizeof(struct ieee80211_twt_params) = 15, so with the minimal 3-byte
element they read up to 12 bytes past the end of the frame.

The broadcast path only rejects the agreement and touches req_type,
which is always present, so it is unaffected. For the individual path,
require twt->length to cover the control byte plus a full
ieee80211_twt_params block before calling drv_add_twt_setup(), and drop
the frame otherwise.

Fixes: f5a4c24e689f ("mac80211: introduce individual TWT support in AP mode")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 net/mac80211/s1g.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/net/mac80211/s1g.c b/net/mac80211/s1g.c
index 5af4a0c6c6424..abc338e22e59c 100644
--- a/net/mac80211/s1g.c
+++ b/net/mac80211/s1g.c
@@ -101,6 +101,10 @@ ieee80211_s1g_rx_twt_setup(struct ieee80211_sub_if_data *sdata,
 	struct ieee80211_twt_setup *twt = (void *)mgmt->u.action.s1g.variable;
 	struct ieee80211_twt_params *twt_agrt = (void *)twt->params;
 
+	if (!(twt->control & IEEE80211_TWT_CONTROL_NEG_TYPE_BROADCAST) &&
+	    twt->length < sizeof(twt->control) + sizeof(*twt_agrt))
+		return;
+
 	twt_agrt->req_type &= cpu_to_le16(~IEEE80211_TWT_REQTYPE_REQUEST);
 
 	/* broadcast TWT not supported yet */
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 5/8] wifi: mac80211: validate probe response countdown offsets
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

ieee80211_set_beacon_cntdwn() writes the current countdown value into
each configured counter offset. For the beacon it skips offset 0 as an
unused entry and bounds each non-zero offset against the template length
before writing. The AP probe-response branch reuses the same
counter-offset array but indexes resp->data[] with neither the
zero-sentinel skip nor the length check.

A probe-response template whose countdown offset is 0 or points past
resp->len therefore takes a stray or out-of-bounds write. Apply the same
zero-sentinel and length checks the beacon path uses before writing the
probe-response countdown byte.

Fixes: 5f9404abdf2a ("mac80211: add support for BSS color change")
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 net/mac80211/tx.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 91b14112e24f0..84b6eda46a8f0 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -5249,6 +5249,10 @@ static void ieee80211_set_beacon_cntdwn(struct ieee80211_sub_if_data *sdata,
 		if (sdata->vif.type == NL80211_IFTYPE_AP && resp) {
 			u16 *resp_offsets = resp->cntdwn_counter_offsets;
 
+			if (!resp_offsets[i])
+				continue;
+			if (WARN_ON_ONCE(resp_offsets[i] >= resp->len))
+				return;
 			resp->data[resp_offsets[i]] = count;
 		}
 	}
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 4/8] wifi: cfg80211: guard optional PMSR nominal time
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li, stable
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

pmsr_parse_ftm() rejects a request that omits NOMINAL_TIME only for
non-trigger-based PD ranging. It then reads the attribute
unconditionally for every non-trigger-based request:

	out->ftm.nominal_time =
		nla_get_u32(tb[NL80211_PMSR_FTM_REQ_ATTR_NOMINAL_TIME]);

For the other non-trigger-based request types NOMINAL_TIME is optional,
so tb[...] can be NULL and nla_get_u32() dereferences a NULL pointer.

Keep the requirement for PD ranging and read the nominal-time value only
when the attribute is present.

Fixes: 8823a9b0e7af ("wifi: cfg80211: add NTB continuous ranging and FTM request type support")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 net/wireless/pmsr.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/net/wireless/pmsr.c b/net/wireless/pmsr.c
index 34c3625f7fd5e..d1e2fae5bc0e5 100644
--- a/net/wireless/pmsr.c
+++ b/net/wireless/pmsr.c
@@ -263,8 +263,9 @@ static int pmsr_parse_ftm(struct cfg80211_registered_device *rdev,
 				       "FTM: nominal time is required for PD NTB ranging");
 			return -EINVAL;
 		}
-		out->ftm.nominal_time =
-			nla_get_u32(tb[NL80211_PMSR_FTM_REQ_ATTR_NOMINAL_TIME]);
+		if (tb[NL80211_PMSR_FTM_REQ_ATTR_NOMINAL_TIME])
+			out->ftm.nominal_time =
+				nla_get_u32(tb[NL80211_PMSR_FTM_REQ_ATTR_NOMINAL_TIME]);
 
 		if (tb[NL80211_PMSR_FTM_REQ_ATTR_MIN_TIME_BETWEEN_MEASUREMENTS])
 			out->ftm.min_time_between_measurements =
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 3/8] wifi: iwlwifi: mld: abort active PMSR requests
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li, stable
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

The iwlwifi MLD driver registers .start_pmsr (iwl_mld_start_pmsr ->
iwl_mld_ftm_start) but not .abort_pmsr. iwl_mld_ftm_start() stores the
cfg80211 peer-measurement request in mld->ftm_initiator.req and the
owning wdev in mld->ftm_initiator.req_wdev, and keeps both until a TOF
range response completes the measurement.

When the user space that started the FTM session closes its netlink
socket (or the wdev goes down) while the measurement is still in
flight, cfg80211_release_pmsr()/cfg80211_pmsr_wdev_down() schedule the
abort work and cfg80211_pmsr_process_abort() runs:

	rdev_abort_pmsr(rdev, wdev, req);
	kfree(req);

rdev_abort_pmsr() only calls the driver op when ops->abort_pmsr is
set, so for MLD it is a no-op. The request is freed while
mld->ftm_initiator.req still points at it; nothing clears the
pointer.

A subsequent TOF range response from the firmware reaches
iwl_mld_handle_ftm_resp_notif(). The !mld->ftm_initiator.req guard
does not catch this because the pointer is dangling, not NULL, so the
handler dereferences the freed request (req->cookie, req->n_peers,
req->peers[]) and passes it with req_wdev to
cfg80211_pmsr_report()/cfg80211_pmsr_complete(); a use-after-free of
the already freed request.

Implement .abort_pmsr. iwl_mld_ftm_abort() checks that the aborted
request is the active one, cancels the pending FTM notifications,
resets the ftm_initiator state (clearing req and req_wdev), and sends
TOF_RANGE_ABORT_CMD to the firmware so no further range responses are
generated for the aborted request.

Fixes: d1e879ec600f ("wifi: iwlwifi: add iwlmld sub-driver")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 .../intel/iwlwifi/mld/ftm-initiator.c         | 22 +++++++++++++++++++
 .../intel/iwlwifi/mld/ftm-initiator.h         |  2 ++
 .../net/wireless/intel/iwlwifi/mld/mac80211.c | 10 +++++++++
 3 files changed, 34 insertions(+)

diff --git a/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.c b/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.c
index 81df3fdfcbf56..b18e4fa9dedf1 100644
--- a/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.c
+++ b/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.c
@@ -13,6 +13,7 @@
 #include "constants.h"
 #include "fw/api/location.h"
 #include "ftm-initiator.h"
+#include "hcmd.h"
 
 static void iwl_mld_ftm_cmd_common(struct iwl_mld *mld,
 				   struct ieee80211_vif *vif,
@@ -282,6 +283,27 @@ static void iwl_mld_ftm_reset(struct iwl_mld *mld)
 	       sizeof(mld->ftm_initiator.responses));
 }
 
+void iwl_mld_ftm_abort(struct iwl_mld *mld,
+		       struct cfg80211_pmsr_request *req)
+{
+	struct iwl_tof_range_abort_cmd cmd = {
+		.request_id = req->cookie,
+	};
+
+	lockdep_assert_wiphy(mld->wiphy);
+
+	if (req != mld->ftm_initiator.req)
+		return;
+
+	iwl_mld_cancel_notifications_of_object(mld, IWL_MLD_OBJECT_TYPE_FTM_REQ,
+					       (u8)req->cookie);
+	iwl_mld_ftm_reset(mld);
+
+	if (iwl_mld_send_cmd_pdu(mld, WIDE_ID(LOCATION_GROUP, TOF_RANGE_ABORT_CMD),
+				 &cmd))
+		IWL_ERR(mld, "failed to abort FTM process\n");
+}
+
 static int iwl_mld_ftm_range_resp_valid(struct iwl_mld *mld, u8 request_id,
 					u8 num_of_aps)
 {
diff --git a/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.h b/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.h
index 3fab25a52508a..7b807605af503 100644
--- a/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.h
+++ b/drivers/net/wireless/intel/iwlwifi/mld/ftm-initiator.h
@@ -21,6 +21,8 @@ struct ftm_initiator_data {
 
 int iwl_mld_ftm_start(struct iwl_mld *mld, struct ieee80211_vif *vif,
 		      struct cfg80211_pmsr_request *req);
+void iwl_mld_ftm_abort(struct iwl_mld *mld,
+		       struct cfg80211_pmsr_request *req);
 
 void iwl_mld_handle_ftm_resp_notif(struct iwl_mld *mld,
 				   struct iwl_rx_packet *pkt);
diff --git a/drivers/net/wireless/intel/iwlwifi/mld/mac80211.c b/drivers/net/wireless/intel/iwlwifi/mld/mac80211.c
index 17286b3341c02..614c55967fcca 100644
--- a/drivers/net/wireless/intel/iwlwifi/mld/mac80211.c
+++ b/drivers/net/wireless/intel/iwlwifi/mld/mac80211.c
@@ -2853,6 +2853,15 @@ static int iwl_mld_start_pmsr(struct ieee80211_hw *hw,
 	return iwl_mld_ftm_start(mld, vif, request);
 }
 
+static void iwl_mld_abort_pmsr(struct ieee80211_hw *hw,
+			       struct ieee80211_vif *vif,
+			       struct cfg80211_pmsr_request *request)
+{
+	struct iwl_mld *mld = IWL_MAC80211_GET_MLD(hw);
+
+	iwl_mld_ftm_abort(mld, request);
+}
+
 static enum ieee80211_neg_ttlm_res
 iwl_mld_can_neg_ttlm(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
 		     struct ieee80211_neg_ttlm *neg_ttlm)
@@ -2974,6 +2983,7 @@ const struct ieee80211_ops iwl_mld_hw_ops = {
 	.prep_add_interface = iwl_mld_prep_add_interface,
 	.set_hw_timestamp = iwl_mld_set_hw_timestamp,
 	.start_pmsr = iwl_mld_start_pmsr,
+	.abort_pmsr = iwl_mld_abort_pmsr,
 	.can_neg_ttlm = iwl_mld_can_neg_ttlm,
 	.start_nan = iwl_mld_start_nan,
 	.stop_nan = iwl_mld_stop_nan,
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 2/8] wifi: mac80211_hwsim: authenticate PMSR report senders
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

hwsim_pmsr_report_nl() looks up the radio by HWSIM_ATTR_ADDR_TRANSMITTER
and, when data->pmsr_request is set, parses the reported peer results,
hands them to cfg80211_pmsr_report(), then unconditionally clears
data->pmsr_request and calls cfg80211_pmsr_complete() to end the
measurement.

Unlike the sibling wmediumd data-path handlers
hwsim_tx_info_frame_received_nl() and hwsim_cloned_frame_received_nl(),
which check the sending socket's netgroup against data->netgroup and its
portid against data->wmediumd, this handler did not check the sender at
all, and its genl op carries no GENL_UNS_ADMIN_PERM flag. In non-virtio
(wmediumd) mode any process in the netns that can reach the hwsim
generic netlink family could therefore send a report. The transmitter
address is not secret, so such a process could inject spoofed ranging
results for another radio's in-flight request and, because the handler
always completes the measurement, terminate a ranging operation owned by
the real wmediumd session.

Reject reports whose sender does not match the registered wmediumd
instance, mirroring the sibling handlers: in non-virtio mode require
the sending socket's netgroup to equal data->netgroup and
info->snd_portid to equal data->wmediumd before touching the request
state.

Fixes: 2af3b2a631b1 ("mac80211_hwsim: add PMSR report support via virtio")
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 drivers/net/wireless/virtual/mac80211_hwsim_main.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index bc0818b525224..6bf9736098b67 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -4211,6 +4211,15 @@ static int hwsim_pmsr_report_nl(struct sk_buff *msg, struct genl_info *info)
 	if (!data)
 		return -EINVAL;
 
+	if (!hwsim_virtio_enabled) {
+		if (hwsim_net_get_netgroup(genl_info_net(info)) !=
+		    data->netgroup)
+			return -EINVAL;
+
+		if (info->snd_portid != data->wmediumd)
+			return -EPERM;
+	}
+
 	mutex_lock(&data->mutex);
 	if (!data->pmsr_request) {
 		err = -EINVAL;
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 1/8] wifi: mac80211_hwsim: clear PMSR request state on abort
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li, stable
In-Reply-To: <20260708195911.84365-1-enderaoelyther@gmail.com>

mac80211_hwsim saves the in-flight cfg80211 PMSR request and its wdev
in data->pmsr_request / data->pmsr_request_wdev when a measurement
starts, and clears them only when it reports completion.

mac80211_hwsim_abort_pmsr() never cleared that saved state. cfg80211
owns the request and frees it once the abort callback returns
(cfg80211_pmsr_process_abort() calls rdev_abort_pmsr() then
kfree(req)), so after an abort data->pmsr_request dangles. A later
hwsim PMSR report then dereferences the freed request in
hwsim_pmsr_report_nl() and completes it; a use-after-free.

Clear data->pmsr_request and data->pmsr_request_wdev once the abort
matches the active request. Move the wmediumd/virtio notification check
below the clear so the saved state is dropped even when no notification
is sent.

Fixes: 5530c04c87c5 ("mac80211_hwsim: add PMSR request support via virtio")
Cc: stable@vger.kernel.org
Assisted-by: Codex:gpt-5
Assisted-by: Claude:opus-4.8
Signed-off-by: Zhao Li <enderaoelyther@gmail.com>
---
 drivers/net/wireless/virtual/mac80211_hwsim_main.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index 956ff9b94526d..bc0818b525224 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -3841,9 +3841,6 @@ static void mac80211_hwsim_abort_pmsr(struct ieee80211_hw *hw,
 	int err = 0;
 
 	data = hw->priv;
-	_portid = READ_ONCE(data->wmediumd);
-	if (!_portid && !hwsim_virtio_enabled)
-		return;
 
 	mutex_lock(&data->mutex);
 
@@ -3852,6 +3849,13 @@ static void mac80211_hwsim_abort_pmsr(struct ieee80211_hw *hw,
 		goto out;
 	}
 
+	data->pmsr_request = NULL;
+	data->pmsr_request_wdev = NULL;
+
+	_portid = READ_ONCE(data->wmediumd);
+	if (!_portid && !hwsim_virtio_enabled)
+		goto out;
+
 	skb = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
 	if (!skb) {
 		err = -ENOMEM;
-- 
2.50.1 (Apple Git-155)

^ permalink raw reply related

* [PATCH 0/8] wifi: fix PMSR lifetime and frame validation issues
From: Zhao Li @ 2026-07-08 19:59 UTC (permalink / raw)
  To: linux-wireless
  Cc: linux-kernel, Miri Korenblit, Brian Norris, Francesco Dolcini,
	Johannes Berg, Jaewan Kim, Daniel Gabay, Emmanuel Grumbach,
	Benjamin Berg, Anjaneyulu, Peddolla Harshavardhan Reddy,
	Lorenzo Bianconi, John Crispin, Avinash Patil, Cathy Luo,
	John W. Linville, Aloka Dixit, Zhao Li

Hi,

This series fixes a set of PMSR lifetime and management-frame
validation issues found while reviewing the wireless stack.

It is based on wireless/main commit cb8afea4655f ("wifi: cfg80211: bound
element ID read when checking non-inheritance").

The patches cover:

- PMSR lifetime handling in mac80211_hwsim and iwlwifi MLD: hwsim drops
  its saved cfg80211 request pointers once an abort succeeds, hwsim
  reports are accepted only from the registered wmediumd sender in the
  matching netgroup, and iwlwifi MLD gains an abort hook for active FTM
  PMSR requests.
- cfg80211 parsing of the optional FTM NOMINAL_TIME attribute.
- fixed-field validation for mac80211 probe-response countdown offsets,
  S1G individual TWT setup frames before driver callbacks, and mwifiex
  action frame category/action-code fields.
- an nl80211 color-change error path that leaked the beacon data parsed
  for the request; it is now freed as in the other beacon setters.

A larger local draft covered more issues, but the AP-template
double-free, MLE and assoc/MLME length validation, S1G beacon TSF, and
RNR cleanup fixes have since landed in wireless/main and are dropped
here.

Checks run:

- git am of patches 1-8 on wireless/main cb8afea4655f: clean
- scripts/checkpatch.pl --strict --no-signoff on this draft: clean
- VPS x86_64 KASAN build: bzImage plus iwlwifi MLD and mwifiex
  modules build clean
- VPS QEMU boot of the patched KASAN+hwsim kernel: a PMSR-capable
  hwsim radio completed PMSR_START; the stale REPORT_PMSR attempt was
  rejected with -EINVAL; no KASAN, use-after-free, out-of-bounds,
  general protection, or "unable to handle" splat was observed

Zhao Li (8):
  wifi: mac80211_hwsim: clear PMSR request state on abort
  wifi: mac80211_hwsim: authenticate PMSR report senders
  wifi: iwlwifi: mld: abort active PMSR requests
  wifi: cfg80211: guard optional PMSR nominal time
  wifi: mac80211: validate probe response countdown offsets
  wifi: mac80211: validate S1G TWT params before driver setup
  wifi: mwifiex: validate action frame fixed fields
  wifi: nl80211: clean up color-change beacon data on errors

 .../intel/iwlwifi/mld/ftm-initiator.c         | 22 +++++++++++++++++++
 .../intel/iwlwifi/mld/ftm-initiator.h         |  2 ++
 .../net/wireless/intel/iwlwifi/mld/mac80211.c | 10 +++++++++
 drivers/net/wireless/marvell/mwifiex/util.c   |  9 ++++++++
 .../wireless/virtual/mac80211_hwsim_main.c    | 19 +++++++++++++---
 net/mac80211/s1g.c                            |  4 ++++
 net/mac80211/tx.c                             |  4 ++++
 net/wireless/nl80211.c                        | 10 +++++----
 net/wireless/pmsr.c                           |  5 +++--
 9 files changed, 76 insertions(+), 9 deletions(-)

-- 
2.50.1 (Apple Git-155)

^ permalink raw reply

* Re: [PATCH wireless-next] wifi: mt76: fix MAC address for non OF pcie cards
From: John Rowley @ 2026-07-08 19:42 UTC (permalink / raw)
  To: Rosen Penev
  Cc: linux-wireless, Felix Fietkau, Lorenzo Bianconi, Ryder Lee,
	Shayne Chen, Sean Wang, Matthias Brugger,
	AngeloGioacchino Del Regno, open list:ARM/Mediatek SoC support,
	moderated list:ARM/Mediatek SoC support,
	moderated list:ARM/Mediatek SoC support
In-Reply-To: <20260630210215.400379-1-rosenp@gmail.com>

On Tue, Jun 30, 2026 at 02:02:15PM -0700, Rosen Penev wrote:
> If seems the check for err is wrong as the proper macaddr gets written
> to from the EEPROM itself. Meaning checking err from of_get_mac_address is
> wrong as the proper macaddr has been written by this point.
> 
> Fixes: 31ee1582717e ("wifi: mt76: fix of_get_mac_address error handling")
> Signed-off-by: Rosen Penev <rosenp@gmail.com>

Fixed, thanks.

Tested-by: John Rowley <lkml@johnrowley.me>

> ---
>  drivers/net/wireless/mediatek/mt76/eeprom.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/drivers/net/wireless/mediatek/mt76/eeprom.c b/drivers/net/wireless/mediatek/mt76/eeprom.c
> index b99d7452800f..afdb73661866 100644
> --- a/drivers/net/wireless/mediatek/mt76/eeprom.c
> +++ b/drivers/net/wireless/mediatek/mt76/eeprom.c
> @@ -181,7 +181,7 @@ mt76_eeprom_override(struct mt76_phy *phy)
>  	if (err == -EPROBE_DEFER)
>  		return err;
>  
> -	if (err) {
> +	if (!is_valid_ether_addr(phy->macaddr)) {
>  		eth_random_addr(phy->macaddr);
>  		dev_info(dev->dev,
>  			 "Invalid MAC address, using random address %pM\n",
> -- 
> 2.55.0
> 
> 

^ permalink raw reply

* [PATCH mt76] wifi: mt76: Disable napi when removing device
From: Nicolas Cavallari @ 2026-07-08 14:43 UTC (permalink / raw)
  To: linux-wireless
  Cc: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, Shayne Chen,
	Sean Wang, Ruslan Isaev

Unloading the mt7915e module with a MT7916 triggers multiples WARN in
__netif_napi_del_locked() and in page_pool_disable_direct_recycling()
because the driver does not disable the napi before destroying it.

This is troublesome since on MT7916 it is required to unload the module
and reinsert it with a different enable_6ghz parameter to change the
frequency.  The system generally becomes unstable after reinserting the
module.

Fix it by disabling napi before deleting it.  Also, do not delete napi
on WED queues since napi is neither used nor initialized on them.

Fixes: 17f1de56df05 ("mt76: add common code shared between multiple chipsets")
Signed-off-by: Nicolas Cavallari <nicolas.cavallari@green-communications.fr>

---
I'm aware of these previous attempts:
https://lore.kernel.org/linux-wireless/agEbiurBZzZv_Yt0@wico-dev.wnam.ru/T/#u
https://lore.kernel.org/linux-wireless/ahTdpNQp2va_xTVR@wico-dev.wnam.ru/T/#u

I can only test on a wed-less mt7916
---
 drivers/net/wireless/mediatek/mt76/dma.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wireless/mediatek/mt76/dma.c b/drivers/net/wireless/mediatek/mt76/dma.c
index f8c2fe5f2f58..322041859217 100644
--- a/drivers/net/wireless/mediatek/mt76/dma.c
+++ b/drivers/net/wireless/mediatek/mt76/dma.c
@@ -1189,7 +1189,10 @@ void mt76_dma_cleanup(struct mt76_dev *dev)
 	mt76_for_each_q_rx(dev, i) {
 		struct mt76_queue *q = &dev->q_rx[i];
 
-		netif_napi_del(&dev->napi[i]);
+		if (!mt76_queue_is_wed_rro(q)) {
+			napi_disable(&dev->napi[i]);
+			netif_napi_del(&dev->napi[i]);
+		}
 		mt76_dma_rx_cleanup(dev, q);
 
 		page_pool_destroy(q->page_pool);

base-commit: 50a7f9f9d48eb50c0e95bef53358acb5af5cb3c6
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 8/8] wifi: ath10k: Use pci_{enable/disable}_link_state() APIs to enable/disable ASPM states
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

It is not recommended to enable/disable the ASPM states on the back of the
PCI core directly using the LNKCTL register. It will break the PCI core's
knowledge about the device ASPM states. So use the APIs exposed by the PCI
core to enable/disable ASPM states.

Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/net/wireless/ath/ath10k/Kconfig |  2 +-
 drivers/net/wireless/ath/ath10k/pci.c   | 11 ++++-------
 drivers/net/wireless/ath/ath10k/pci.h   |  5 ++---
 3 files changed, 7 insertions(+), 11 deletions(-)

diff --git a/drivers/net/wireless/ath/ath10k/Kconfig b/drivers/net/wireless/ath/ath10k/Kconfig
index efb9f022d8c6..eac3ec5a4e78 100644
--- a/drivers/net/wireless/ath/ath10k/Kconfig
+++ b/drivers/net/wireless/ath/ath10k/Kconfig
@@ -17,7 +17,7 @@ config ATH10K_CE
 
 config ATH10K_PCI
 	tristate "Atheros ath10k PCI support"
-	depends on ATH10K && PCI
+	depends on ATH10K && PCI && PCIEASPM
 	help
 	  This module adds support for PCIE bus
 
diff --git a/drivers/net/wireless/ath/ath10k/pci.c b/drivers/net/wireless/ath/ath10k/pci.c
index 335bc7c488e4..2e27a70065cc 100644
--- a/drivers/net/wireless/ath/ath10k/pci.c
+++ b/drivers/net/wireless/ath/ath10k/pci.c
@@ -1965,9 +1965,7 @@ static int ath10k_pci_hif_start(struct ath10k *ar)
 	ath10k_pci_irq_enable(ar);
 	ath10k_pci_rx_post(ar);
 
-	pcie_capability_clear_and_set_word(ar_pci->pdev, PCI_EXP_LNKCTL,
-					   PCI_EXP_LNKCTL_ASPMC,
-					   ar_pci->link_ctl & PCI_EXP_LNKCTL_ASPMC);
+	pci_force_enable_link_state(ar_pci->pdev, ar_pci->aspm_states);
 
 	return 0;
 }
@@ -2822,10 +2820,9 @@ static int ath10k_pci_hif_power_up(struct ath10k *ar,
 
 	ath10k_dbg(ar, ATH10K_DBG_BOOT, "boot hif power up\n");
 
-	pcie_capability_read_word(ar_pci->pdev, PCI_EXP_LNKCTL,
-				  &ar_pci->link_ctl);
-	pcie_capability_clear_word(ar_pci->pdev, PCI_EXP_LNKCTL,
-				   PCI_EXP_LNKCTL_ASPMC);
+	ar_pci->aspm_states = pcie_aspm_enabled(ar_pci->pdev);
+
+	pci_disable_link_state(ar_pci->pdev, PCIE_LINK_STATE_ASPM_ALL);
 
 	/*
 	 * Bring the target up cleanly.
diff --git a/drivers/net/wireless/ath/ath10k/pci.h b/drivers/net/wireless/ath/ath10k/pci.h
index d114778edb41..122e365b7cdd 100644
--- a/drivers/net/wireless/ath/ath10k/pci.h
+++ b/drivers/net/wireless/ath/ath10k/pci.h
@@ -128,10 +128,9 @@ struct ath10k_pci {
 	struct timer_list rx_post_retry;
 
 	/* Due to HW quirks it is recommended to disable ASPM during device
-	 * bootup. To do that the original PCI-E Link Control is stored before
-	 * device bootup is executed and re-programmed later.
+	 * bootup. To do that the ASPM states are saved and re-programmed later.
 	 */
-	u16 link_ctl;
+	u32 aspm_states;
 
 	/* Protects ps_awake and ps_wake_refcount */
 	spinlock_t ps_lock;

-- 
2.43.0



^ permalink raw reply related

* [PATCH v3 6/8] wifi: ath12k: Use pci_{enable/disable}_link_state() APIs to enable/disable ASPM states
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

It is not recommended to enable/disable the ASPM states on the back of the
PCI core directly using the LNKCTL register. It will break the PCI core's
knowledge about the device ASPM states. So use the APIs exposed by the PCI
core to enable/disable ASPM states.

Tested-on: WCN7850 hw2.0 PCI WLAN.HMT.1.0.c5-00481-QCAHMTSWPL_V1.0_V2.0_SILICONZ-3

Reported-by: Qiang Yu <qiang.yu@oss.qualcomm.com>
Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/net/wireless/ath/ath12k/Kconfig |  2 +-
 drivers/net/wireless/ath/ath12k/pci.c   | 19 +++----------------
 drivers/net/wireless/ath/ath12k/pci.h   |  4 +++-
 3 files changed, 7 insertions(+), 18 deletions(-)

diff --git a/drivers/net/wireless/ath/ath12k/Kconfig b/drivers/net/wireless/ath/ath12k/Kconfig
index 4a2b240f967a..7852ede3eaea 100644
--- a/drivers/net/wireless/ath/ath12k/Kconfig
+++ b/drivers/net/wireless/ath/ath12k/Kconfig
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: BSD-3-Clause-Clear
 config ATH12K
 	tristate "Qualcomm Wi-Fi 7 support (ath12k)"
-	depends on MAC80211 && HAS_DMA && PCI
+	depends on MAC80211 && HAS_DMA && PCI && PCIEASPM
 	select QCOM_QMI_HELPERS
 	select MHI_BUS
 	select QRTR
diff --git a/drivers/net/wireless/ath/ath12k/pci.c b/drivers/net/wireless/ath/ath12k/pci.c
index d9a22d6afbb0..6d48fb25690d 100644
--- a/drivers/net/wireless/ath/ath12k/pci.c
+++ b/drivers/net/wireless/ath/ath12k/pci.c
@@ -884,19 +884,9 @@ static void ath12k_pci_free_region(struct ath12k_pci *ab_pci)
 
 static void ath12k_pci_aspm_disable(struct ath12k_pci *ab_pci)
 {
-	struct ath12k_base *ab = ab_pci->ab;
-
-	pcie_capability_read_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-				  &ab_pci->link_ctl);
-
-	ath12k_dbg(ab, ATH12K_DBG_PCI, "pci link_ctl 0x%04x L0s %d L1 %d\n",
-		   ab_pci->link_ctl,
-		   u16_get_bits(ab_pci->link_ctl, PCI_EXP_LNKCTL_ASPM_L0S),
-		   u16_get_bits(ab_pci->link_ctl, PCI_EXP_LNKCTL_ASPM_L1));
+	ab_pci->aspm_states = pcie_aspm_enabled(ab_pci->pdev);
 
-	/* disable L0s and L1 */
-	pcie_capability_clear_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-				   PCI_EXP_LNKCTL_ASPMC);
+	pci_disable_link_state(ab_pci->pdev, PCIE_LINK_STATE_ASPM_ALL);
 
 	set_bit(ATH12K_PCI_ASPM_RESTORE, &ab_pci->flags);
 }
@@ -925,10 +915,7 @@ static void ath12k_pci_aspm_restore(struct ath12k_pci *ab_pci)
 {
 	if (ab_pci->ab->hw_params->supports_aspm &&
 	    test_and_clear_bit(ATH12K_PCI_ASPM_RESTORE, &ab_pci->flags))
-		pcie_capability_clear_and_set_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-						   PCI_EXP_LNKCTL_ASPMC,
-						   ab_pci->link_ctl &
-						   PCI_EXP_LNKCTL_ASPMC);
+		pci_force_enable_link_state(ab_pci->pdev, ab_pci->aspm_states);
 }
 
 static void ath12k_pci_cancel_workqueue(struct ath12k_base *ab)
diff --git a/drivers/net/wireless/ath/ath12k/pci.h b/drivers/net/wireless/ath/ath12k/pci.h
index 0e0e2020c6ae..409ef063cd69 100644
--- a/drivers/net/wireless/ath/ath12k/pci.h
+++ b/drivers/net/wireless/ath/ath12k/pci.h
@@ -128,7 +128,9 @@ struct ath12k_pci {
 
 	/* enum ath12k_pci_flags */
 	unsigned long flags;
-	u16 link_ctl;
+
+	/* Cached PCIe ASPM states */
+	u32 aspm_states;
 	unsigned long irq_flags;
 	const struct ath12k_pci_ops *pci_ops;
 	u32 qmi_instance;

-- 
2.43.0



^ permalink raw reply related

* [PATCH v3 5/8] PCI/ASPM: Return enabled ASPM states from pcie_aspm_enabled() API
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

pcie_aspm_enabled() returns 'pcie_link_state::aspm_enabled' parameter which
contains the enabled states. But the API currently returns the 'bool' type
which is used by the callers to decide if ASPM is enabled or not.

To allow the future callers to also make use of the enabled ASPM states,
return the actual type of 'pcie_link_state::aspm_enabled' parameter, 'u32'.

Existing callers can still treat the return value as a 'bool' as the C11
standard guarantees the behavior (this API relied on the same behavior
before as well).

Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/pci/pcie/aspm.c | 6 ++++--
 include/linux/pci.h     | 4 ++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/drivers/pci/pcie/aspm.c b/drivers/pci/pcie/aspm.c
index 5e7e199c78d2..3ce6b9319566 100644
--- a/drivers/pci/pcie/aspm.c
+++ b/drivers/pci/pcie/aspm.c
@@ -1694,15 +1694,17 @@ module_param_call(policy, pcie_aspm_set_policy, pcie_aspm_get_policy,
 	NULL, 0644);
 
 /**
- * pcie_aspm_enabled - Check if PCIe ASPM has been enabled for a device.
+ * pcie_aspm_enabled - Get the enabled PCIe ASPM states for a device.
  * @pdev: Target device.
  *
  * Relies on the upstream bridge's link_state being valid.  The link_state
  * is deallocated only when the last child of the bridge (i.e., @pdev or a
  * sibling) is removed, and the caller should be holding a reference to
  * @pdev, so this should be safe.
+ *
+ * Return: Enabled PCIe ASPM states. 0 if ASPM is disabled.
  */
-bool pcie_aspm_enabled(struct pci_dev *pdev)
+u32 pcie_aspm_enabled(struct pci_dev *pdev)
 {
 	struct pcie_link_state *link = pcie_aspm_get_link(pdev);
 
diff --git a/include/linux/pci.h b/include/linux/pci.h
index be6719626c8f..61f6b78c19fd 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1921,7 +1921,7 @@ int pci_enable_link_state_locked(struct pci_dev *pdev, int state);
 int pci_force_enable_link_state(struct pci_dev *pdev, int state);
 void pcie_no_aspm(void);
 bool pcie_aspm_support_enabled(void);
-bool pcie_aspm_enabled(struct pci_dev *pdev);
+u32 pcie_aspm_enabled(struct pci_dev *pdev);
 #else
 static inline int pci_disable_link_state(struct pci_dev *pdev, int state)
 { return 0; }
@@ -1935,7 +1935,7 @@ static inline int pci_force_enable_link_state(struct pci_dev *pdev, int state)
 { return 0; }
 static inline void pcie_no_aspm(void) { }
 static inline bool pcie_aspm_support_enabled(void) { return false; }
-static inline bool pcie_aspm_enabled(struct pci_dev *pdev) { return false; }
+static inline u32 pcie_aspm_enabled(struct pci_dev *pdev) { return 0; }
 #endif
 
 #ifdef CONFIG_HOTPLUG_PCI

-- 
2.43.0



^ permalink raw reply related

* [PATCH v3 7/8] wifi: ath11k: Use pci_{enable/disable}_link_state() APIs to enable/disable ASPM states
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

It is not recommended to enable/disable the ASPM states on the back of the
PCI core directly using the LNKCTL register. It will break the PCI core's
knowledge about the device ASPM states. So use the APIs exposed by the PCI
core to enable/disable ASPM states.

Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/net/wireless/ath/ath11k/Kconfig |  2 +-
 drivers/net/wireless/ath/ath11k/pci.c   | 19 +++----------------
 drivers/net/wireless/ath/ath11k/pci.h   |  3 ++-
 3 files changed, 6 insertions(+), 18 deletions(-)

diff --git a/drivers/net/wireless/ath/ath11k/Kconfig b/drivers/net/wireless/ath/ath11k/Kconfig
index 122726f84492..c8b418521173 100644
--- a/drivers/net/wireless/ath/ath11k/Kconfig
+++ b/drivers/net/wireless/ath/ath11k/Kconfig
@@ -19,7 +19,7 @@ config ATH11K_AHB
 
 config ATH11K_PCI
 	tristate "Atheros ath11k PCI support"
-	depends on ATH11K && PCI
+	depends on ATH11K && PCI && PCIEASPM
 	select MHI_BUS
 	select QRTR
 	select QRTR_MHI
diff --git a/drivers/net/wireless/ath/ath11k/pci.c b/drivers/net/wireless/ath/ath11k/pci.c
index 35bb9e7a63a2..da980e01d1fc 100644
--- a/drivers/net/wireless/ath/ath11k/pci.c
+++ b/drivers/net/wireless/ath/ath11k/pci.c
@@ -604,19 +604,9 @@ static void ath11k_pci_free_region(struct ath11k_pci *ab_pci)
 
 static void ath11k_pci_aspm_disable(struct ath11k_pci *ab_pci)
 {
-	struct ath11k_base *ab = ab_pci->ab;
-
-	pcie_capability_read_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-				  &ab_pci->link_ctl);
-
-	ath11k_dbg(ab, ATH11K_DBG_PCI, "link_ctl 0x%04x L0s %d L1 %d\n",
-		   ab_pci->link_ctl,
-		   u16_get_bits(ab_pci->link_ctl, PCI_EXP_LNKCTL_ASPM_L0S),
-		   u16_get_bits(ab_pci->link_ctl, PCI_EXP_LNKCTL_ASPM_L1));
+	ab_pci->aspm_states = pcie_aspm_enabled(ab_pci->pdev);
 
-	/* disable L0s and L1 */
-	pcie_capability_clear_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-				   PCI_EXP_LNKCTL_ASPMC);
+	pci_disable_link_state(ab_pci->pdev, PCIE_LINK_STATE_ASPM_ALL);
 
 	set_bit(ATH11K_PCI_ASPM_RESTORE, &ab_pci->flags);
 }
@@ -624,10 +614,7 @@ static void ath11k_pci_aspm_disable(struct ath11k_pci *ab_pci)
 static void ath11k_pci_aspm_restore(struct ath11k_pci *ab_pci)
 {
 	if (test_and_clear_bit(ATH11K_PCI_ASPM_RESTORE, &ab_pci->flags))
-		pcie_capability_clear_and_set_word(ab_pci->pdev, PCI_EXP_LNKCTL,
-						   PCI_EXP_LNKCTL_ASPMC,
-						   ab_pci->link_ctl &
-						   PCI_EXP_LNKCTL_ASPMC);
+		pci_force_enable_link_state(ab_pci->pdev, ab_pci->aspm_states);
 }
 
 #ifdef CONFIG_DEV_COREDUMP
diff --git a/drivers/net/wireless/ath/ath11k/pci.h b/drivers/net/wireless/ath/ath11k/pci.h
index 1e3005a4b64c..474c18741418 100644
--- a/drivers/net/wireless/ath/ath11k/pci.h
+++ b/drivers/net/wireless/ath/ath11k/pci.h
@@ -72,7 +72,8 @@ struct ath11k_pci {
 
 	/* enum ath11k_pci_flags */
 	unsigned long flags;
-	u16 link_ctl;
+	/* Cached PCIe ASPM states */
+	u32 aspm_states;
 	u64 dma_mask;
 };
 

-- 
2.43.0



^ permalink raw reply related

* [PATCH v3 3/8] PCI/ASPM: Transition the device to D0 (if required) when enabling ASPM link states
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

Per PCIe spec r6.0, sec 5.5.4:

"If setting either or both of the enable bits for PCI-PM L1 PM Substates,
both ports must be configured as described in this section while in D0."

Currently, the callers of pci_enable_link_state_locked() (vmd, pcie-qcom)
transition the device to D0 themselves before enabling the link state. But
this is easy to get wrong and has to be duplicated by every caller.

Move the D0 transition into the shared __pci_enable_link_state() helper so
that all three APIs pci_enable_link_state(), pci_enable_link_state_locked()
and pci_force_enable_link_state() perform it, and only when the PCI-PM L1
PM Substates are getting enabled.

Now that the helper handles the transition, drop the redundant D0 transition
from the vmd and pcie-qcom callers.

Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/pci/controller/dwc/pcie-qcom.c |  5 -----
 drivers/pci/controller/vmd.c           |  5 -----
 drivers/pci/pcie/aspm.c                | 23 +++++++++++++++++------
 3 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/drivers/pci/controller/dwc/pcie-qcom.c b/drivers/pci/controller/dwc/pcie-qcom.c
index d8eb52857f69..45f4caeb0814 100644
--- a/drivers/pci/controller/dwc/pcie-qcom.c
+++ b/drivers/pci/controller/dwc/pcie-qcom.c
@@ -1085,11 +1085,6 @@ static int qcom_pcie_post_init_2_7_0(struct qcom_pcie *pcie)
 
 static int qcom_pcie_enable_aspm(struct pci_dev *pdev, void *userdata)
 {
-	/*
-	 * Downstream devices need to be in D0 state before enabling PCI PM
-	 * substates.
-	 */
-	pci_set_power_state_locked(pdev, PCI_D0);
 	pci_enable_link_state_locked(pdev, PCIE_LINK_STATE_ALL);
 
 	return 0;
diff --git a/drivers/pci/controller/vmd.c b/drivers/pci/controller/vmd.c
index d4ae250d4bc6..20597d2cf66e 100644
--- a/drivers/pci/controller/vmd.c
+++ b/drivers/pci/controller/vmd.c
@@ -762,11 +762,6 @@ static int vmd_pm_enable_quirk(struct pci_dev *pdev, void *userdata)
 	pci_info(pdev, "VMD: Default LTR value set by driver\n");
 
 out_state_change:
-	/*
-	 * Ensure devices are in D0 before enabling PCI-PM L1 PM Substates, per
-	 * PCIe r6.0, sec 5.5.4.
-	 */
-	pci_set_power_state_locked(pdev, PCI_D0);
 	pci_enable_link_state_locked(pdev, PCIE_LINK_STATE_ALL);
 	return 0;
 }
diff --git a/drivers/pci/pcie/aspm.c b/drivers/pci/pcie/aspm.c
index c04fb71de91c..6d6862fd2ebb 100644
--- a/drivers/pci/pcie/aspm.c
+++ b/drivers/pci/pcie/aspm.c
@@ -1524,6 +1524,17 @@ static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked,
 		return -EPERM;
 	}
 
+	/*
+	 * Ensure the device is in D0 before enabling PCI-PM L1 PM Substates, per
+	 * PCIe r6.0, sec 5.5.4.
+	 */
+	if (state & PCIE_LINK_STATE_L1_SS_PCIPM) {
+		if (locked)
+			pci_set_power_state_locked(pdev, PCI_D0);
+		else
+			pci_set_power_state(pdev, PCI_D0);
+	}
+
 	if (!locked)
 		down_read(&pci_bus_sem);
 	mutex_lock(&aspm_lock);
@@ -1550,8 +1561,8 @@ static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked,
  * touch the LNKCTL register. Also note that this does not enable states
  * disabled by pci_disable_link_state(). Return 0 or a negative errno.
  *
- * Note: Ensure devices are in D0 before enabling PCI-PM L1 PM Substates, per
- * PCIe r6.0, sec 5.5.4.
+ * Note: The device will be transitioned to D0 state if the PCI-PM L1 Substates
+ * are getting enabled.
  *
  * @pdev: PCI device
  * @state: Mask of ASPM link states to enable
@@ -1569,8 +1580,8 @@ EXPORT_SYMBOL(pci_enable_link_state);
  * can't touch the LNKCTL register. Also note that this does not enable states
  * disabled by pci_disable_link_state(). Return 0 or a negative errno.
  *
- * Note: Ensure devices are in D0 before enabling PCI-PM L1 PM Substates, per
- * PCIe r6.0, sec 5.5.4.
+ * Note: The device will be transitioned to D0 state if the PCI-PM L1 Substates
+ * are getting enabled.
  *
  * @pdev: PCI device
  * @state: Mask of ASPM link states to enable
@@ -1600,8 +1611,8 @@ EXPORT_SYMBOL(pci_enable_link_state_locked);
  * Note that if the BIOS didn't grant ASPM control to the OS, this does nothing
  * because we can't touch the LNKCTL register.
  *
- * Note: Ensure devices are in D0 before enabling PCI-PM L1 PM Substates, per
- * PCIe r6.0, sec 5.5.4.
+ * Note: The device will be transitioned to D0 state if the PCI-PM L1 Substates
+ * are getting enabled.
  *
  * Return: 0 on success, a negative errno otherwise.
  */

-- 
2.43.0



^ permalink raw reply related

* [PATCH v3 2/8] PCI/ASPM: Add pci_force_enable_link_state() API
From: Manivannan Sadhasivam @ 2026-07-08 14:30 UTC (permalink / raw)
  To: Bjorn Helgaas, Lorenzo Pieralisi, Krzysztof Wilczyński,
	Rob Herring, Nirmal Patel, Jonathan Derrick, Jeff Johnson
  Cc: linux-pci, linux-kernel, linux-arm-msm, linux-wireless, ath12k,
	ath11k, ath10k, Krishna Chaitanya Chundru, Qiang Yu, mani,
	Ilpo Järvinen, Manivannan Sadhasivam
In-Reply-To: <20260708-pci-aspm-fix-v3-0-6bd72451746e@kernel.org>

From: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>

pci_enable_link_state() and pci_enable_link_state_locked() APIs only enable
the ASPM states that were not previously disabled via
pci_disable_link_state*() API or blacklisted during init (e.g. the pre-1.1
device blacklist, which is only meant to be overridden with
'pcie_aspm=force'). This is an intentional behavior as these APIs must not
silently re-enable states that were deliberately disabled, since doing so
could enable ASPM on a link where it is known to be unsafe.

However, some drivers (e.g. the atheros WLAN drivers) save the currently
enabled ASPM states, disable all ASPM states before firmware download, and
then try to restore exactly the states that were enabled before. Restoring
those states requires re-enabling states that were just disabled via
pci_disable_link_state() API. But, this cannot be achieved using the
existing APIs.

Hence, add pci_force_enable_link_state() API for such callers. Unlike
pci_enable_link_state(), it re-enables the requested states even if they
were previously disabled via pci_disable_link_state() or disabled during
init. The caller is therefore responsible for only enabling states the
device actually supports, typically the ones previously reported by
pcie_aspm_enabled().

This API is implemented by adding a 'force' parameter to the shared
__pci_enable_link_state() helper. When 'force' is true, the requested
states are cleared from 'link->aspm_disable' before enabling.

Suggested-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
Signed-off-by: Manivannan Sadhasivam <manivannan.sadhasivam@oss.qualcomm.com>
---
 drivers/pci/pcie/aspm.c | 37 ++++++++++++++++++++++++++++++++++---
 include/linux/pci.h     |  3 +++
 2 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/drivers/pci/pcie/aspm.c b/drivers/pci/pcie/aspm.c
index 1c15a14718e8..c04fb71de91c 100644
--- a/drivers/pci/pcie/aspm.c
+++ b/drivers/pci/pcie/aspm.c
@@ -1506,7 +1506,8 @@ int pci_disable_link_state(struct pci_dev *pdev, int state)
 }
 EXPORT_SYMBOL(pci_disable_link_state);
 
-static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked)
+static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked,
+				   bool force)
 {
 	struct pcie_link_state *link = pcie_aspm_get_link(pdev);
 
@@ -1527,6 +1528,10 @@ static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked)
 		down_read(&pci_bus_sem);
 	mutex_lock(&aspm_lock);
 	link->aspm_default = pci_calc_aspm_enable_mask(state);
+
+	/* Force enable states that were previously disabled */
+	if (force)
+		link->aspm_disable &= ~link->aspm_default;
 	pcie_config_aspm_link(link, policy_to_aspm_state(link));
 
 	link->clkpm_default = (state & PCIE_LINK_STATE_CLKPM) ? 1 : 0;
@@ -1553,7 +1558,7 @@ static int __pci_enable_link_state(struct pci_dev *pdev, int state, bool locked)
  */
 int pci_enable_link_state(struct pci_dev *pdev, int state)
 {
-	return __pci_enable_link_state(pdev, state, false);
+	return __pci_enable_link_state(pdev, state, false, false);
 }
 EXPORT_SYMBOL(pci_enable_link_state);
 
@@ -1576,10 +1581,36 @@ int pci_enable_link_state_locked(struct pci_dev *pdev, int state)
 {
 	lockdep_assert_held_read(&pci_bus_sem);
 
-	return __pci_enable_link_state(pdev, state, true);
+	return __pci_enable_link_state(pdev, state, true, false);
 }
 EXPORT_SYMBOL(pci_enable_link_state_locked);
 
+/**
+ * pci_force_enable_link_state - Force enable device link state
+ * @pdev: PCI device
+ * @state: Mask of ASPM link states to enable
+ *
+ * Enable device link state, so the link will enter the specified states.
+ * Unlike pci_enable_link_state(), this also re-enables states previously
+ * disabled by pci_disable_link_state() and overrides the ASPM states disabled
+ * during init (e.g., the pre-1.1 device blacklist). The caller is therefore
+ * responsible for only enabling states the device supports, typically the ones
+ * previously reported by pcie_aspm_enabled().
+ *
+ * Note that if the BIOS didn't grant ASPM control to the OS, this does nothing
+ * because we can't touch the LNKCTL register.
+ *
+ * Note: Ensure devices are in D0 before enabling PCI-PM L1 PM Substates, per
+ * PCIe r6.0, sec 5.5.4.
+ *
+ * Return: 0 on success, a negative errno otherwise.
+ */
+int pci_force_enable_link_state(struct pci_dev *pdev, int state)
+{
+	return __pci_enable_link_state(pdev, state, false, true);
+}
+EXPORT_SYMBOL(pci_force_enable_link_state);
+
 void pcie_aspm_remove_cap(struct pci_dev *pdev, u32 lnkcap)
 {
 	if (lnkcap & PCI_EXP_LNKCAP_ASPM_L0S)
diff --git a/include/linux/pci.h b/include/linux/pci.h
index ebb5b9d76360..be6719626c8f 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1918,6 +1918,7 @@ int pci_disable_link_state(struct pci_dev *pdev, int state);
 int pci_disable_link_state_locked(struct pci_dev *pdev, int state);
 int pci_enable_link_state(struct pci_dev *pdev, int state);
 int pci_enable_link_state_locked(struct pci_dev *pdev, int state);
+int pci_force_enable_link_state(struct pci_dev *pdev, int state);
 void pcie_no_aspm(void);
 bool pcie_aspm_support_enabled(void);
 bool pcie_aspm_enabled(struct pci_dev *pdev);
@@ -1930,6 +1931,8 @@ static inline int pci_enable_link_state(struct pci_dev *pdev, int state)
 { return 0; }
 static inline int pci_enable_link_state_locked(struct pci_dev *pdev, int state)
 { return 0; }
+static inline int pci_force_enable_link_state(struct pci_dev *pdev, int state)
+{ return 0; }
 static inline void pcie_no_aspm(void) { }
 static inline bool pcie_aspm_support_enabled(void) { return false; }
 static inline bool pcie_aspm_enabled(struct pci_dev *pdev) { return false; }

-- 
2.43.0



^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox