Linux wireless drivers development
 help / color / mirror / Atom feed
* Re: [PATCH 2/6] remoteproc: qcom: Add M0 BTSS secure PIL driver
From: Bjorn Andersson @ 2026-06-28  3:07 UTC (permalink / raw)
  To: george.moussalem
  Cc: Jens Axboe, Ulf Hansson, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Johannes Berg, Jeff Johnson, Bartosz Golaszewski,
	Marcel Holtmann, Luiz Augusto von Dentz, Balakrishna Godavarthi,
	Rocky Liao, Saravana Kannan, Andrew Lunn, Heiner Kallweit,
	Russell King, David S. Miller, Eric Dumazet, Jakub Kicinski,
	Paolo Abeni, Simon Horman, Konrad Dybcio, Mathieu Poirier,
	Philipp Zabel, linux-block, linux-kernel, linux-mmc, devicetree,
	linux-wireless, ath10k, linux-arm-msm, linux-bluetooth, netdev,
	linux-remoteproc
In-Reply-To: <20260625-ipq5018-bluetooth-v1-2-d999be0e04f7@outlook.com>

On Thu, Jun 25, 2026 at 06:10:06PM +0400, George Moussalem via B4 Relay wrote:
> From: George Moussalem <george.moussalem@outlook.com>
> 
> Add support to bring up the M0 core of the bluetooth subsystem found in
> the IPQ5018 SoC.
> 
> The signed firmware loaded is authenticated by TrustZone. If successful,
> the M0 core boots the firmware and the peripheral is taken out of reset
> using a Secure Channel Manager call to TrustZone.
> 

The remoteproc framework deals with life cycle management of
coprocessors, but you don't want that - you want the BT driver to own
the life cycle.

Further, the fact that you split this in "BT" and "remoteproc", results
in you having two representations in DeviceTree and in the device model
for the same hardware.

I know we have examples of this in the kernel already, but they are all
racy...

Please see if you can embed the firmware loading, authentication and
PAS calls directly into the BT driver - to have a single entity managing
the life cycle of your M0 processor.

Regards,
Bjorn

> Signed-off-by: George Moussalem <george.moussalem@outlook.com>
> ---
>  drivers/remoteproc/Kconfig            |  12 ++
>  drivers/remoteproc/Makefile           |   1 +
>  drivers/remoteproc/qcom_m0_btss_pil.c | 261 ++++++++++++++++++++++++++++++++++
>  3 files changed, 274 insertions(+)
> 
> diff --git a/drivers/remoteproc/Kconfig b/drivers/remoteproc/Kconfig
> index c521c744e7db..6b52f78f1427 100644
> --- a/drivers/remoteproc/Kconfig
> +++ b/drivers/remoteproc/Kconfig
> @@ -163,6 +163,18 @@ config PRU_REMOTEPROC
>  	  processors on various TI SoCs. It's safe to say N here if you're
>  	  not interested in the PRU or if you are unsure.
>  
> +config QCOM_M0_BTSS_PIL
> +	tristate "Qualcomm M0 BTSS Peripheral Image Loader"
> +	depends on OF && ARCH_QCOM
> +	select QCOM_MDT_LOADER
> +	select QCOM_RPROC_COMMON
> +	select QCOM_SCM
> +	help
> +	  Say y here to support the Secure Peripheral Imager Loader for the
> +	  Qualcomm Bluetooth Subsystem running on the M0 remote processor found
> +	  in the IPQ5018 SoC. The M0 core is started and stopped using a
> +	  Secure Channel Manager call to TrustZone.
> +
>  config QCOM_PIL_INFO
>  	tristate
>  
> diff --git a/drivers/remoteproc/Makefile b/drivers/remoteproc/Makefile
> index 1c7598b8475d..df80faf8d0df 100644
> --- a/drivers/remoteproc/Makefile
> +++ b/drivers/remoteproc/Makefile
> @@ -21,6 +21,7 @@ obj-$(CONFIG_DA8XX_REMOTEPROC)		+= da8xx_remoteproc.o
>  obj-$(CONFIG_KEYSTONE_REMOTEPROC)	+= keystone_remoteproc.o
>  obj-$(CONFIG_MESON_MX_AO_ARC_REMOTEPROC)+= meson_mx_ao_arc.o
>  obj-$(CONFIG_PRU_REMOTEPROC)		+= pru_rproc.o
> +obj-$(CONFIG_QCOM_M0_BTSS_PIL)		+= qcom_m0_btss_pil.o
>  obj-$(CONFIG_QCOM_PIL_INFO)		+= qcom_pil_info.o
>  obj-$(CONFIG_QCOM_RPROC_COMMON)		+= qcom_common.o
>  obj-$(CONFIG_QCOM_Q6V5_COMMON)		+= qcom_q6v5.o
> diff --git a/drivers/remoteproc/qcom_m0_btss_pil.c b/drivers/remoteproc/qcom_m0_btss_pil.c
> new file mode 100644
> index 000000000000..7168e270e4d4
> --- /dev/null
> +++ b/drivers/remoteproc/qcom_m0_btss_pil.c
> @@ -0,0 +1,261 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright (c) 2026 The Linux Foundation. All rights reserved.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/delay.h>
> +#include <linux/elf.h>
> +#include <linux/firmware/qcom/qcom_scm.h>
> +#include <linux/io.h>
> +#include <linux/kernel.h>
> +#include <linux/of_reserved_mem.h>
> +#include <linux/platform_device.h>
> +#include <linux/reset.h>
> +#include <linux/soc/qcom/mdt_loader.h>
> +
> +#include "qcom_common.h"
> +
> +#define BTSS_PAS_ID	0xc
> +
> +struct m0_btss {
> +	struct device *dev;
> +	phys_addr_t mem_phys;
> +	phys_addr_t mem_reloc;
> +	void __iomem *mem_region;
> +	size_t mem_size;
> +	struct reset_control *btss_reset;
> +};
> +
> +static int m0_btss_start(struct rproc *rproc)
> +{
> +	int ret;
> +
> +	if (!qcom_scm_pas_supported(BTSS_PAS_ID)) {
> +		dev_err(rproc->dev.parent,
> +			"PAS is not available for peripheral: 0x%x\n",
> +			BTSS_PAS_ID);
> +		return -ENODEV;
> +	}
> +
> +	ret = qcom_scm_pas_auth_and_reset(BTSS_PAS_ID);
> +	if (ret) {
> +		dev_err(rproc->dev.parent, "Failed to start rproc: %d\n", ret);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int m0_btss_stop(struct rproc *rproc)
> +{
> +	int ret;
> +
> +	if (rproc->state == RPROC_RUNNING || rproc->state == RPROC_CRASHED) {
> +		ret = qcom_scm_pas_shutdown(BTSS_PAS_ID);
> +		if (ret) {
> +			dev_err(rproc->dev.parent, "Failed to stop rproc: %d\n",
> +				ret);
> +			return ret;
> +		}
> +
> +		dev_info(rproc->dev.parent, "Successfully stopped rproc\n");
> +	}
> +
> +	return 0;
> +}
> +
> +static int m0_btss_load(struct rproc *rproc, const struct firmware *fw)
> +{
> +	struct m0_btss *desc = rproc->priv;
> +	const struct elf32_phdr *phdrs;
> +	const struct firmware *seg_fw;
> +	const struct elf32_phdr *phdr;
> +	const struct elf32_hdr *ehdr;
> +	void __iomem *metadata;
> +	size_t metadata_size;
> +	int i, ret;
> +
> +	ehdr = (const struct elf32_hdr *)fw->data;
> +	phdrs = (const struct elf32_phdr *)(ehdr + 1);
> +
> +	ret = request_firmware(&fw, rproc->firmware, rproc->dev.parent);
> +	if (ret) {
> +		dev_err(rproc->dev.parent, "Failed to request firmware: %d\n",
> +			ret);
> +		return ret;
> +	}
> +
> +	metadata = qcom_mdt_read_metadata(fw, &metadata_size, rproc->firmware,
> +					  rproc->dev.parent);
> +	if (IS_ERR(metadata)) {
> +		ret = PTR_ERR(metadata);
> +		dev_err(rproc->dev.parent,
> +			"Failed to read firmware metadata: %d\n", ret);
> +		goto release_fw;
> +	}
> +
> +	ret = qcom_scm_pas_init_image(BTSS_PAS_ID, metadata,
> +				      metadata_size, NULL);
> +	if (ret) {
> +		dev_err(rproc->dev.parent, "PAS init image failed: %d\n", ret);
> +		goto free_metadata;
> +	}
> +
> +	for (i = 0; i < ehdr->e_phnum; i++) {
> +		char *seg_name __free(kfree) = kstrdup(rproc->firmware,
> +						       GFP_KERNEL);
> +		if (!seg_name)
> +			return -ENOMEM;
> +
> +		phdr = &phdrs[i];
> +
> +		/* Only process valid loadable data segments */
> +		if (phdr->p_type != PT_LOAD || !phdr->p_memsz)
> +			continue;
> +
> +		if (phdr->p_vaddr + phdr->p_filesz > desc->mem_size) {
> +			dev_err(rproc->dev.parent,
> +				"Segment data exceeds the reserved memory area!\n");
> +			goto free_metadata;
> +		}
> +
> +		/* Check if firmware is split across multiple segment files */
> +		if (phdr->p_offset > fw->size ||
> +		    phdr->p_offset + phdr->p_filesz > fw->size) {
> +			sprintf(seg_name + strlen(seg_name) - 3, "b%02d", i);
> +			ret = request_firmware(&seg_fw, seg_name,
> +					       rproc->dev.parent);
> +			if (ret) {
> +				dev_err(rproc->dev.parent,
> +					"Could not find split segment binary: %s\n",
> +					seg_name);
> +				goto free_metadata;
> +			}
> +
> +			/*
> +			 * Use the virtual instead of the physical address as
> +			 * the offset
> +			 */
> +			memcpy_toio(desc->mem_region + phdr->p_vaddr,
> +				    seg_fw->data, phdr->p_filesz);
> +
> +			release_firmware(seg_fw);
> +		} else {
> +			memcpy_toio(desc->mem_region + phdr->p_vaddr,
> +				    fw->data + phdr->p_offset, phdr->p_filesz);
> +		}
> +	}
> +
> +	return 0;
> +
> +free_metadata:
> +	kfree(metadata);
> +release_fw:
> +	release_firmware(fw);
> +	return ret;
> +}
> +
> +static const struct rproc_ops m0_btss_ops = {
> +	.start = m0_btss_start,
> +	.stop = m0_btss_stop,
> +	.load = m0_btss_load,
> +	.get_boot_addr = rproc_elf_get_boot_addr,
> +};
> +
> +static int m0_btss_alloc_memory_region(struct m0_btss *desc)
> +{
> +	struct device *dev = desc->dev;
> +	struct resource res;
> +	int ret;
> +
> +	ret = of_reserved_mem_region_to_resource(dev->of_node, 0, &res);
> +	if (ret) {
> +		dev_err(dev, "unable to acquire memory-region resource\n");
> +		return ret;
> +	}
> +
> +	desc->mem_phys = res.start;
> +	desc->mem_reloc = res.start;
> +	desc->mem_size = resource_size(&res);
> +	desc->mem_region = devm_ioremap(dev, desc->mem_phys, desc->mem_size);
> +	if (!desc->mem_region) {
> +		dev_err(dev, "unable to map memory region: %pR\n", &res);
> +		return -ENOMEM;
> +	}
> +
> +	return 0;
> +}
> +
> +static int m0_btss_pil_probe(struct platform_device *pdev)
> +{
> +	// struct reset_control *btss_reset;
> +	struct device *dev = &pdev->dev;
> +	const char *fw_name = NULL;
> +	struct m0_btss *desc;
> +	struct clk *lpo_clk;
> +	struct rproc *rproc;
> +	int ret;
> +
> +	ret = of_property_read_string(dev->of_node, "firmware-name",
> +				      &fw_name);
> +	if (ret < 0)
> +		return ret;
> +
> +	rproc = devm_rproc_alloc(dev, "m0btss", &m0_btss_ops,
> +				 fw_name, sizeof(*desc));
> +	if (!rproc) {
> +		dev_err(dev, "failed to allocate rproc\n");
> +		return -ENOMEM;
> +	}
> +
> +	desc = rproc->priv;
> +	desc->dev = dev;
> +
> +	ret = m0_btss_alloc_memory_region(desc);
> +	if (ret)
> +		return ret;
> +
> +	lpo_clk = devm_clk_get_enabled(dev, "btss_lpo_clk");
> +	if (IS_ERR(lpo_clk))
> +		return dev_err_probe(dev, PTR_ERR(lpo_clk),
> +				     "Failed to get lpo clock\n");
> +
> +	desc->btss_reset = devm_reset_control_get(dev, "btss_reset");
> +	if (IS_ERR_OR_NULL(desc->btss_reset))
> +		return dev_err_probe(dev, PTR_ERR(desc->btss_reset),
> +				     "unable to acquire btss_reset\n");
> +
> +	ret = reset_control_deassert(desc->btss_reset);
> +	if (ret)
> +		return dev_err_probe(rproc->dev.parent, ret,
> +				     "Failed to deassert reset\n");
> +
> +	rproc->auto_boot = false;
> +	ret = devm_rproc_add(dev, rproc);
> +	if (ret)
> +		return ret;
> +
> +	platform_set_drvdata(pdev, rproc);
> +
> +	return 0;
> +}
> +
> +static const struct of_device_id m0_btss_of_match[] = {
> +	{ .compatible = "qcom,ipq5018-btss-pil" },
> +	{ },
> +};
> +MODULE_DEVICE_TABLE(of, m0_btss_of_match);
> +
> +static struct platform_driver m0_btss_pil_driver = {
> +	.probe = m0_btss_pil_probe,
> +	.driver = {
> +		.name = "qcom-m0-btss-pil",
> +		.of_match_table = m0_btss_of_match,
> +	},
> +};
> +
> +module_platform_driver(m0_btss_pil_driver);
> +
> +MODULE_DESCRIPTION("Qualcomm M0 Bluetooth Subsystem Peripheral Image Loader");
> +MODULE_LICENSE("GPL");
> 
> -- 
> 2.53.0
> 
> 

^ permalink raw reply

* [PATCH] wifi: mac80211_hwsim: avoid treating MCS as legacy rate index
From: Yousef Alhouseen @ 2026-06-28  0:25 UTC (permalink / raw)
  To: Johannes Berg
  Cc: linux-wireless, linux-kernel, Benjamin Berg, stable,
	syzbot+21629c14aa749636db9d, Yousef Alhouseen

Injected HT and VHT rates store an MCS value in rates[0].idx rather
than an index into the legacy bitrate table. hwsim nevertheless passes
these rates to ieee80211_get_tx_rate() while generating monitor frames
and timestamps.

A crafted injected frame can therefore read beyond the bitrate table.
If the resulting bitrate is zero, mac80211_hwsim_write_tsf() also
divides by zero, as observed by syzbot.

Use ieee80211_get_tx_rate() only for legacy rates. The existing fallback
continues to supply a conservative bitrate where hwsim does not yet
calculate MCS rates.

Fixes: e75129031f1c ("wifi: mac80211_hwsim: move timestamp writing later in the datapath")
Reported-by: syzbot+21629c14aa749636db9d@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=21629c14aa749636db9d
Cc: stable@vger.kernel.org
Signed-off-by: Yousef Alhouseen <alhouseenyousef@gmail.com>
---
 .../net/wireless/virtual/mac80211_hwsim_main.c    | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_main.c b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
index 0dd8a6c85953..4a66272526f3 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_main.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_main.c
@@ -1324,6 +1324,17 @@ static void mac80211_hwsim_set_tsf(struct ieee80211_hw *hw,
 	}
 }
 
+static struct ieee80211_rate *
+mac80211_hwsim_get_tx_rate(struct ieee80211_hw *hw,
+			   struct ieee80211_tx_info *info)
+{
+	if (info->control.rates[0].flags &
+	    (IEEE80211_TX_RC_MCS | IEEE80211_TX_RC_VHT_MCS))
+		return NULL;
+
+	return ieee80211_get_tx_rate(hw, info);
+}
+
 static void mac80211_hwsim_monitor_rx(struct ieee80211_hw *hw,
 				      struct sk_buff *tx_skb,
 				      struct ieee80211_channel *chan)
@@ -1333,7 +1344,7 @@ static void mac80211_hwsim_monitor_rx(struct ieee80211_hw *hw,
 	struct hwsim_radiotap_hdr *hdr;
 	u16 flags, bitrate;
 	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx_skb);
-	struct ieee80211_rate *txrate = ieee80211_get_tx_rate(hw, info);
+	struct ieee80211_rate *txrate = mac80211_hwsim_get_tx_rate(hw, info);
 
 	if (!txrate)
 		bitrate = 0;
@@ -1603,7 +1614,7 @@ static void mac80211_hwsim_write_tsf(struct mac80211_hwsim_data *data,
 
 	spin_lock_bh(&data->tsf_offset_lock);
 
-	txrate = ieee80211_get_tx_rate(data->hw, info);
+	txrate = mac80211_hwsim_get_tx_rate(data->hw, info);
 	if (txrate)
 		bitrate = txrate->bitrate;
 
-- 
2.54.0


^ permalink raw reply related

* [PATCH] wifi: ath9k: avoid device access after async firmware request
From: Yousef Alhouseen @ 2026-06-28  0:13 UTC (permalink / raw)
  To: Toke Høiland-Jørgensen
  Cc: linux-wireless, linux-kernel, stable, syzbot+cb7ed9d85261445a0201,
	Yousef Alhouseen

request_firmware_nowait() may invoke the callback before the requesting
context resumes. When a firmware lookup fails, the callback starts the
next fallback request. That nested request can exhaust the fallback list,
complete fw_done, and let disconnect free hif_dev before the parent request
returns.

The parent then dereferences hif_dev only to print a successful-request
message. Remove that post-request access so completion cannot leave an
older callback using the freed device state.

Fixes: e904cf6fe230 ("ath9k_htc: introduce support for different fw versions")
Reported-by: syzbot+cb7ed9d85261445a0201@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=cb7ed9d85261445a0201
Cc: stable@vger.kernel.org
Signed-off-by: Yousef Alhouseen <alhouseenyousef@gmail.com>
---
 drivers/net/wireless/ath/ath9k/hif_usb.c | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/drivers/net/wireless/ath/ath9k/hif_usb.c b/drivers/net/wireless/ath/ath9k/hif_usb.c
index 515267f48d80..aaf924cb8860 100644
--- a/drivers/net/wireless/ath/ath9k/hif_usb.c
+++ b/drivers/net/wireless/ath/ath9k/hif_usb.c
@@ -1222,9 +1222,6 @@ static int ath9k_hif_request_firmware(struct hif_device_usb *hif_dev,
 		return ret;
 	}
 
-	dev_info(&hif_dev->udev->dev, "ath9k_htc: Firmware %s requested\n",
-		 hif_dev->fw_name);
-
 	return ret;
 }
 
-- 
2.54.0


^ permalink raw reply related

* [PATCH wireless] wifi: p54: validate RX frame length in p54_rx_eeprom_readback()
From: Xiang Mei @ 2026-06-28  0:05 UTC (permalink / raw)
  To: Christian Lamparter, linux-wireless
  Cc: linux-kernel, Johannes Berg, Arien Judge, Lachlan Hodges,
	Weiming Shi, Xiang Mei

p54_rx_eeprom_readback() copies the requested EEPROM slice out of a
device-supplied readback frame without checking that the skb actually holds
that many bytes. Commit da1b9a55ff11 ("wifi: p54: prevent buffer-overflow in
p54_rx_eeprom_readback()") closed the destination overflow by copying a
fixed priv->eeprom_slice_size (and rejecting a mismatched advertised len),
but the source side is still unbounded: nothing verifies the frame is long
enough to supply that many bytes.

A malicious USB device can send a short frame whose advertised len matches
priv->eeprom_slice_size while the payload is truncated. The equality check
passes and memcpy() reads past the end of the skb, leaking adjacent heap:

  BUG: KASAN: slab-out-of-bounds in p54_rx (drivers/net/wireless/intersil/p54/txrx.c:507)
  Read of size 1016 at addr ffff88800f077114 by task swapper/0/0
  Call Trace:
   <IRQ>
   ...
   __asan_memcpy (mm/kasan/shadow.c:105)
   p54_rx (drivers/net/wireless/intersil/p54/txrx.c:507)
   p54u_rx_cb (drivers/net/wireless/intersil/p54/p54usb.c:163)
   __usb_hcd_giveback_urb (drivers/usb/core/hcd.c:1657)
   dummy_timer (drivers/usb/gadget/udc/dummy_hcd.c:2005)
   ...
   </IRQ>

  The buggy address belongs to the object at ffff88800f0770c0
   which belongs to the cache skbuff_small_head of size 704
  The buggy address is located 84 bytes inside of
   allocated 704-byte region [ffff88800f0770c0, ffff88800f077380)

Check that the slice fits in the skb before copying.

Fixes: 7cb770729ba8 ("p54: move eeprom code into common library")
Reported-by: Weiming Shi <bestswngs@gmail.com>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Xiang Mei <xmei5@asu.edu>
---
 drivers/net/wireless/intersil/p54/txrx.c | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/drivers/net/wireless/intersil/p54/txrx.c b/drivers/net/wireless/intersil/p54/txrx.c
index 1294a1d6528e..9f491334c8d0 100644
--- a/drivers/net/wireless/intersil/p54/txrx.c
+++ b/drivers/net/wireless/intersil/p54/txrx.c
@@ -499,11 +499,19 @@ static void p54_rx_eeprom_readback(struct p54_common *priv,
 		if (le16_to_cpu(eeprom->v2.len) != priv->eeprom_slice_size)
 			return;
 
+		if (eeprom->v2.data + priv->eeprom_slice_size >
+		    skb_tail_pointer(skb))
+			return;
+
 		memcpy(priv->eeprom, eeprom->v2.data, priv->eeprom_slice_size);
 	} else {
 		if (le16_to_cpu(eeprom->v1.len) != priv->eeprom_slice_size)
 			return;
 
+		if (eeprom->v1.data + priv->eeprom_slice_size >
+		    skb_tail_pointer(skb))
+			return;
+
 		memcpy(priv->eeprom, eeprom->v1.data, priv->eeprom_slice_size);
 	}
 
-- 
2.43.0


^ permalink raw reply related

* [PATCHv3 ath-next] wifi: ath9k: fix stale list pointers on A-MPDU subframe retry
From: Rosen Penev @ 2026-06-27 21:24 UTC (permalink / raw)
  To: linux-wireless; +Cc: Toke Høiland-Jørgensen, open list

In ath_tx_complete_aggr(), subframes that need retransmission are moved
to the local bf_head list via list_move_tail() For retry frames
(txpending), the skb is requeued to bf_pending, but bf_head goes out of
scope at the end of the loop iteration, leaving bf->list pointing to freed
stack memory.  On retry, the buffer is recovered via fi->bf in
ath_tx_get_tid_subframe(), and list_add_tail() in ath_tx_form_aggr()
overwrites the stale pointers harmlessly, so there is no leak, but the
stale pointer is technically a use-after-scope bug.

Fix by calling list_del_init() to cleanly remove the buffer from the
dead bf_head and reinitialize its list pointers before the iteration
ends.  Do not unmap DMA, do not clear fi->bf, and do not return the
buffer to the pool - the buffer is needed on retry with the original
sequence number to keep the Block Ack window consistent.

Assisted-by: opencode:big-pickle
Signed-off-by: Rosen Penev <rosenp@gmail.com>
---
 v3: simplify to just delete. No need for handling others.
 v2: set fi-bf to NULL.
 drivers/net/wireless/ath/ath9k/xmit.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/drivers/net/wireless/ath/ath9k/xmit.c b/drivers/net/wireless/ath/ath9k/xmit.c
index 7e134dd8b8c8..adb3af1b43d4 100644
--- a/drivers/net/wireless/ath/ath9k/xmit.c
+++ b/drivers/net/wireless/ath/ath9k/xmit.c
@@ -667,6 +667,9 @@ static void ath_tx_complete_aggr(struct ath_softc *sc, struct ath_txq *txq,
 			 * queue to retain ordering
 			 */
 			__skb_queue_tail(&bf_pending, skb);
+
+			if (!list_empty(&bf_head))
+				list_del_init(&bf->list);
 		}
 
 		bf = bf_next;
-- 
2.54.0


^ permalink raw reply related

* [PATCHv4 ath-next] wifi: ath9k: eeprom: drop static from local pdadc and vpdTable arrays
From: Rosen Penev @ 2026-06-27 21:20 UTC (permalink / raw)
  To: linux-wireless
  Cc: Toke Høiland-Jørgensen, Nathan Chancellor,
	Nick Desaulniers, Bill Wendling, Justin Stitt, open list,
	open list:CLANG/LLVM BUILD SUPPORT:Keyword:b(?i:clang|llvm)b

Remove the static qualifier from mutable local arrays in three EEPROM
power-calibration functions.  These arrays are written to during normal
operation, so static storage is both unnecessary and misleading: it
implies sharing across calls when no such sharing is intended, and it
makes the code subtly non-reentrant.  The sibling function in
eeprom_9287.c already uses an automatic (stack-local) pdadcValues,
confirming this is the correct pattern.

This keeps ~1 KB of data off the static data section at the cost of
stack usage, consistent with the rest of the driver's coding style.

As a safety measure, also add bounds validation for the EEPROM-derived
loop limits and indices that drive these arrays.  Without these guards,
a malformed EEPROM calibration dataset can cause stack buffer overflows
(vpdTable rows are 64 bytes but the fill loop runs up to 128 iterations),
out-of-bounds reads when the VPD table has fewer than 2 entries, a
negative-index fallback when numXpdGains == 0, and unbounded shifts in
the pdadc adjustment functions.  All of these are reachable through
on-device EEPROM data and were latent as BSS corruptions before the
stack move.

Also alias vpdTableI onto vpdTableL to shrink stack frame

vpdTableL, vpdTableR, and vpdTableI are never live simultaneously.
vpdTableL and vpdTableR are consumed during the frequency-interpolation
step that writes vpdTableI; after the if/else they are never read
again.  Reuse vpdTableL for the interpolated result (what was
vpdTableI), reducing the stack frame by one 256-byte array.

The read-via-write in the else branch is safe: ath9k_hw_interpolate()
receives vpdTableL[i][j] by value as a function argument before the
return value is written back to vpdTableL[i][j].

Stack frame size change (x86_64, clang):
  before: 0x440 (1088 B)
  after:  0x330 (816 B)

Also fix two pre-existing bugs identified in review:

 - When numXpdGains == 0 and eeprom_4k is false, the fallback read
   pPdGainBoundaries[i - 1] accessed index -1.  Guard with i > 0 and
   fall back to MAX_RATE_POWER.

 - In both eeprom_def.c and eeprom_9287.c, the PDADC padding loops
   extended the unshifted element at NUM_PDADC_VALUES - diff instead
   of the last shifted element at NUM_PDADC_VALUES - diff - 1, which
   could cause a discontinuous drop in the calibration curve's tail.

Assisted-by: opencode:big-pickle
Signed-off-by: Rosen Penev <rosenp@gmail.com
---
 v4: fix two off by one issues
 v3: clean up based on review
 v2: add bounds checks
 drivers/net/wireless/ath/ath9k/eeprom.c      | 115 +++++++++++++------
 drivers/net/wireless/ath/ath9k/eeprom_4k.c   |   2 +-
 drivers/net/wireless/ath/ath9k/eeprom_9287.c |  33 ++++--
 drivers/net/wireless/ath/ath9k/eeprom_def.c  |  12 +-
 4 files changed, 113 insertions(+), 49 deletions(-)

diff --git a/drivers/net/wireless/ath/ath9k/eeprom.c b/drivers/net/wireless/ath/ath9k/eeprom.c
index df58dc02e104..d4f1d34510e9 100644
--- a/drivers/net/wireless/ath/ath9k/eeprom.c
+++ b/drivers/net/wireless/ath/ath9k/eeprom.c
@@ -241,11 +241,24 @@ void ath9k_hw_fill_vpd_table(u8 pwrMin, u8 pwrMax, u8 *pPwrList,
 			     u8 *pVpdList, u16 numIntercepts,
 			     u8 *pRetVpdList)
 {
-	u16 i, k;
+	u16 i, k, maxIndex;
+	u16 range;
 	u8 currPwr = pwrMin;
 	u16 idxL = 0, idxR = 0;
 
-	for (i = 0; i <= (pwrMax - pwrMin) / 2; i++) {
+	if (pwrMax < pwrMin) {
+		pr_warn_ratelimited("ath9k: VPD table pwrMax (%u) < pwrMin (%u)\n", pwrMax, pwrMin);
+		memset(pRetVpdList, 0, AR5416_MAX_PWR_RANGE_IN_HALF_DB);
+		return;
+	}
+
+	range = (pwrMax - pwrMin) / 2;
+	maxIndex = min_t(u16, range, AR5416_MAX_PWR_RANGE_IN_HALF_DB - 1);
+	if (range >= AR5416_MAX_PWR_RANGE_IN_HALF_DB)
+		pr_warn_ratelimited("ath9k: VPD table range %u exceeds maximum, clamped to %u\n",
+				    range, maxIndex);
+
+	for (i = 0; i <= maxIndex; i++) {
 		ath9k_hw_get_lower_upper_index(currPwr, pPwrList,
 					       numIntercepts, &(idxL),
 					       &(idxR));
@@ -460,12 +473,8 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 	int i, j, k;
 	int16_t ss;
 	u16 idxL = 0, idxR = 0, numPiers;
-	static u8 vpdTableL[AR5416_NUM_PD_GAINS]
-		[AR5416_MAX_PWR_RANGE_IN_HALF_DB];
-	static u8 vpdTableR[AR5416_NUM_PD_GAINS]
-		[AR5416_MAX_PWR_RANGE_IN_HALF_DB];
-	static u8 vpdTableI[AR5416_NUM_PD_GAINS]
-		[AR5416_MAX_PWR_RANGE_IN_HALF_DB];
+	u8 vpdTableL[AR5416_NUM_PD_GAINS][AR5416_MAX_PWR_RANGE_IN_HALF_DB];
+	u8 vpdTableR[AR5416_NUM_PD_GAINS][AR5416_MAX_PWR_RANGE_IN_HALF_DB];
 
 	u8 *pVpdL, *pVpdR, *pPwrL, *pPwrR;
 	u8 minPwrT4[AR5416_NUM_PD_GAINS];
@@ -473,6 +482,8 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 	int16_t vpdStep;
 	int16_t tmpVal;
 	u16 sizeCurrVpdTable, maxIndex, tgtIndex;
+	u16 vpdRange, vpdFillMax;
+	u16 vpdFillMaxArr[AR5416_NUM_PD_GAINS];
 	bool match;
 	int16_t minDelta = 0;
 	struct chan_centers centers;
@@ -506,30 +517,27 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 				minPwrT4[i] = data_9287[idxL].pwrPdg[i][0];
 				maxPwrT4[i] = data_9287[idxL].pwrPdg[i][intercepts - 1];
 				ath9k_hw_fill_vpd_table(minPwrT4[i], maxPwrT4[i],
-						data_9287[idxL].pwrPdg[i],
-						data_9287[idxL].vpdPdg[i],
-						intercepts,
-						vpdTableI[i]);
+							data_9287[idxL].pwrPdg[i],
+							data_9287[idxL].vpdPdg[i], intercepts,
+							vpdTableL[i]);
 			}
 		} else if (eeprom_4k) {
 			for (i = 0; i < numXpdGains; i++) {
 				minPwrT4[i] = data_4k[idxL].pwrPdg[i][0];
 				maxPwrT4[i] = data_4k[idxL].pwrPdg[i][intercepts - 1];
 				ath9k_hw_fill_vpd_table(minPwrT4[i], maxPwrT4[i],
-						data_4k[idxL].pwrPdg[i],
-						data_4k[idxL].vpdPdg[i],
-						intercepts,
-						vpdTableI[i]);
+							data_4k[idxL].pwrPdg[i],
+							data_4k[idxL].vpdPdg[i], intercepts,
+							vpdTableL[i]);
 			}
 		} else {
 			for (i = 0; i < numXpdGains; i++) {
 				minPwrT4[i] = data_def[idxL].pwrPdg[i][0];
 				maxPwrT4[i] = data_def[idxL].pwrPdg[i][intercepts - 1];
 				ath9k_hw_fill_vpd_table(minPwrT4[i], maxPwrT4[i],
-						data_def[idxL].pwrPdg[i],
-						data_def[idxL].vpdPdg[i],
-						intercepts,
-						vpdTableI[i]);
+							data_def[idxL].pwrPdg[i],
+							data_def[idxL].vpdPdg[i], intercepts,
+							vpdTableL[i]);
 			}
 		}
 	} else {
@@ -567,19 +575,37 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 						intercepts,
 						vpdTableR[i]);
 
-			for (j = 0; j <= (maxPwrT4[i] - minPwrT4[i]) / 2; j++) {
-				vpdTableI[i][j] =
-					(u8)(ath9k_hw_interpolate((u16)
-					     FREQ2FBIN(centers.
-						       synth_center,
-						       IS_CHAN_2GHZ
-						       (chan)),
-					     bChans[idxL], bChans[idxR],
-					     vpdTableL[i][j], vpdTableR[i][j]));
+			vpdRange = (maxPwrT4[i] >= minPwrT4[i]) ? (maxPwrT4[i] - minPwrT4[i]) / 2 :
+								  0;
+			vpdFillMax = min_t(u16, vpdRange, AR5416_MAX_PWR_RANGE_IN_HALF_DB - 1);
+			/*
+			 * vpdTableL doubles as the interpolated output in-place.
+			 * ath9k_hw_interpolate() receives vpdTableL[i][j] by
+			 * value (left-pier data) before the return overwrites
+			 * vpdTableL[i][j], and each column j is read only once
+			 * per iteration - safe as long as vpdTableR remains a
+			 * separate buffer.
+			 */
+			for (j = 0; j <= vpdFillMax; j++) {
+				vpdTableL[i][j] = (u8)(ath9k_hw_interpolate(
+					(u16)FREQ2FBIN(centers.synth_center, IS_CHAN_2GHZ(chan)),
+					bChans[idxL], bChans[idxR], vpdTableL[i][j],
+					vpdTableR[i][j]));
 			}
 		}
 	}
 
+	/*
+	 * Compute safe VPD table index limit for each gain row.
+	 * This mirrors the vpdFillMax computation in the else-branch
+	 * interpolation above - both clamp the same derived value.
+	 */
+	for (i = 0; i < numXpdGains; i++) {
+		vpdFillMaxArr[i] = min_t(
+			u16, (maxPwrT4[i] >= minPwrT4[i]) ? (maxPwrT4[i] - minPwrT4[i]) / 2 : 0,
+			AR5416_MAX_PWR_RANGE_IN_HALF_DB - 1);
+	}
+
 	k = 0;
 
 	for (i = 0; i < numXpdGains; i++) {
@@ -605,33 +631,39 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 					(minPwrT4[i] / 2)) -
 				       tPdGainOverlap + 1 + minDelta);
 		}
-		vpdStep = (int16_t)(vpdTableI[i][1] - vpdTableI[i][0]);
+		sizeCurrVpdTable = vpdFillMaxArr[i] + 1;
+
+		if (sizeCurrVpdTable >= 2)
+			vpdStep = (int16_t)(vpdTableL[i][1] - vpdTableL[i][0]);
+		else
+			vpdStep = 1; /* no entries to diff; avoids zero-step extrapolation */
 		vpdStep = (int16_t)((vpdStep < 1) ? 1 : vpdStep);
 
 		while ((ss < 0) && (k < (AR5416_NUM_PDADC_VALUES - 1))) {
-			tmpVal = (int16_t)(vpdTableI[i][0] + ss * vpdStep);
+			tmpVal = (int16_t)(vpdTableL[i][0] + ss * vpdStep);
 			pPDADCValues[k++] = (u8)((tmpVal < 0) ? 0 : tmpVal);
 			ss++;
 		}
-
-		sizeCurrVpdTable = (u8) ((maxPwrT4[i] - minPwrT4[i]) / 2 + 1);
 		tgtIndex = (u8)(pPdGainBoundaries[i] + tPdGainOverlap -
 				(minPwrT4[i] / 2));
 		maxIndex = (tgtIndex < sizeCurrVpdTable) ?
 			tgtIndex : sizeCurrVpdTable;
 
 		while ((ss < maxIndex) && (k < (AR5416_NUM_PDADC_VALUES - 1))) {
-			pPDADCValues[k++] = vpdTableI[i][ss++];
+			pPDADCValues[k++] = vpdTableL[i][ss++];
 		}
 
-		vpdStep = (int16_t)(vpdTableI[i][sizeCurrVpdTable - 1] -
-				    vpdTableI[i][sizeCurrVpdTable - 2]);
+		if (sizeCurrVpdTable >= 2)
+			vpdStep = (int16_t)(vpdTableL[i][sizeCurrVpdTable - 1] -
+					    vpdTableL[i][sizeCurrVpdTable - 2]);
+		else
+			vpdStep = 1; /* no entries to diff; avoids zero-step extrapolation */
 		vpdStep = (int16_t)((vpdStep < 1) ? 1 : vpdStep);
 
 		if (tgtIndex >= maxIndex) {
 			while ((ss <= tgtIndex) &&
 			       (k < (AR5416_NUM_PDADC_VALUES - 1))) {
-				tmpVal = (int16_t)((vpdTableI[i][sizeCurrVpdTable - 1] +
+				tmpVal = (int16_t)((vpdTableL[i][sizeCurrVpdTable - 1] +
 						    (ss - maxIndex + 1) * vpdStep));
 				pPDADCValues[k++] = (u8)((tmpVal > 255) ?
 							 255 : tmpVal);
@@ -642,14 +674,21 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
 
 	if (eeprom_4k)
 		pdgain_boundary_default = 58;
-	else
+	else if (i > 0)
 		pdgain_boundary_default = pPdGainBoundaries[i - 1];
+	else
+		pdgain_boundary_default = MAX_RATE_POWER;
 
 	while (i < AR5416_PD_GAINS_IN_MASK) {
 		pPdGainBoundaries[i] = pdgain_boundary_default;
 		i++;
 	}
 
+	/* Ensure the table contains at least one valid element. */
+	if (k == 0) {
+		WARN_ONCE(1, "ath9k: no PDADC values produced for gain boundaries\n");
+		pPDADCValues[k++] = 0;
+	}
 	while (k < AR5416_NUM_PDADC_VALUES) {
 		pPDADCValues[k] = pPDADCValues[k - 1];
 		k++;
diff --git a/drivers/net/wireless/ath/ath9k/eeprom_4k.c b/drivers/net/wireless/ath/ath9k/eeprom_4k.c
index 3e16cfe059f3..eec7efdc21c3 100644
--- a/drivers/net/wireless/ath/ath9k/eeprom_4k.c
+++ b/drivers/net/wireless/ath/ath9k/eeprom_4k.c
@@ -288,7 +288,7 @@ static void ath9k_hw_set_4k_power_cal_table(struct ath_hw *ah,
 	struct cal_data_per_freq_4k *pRawDataset;
 	u8 *pCalBChans = NULL;
 	u16 pdGainOverlap_t2;
-	static u8 pdadcValues[AR5416_NUM_PDADC_VALUES];
+	u8 pdadcValues[AR5416_NUM_PDADC_VALUES];
 	u16 gainBoundaries[AR5416_PD_GAINS_IN_MASK];
 	u16 numPiers, i, j;
 	u16 numXpdGain, xpdMask;
diff --git a/drivers/net/wireless/ath/ath9k/eeprom_9287.c b/drivers/net/wireless/ath/ath9k/eeprom_9287.c
index c139ac49ccf6..951943d71339 100644
--- a/drivers/net/wireless/ath/ath9k/eeprom_9287.c
+++ b/drivers/net/wireless/ath/ath9k/eeprom_9287.c
@@ -366,6 +366,15 @@ static void ath9k_hw_set_ar9287_power_cal_table(struct ath_hw *ah,
 	int16_t diff = 0;
 	struct ar9287_eeprom *pEepData = &ah->eeprom.map9287;
 
+	/*
+	 * pdadcValues must be zeroed here: under EEP_OL_PWRCTRL the
+	 * ath9k_hw_get_gain_boundaries_pdadcs() init path is skipped and
+	 * only the unconditional diff-offset shift below runs, which would
+	 * otherwise operate on indeterminate stack data.  eeprom_def.c
+	 * does not need this because both branches of its OLC/!OLC fork
+	 * fully populate the array.
+	 */
+	memset(pdadcValues, 0, sizeof(pdadcValues));
 	xpdMask = pEepData->modalHeader.xpdGain;
 
 	if (ath9k_hw_ar9287_get_eeprom_rev(ah) >= AR9287_EEP_MINOR_VER_2)
@@ -463,13 +472,23 @@ static void ath9k_hw_set_ar9287_power_cal_table(struct ath_hw *ah,
 					     (int32_t)AR9287_PWR_TABLE_OFFSET_DB);
 				diff *= 2;
 
-				for (j = 0; j < ((u16)AR5416_NUM_PDADC_VALUES-diff); j++)
-					pdadcValues[j] = pdadcValues[j+diff];
-
-				for (j = (u16)(AR5416_NUM_PDADC_VALUES-diff);
-				     j < AR5416_NUM_PDADC_VALUES; j++)
-					pdadcValues[j] =
-					  pdadcValues[AR5416_NUM_PDADC_VALUES-diff];
+				/* diff is safe: the bounds check above ensures
+				 * it is in [0, AR5416_NUM_PDADC_VALUES), so the
+				 * subtraction AR5416_NUM_PDADC_VALUES - diff
+				 * cannot underflow.
+				 */
+				if (diff >= 0 && diff < AR5416_NUM_PDADC_VALUES) {
+					for (j = 0; j < ((u16)AR5416_NUM_PDADC_VALUES - diff); j++)
+						pdadcValues[j] = pdadcValues[j + diff];
+
+					for (j = (u16)(AR5416_NUM_PDADC_VALUES - diff);
+					     j < AR5416_NUM_PDADC_VALUES; j++)
+						pdadcValues[j] =
+							pdadcValues[AR5416_NUM_PDADC_VALUES - diff - 1];
+				} else {
+					ath_warn(ath9k_hw_common(ah),
+						 "ignoring invalid PDADC offset %d\n", diff);
+				}
 			}
 
 			if (!ath9k_hw_ar9287_get_eeprom(ah, EEP_OL_PWRCTRL)) {
diff --git a/drivers/net/wireless/ath/ath9k/eeprom_def.c b/drivers/net/wireless/ath/ath9k/eeprom_def.c
index 5ba467cb7425..07ce4c8baa85 100644
--- a/drivers/net/wireless/ath/ath9k/eeprom_def.c
+++ b/drivers/net/wireless/ath/ath9k/eeprom_def.c
@@ -744,14 +744,20 @@ static void ath9k_adjust_pdadc_values(struct ath_hw *ah,
 	 */
 	if (AR_SREV_9280_20_OR_LATER(ah)) {
 		if (AR5416_PWR_TABLE_OFFSET_DB != pwr_table_offset) {
+			if (diff < 0 || diff >= AR5416_NUM_PDADC_VALUES) {
+				ath_warn(ath9k_hw_common(ah), "ignoring invalid PDADC offset %d\n",
+					 diff);
+				return;
+			}
+
 			/* shift the table to start at the new offset */
-			for (k = 0; k < (u16)NUM_PDADC(diff); k++ ) {
+			for (k = 0; k < (u16)NUM_PDADC(diff); k++) {
 				pdadcValues[k] = pdadcValues[k + diff];
 			}
 
 			/* fill the back of the table */
 			for (k = (u16)NUM_PDADC(diff); k < NUM_PDADC(0); k++) {
-				pdadcValues[k] = pdadcValues[NUM_PDADC(diff)];
+				pdadcValues[k] = pdadcValues[NUM_PDADC(diff) - 1];
 			}
 		}
 	}
@@ -769,7 +775,7 @@ static void ath9k_hw_set_def_power_cal_table(struct ath_hw *ah,
 	struct cal_data_per_freq *pRawDataset;
 	u8 *pCalBChans = NULL;
 	u16 pdGainOverlap_t2;
-	static u8 pdadcValues[AR5416_NUM_PDADC_VALUES];
+	u8 pdadcValues[AR5416_NUM_PDADC_VALUES];
 	u16 gainBoundaries[AR5416_PD_GAINS_IN_MASK];
 	u16 numPiers, i, j;
 	int16_t diff = 0;
-- 
2.54.0


^ permalink raw reply related

* [PATCH] wifi: mt76: mt7925: cancel mlo_pm_work on stop
From: Devin Wittmayer @ 2026-06-27 20:29 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, linux-wireless
  Cc: Shayne Chen, Sean Wang, Ming Yen Hsieh, linux-kernel

mt7925 queues mlo_pm_work with a 5 second delay during multi-link
power-save setup and never cancels it on the stop path. If the device is
torn down inside that window, the work outlives the teardown and its timer
fires afterwards, trying to queue onto the workqueue that is already gone:

  workqueue: cannot queue mt7925_mlo_pm_work [mt7925_common] on wq phy0
  WARNING: kernel/workqueue.c:2283 at __queue_work+0x59/0xa0, CPU#1: swapper/1/0
   call_timer_fn+0x2a/0x140
   __run_timers+0x203/0x330
   run_timer_softirq+0x86/0xf0

mt7921 already has its own stop callback, so add one for mt7925 that
cancels the work before calling mt792x_stop(). mt7925_ops backs both the
PCIe and USB drivers, so this covers both.

Fixes: 276a56883257 ("wifi: mt76: mt7925: update the power-saving flow")
Cc: stable@vger.kernel.org
Tested-by: Traockl <281473483+Traockl@users.noreply.github.com>
Signed-off-by: Devin Wittmayer <lucid_duck@justthetip.ca>
---
 drivers/net/wireless/mediatek/mt76/mt7925/main.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/main.c b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
index a9059866b701..9156530272c8 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/main.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/main.c
@@ -2481,10 +2481,19 @@ static void mt7925_channel_switch_rx_beacon(struct ieee80211_hw *hw,
 	}
 }
 
+static void mt7925_stop(struct ieee80211_hw *hw, bool suspend)
+{
+	struct mt792x_dev *dev = mt792x_hw_dev(hw);
+
+	cancel_delayed_work_sync(&dev->mlo_pm_work);
+
+	mt792x_stop(hw, suspend);
+}
+
 const struct ieee80211_ops mt7925_ops = {
 	.tx = mt792x_tx,
 	.start = mt7925_start,
-	.stop = mt792x_stop,
+	.stop = mt7925_stop,
 	.add_interface = mt7925_add_interface,
 	.remove_interface = mt792x_remove_interface,
 	.config = mt7925_config,
-- 
2.54.0


^ permalink raw reply related

* [PATCH 3/3] wifi: mt76: mt7615: drop TXRX_NOTIFY on non-mmio buses
From: Devin Wittmayer @ 2026-06-27 19:13 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, linux-wireless
  Cc: Shayne Chen, Sean Wang, Ming Yen Hsieh, linux-kernel
In-Reply-To: <20260627191336.20223-1-lucid_duck@justthetip.ca>

PKT_TYPE_TXRX_NOTIFY is an mmio-only event, but mt7615_rx_check() and
mt7615_queue_rx_skb() dispatch it to mt7615_mac_tx_free() on every bus.
mt7615_mac_tx_free() cleans the DMA tx queues with
mt76_queue_tx_cleanup(), which calls queue_ops->tx_cleanup(). Only the
mmio queue ops implement that callback; on the mt7663 USB and SDIO
buses it is NULL, so a TXRX_NOTIFY there calls a NULL pointer in the RX
worker. Same defect as the mt7921 and mt7925 patches in this series.

Drop the event on non-mmio buses via mt76_is_mmio(), as in
commit 5683e1488aa9 ("wifi: mt76: connac: do not check WED status for
non-mmio devices").

Fixes: eb99cc95c3b6 ("mt76: mt7615: introduce mt7663u support")
Cc: stable@vger.kernel.org
Signed-off-by: Devin Wittmayer <lucid_duck@justthetip.ca>
---
 drivers/net/wireless/mediatek/mt76/mt7615/mac.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7615/mac.c b/drivers/net/wireless/mediatek/mt76/mt7615/mac.c
index ce0051468501..aad232c5a6fa 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7615/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7615/mac.c
@@ -1601,6 +1601,8 @@ bool mt7615_rx_check(struct mt76_dev *mdev, void *data, int len)
 
 	switch (type) {
 	case PKT_TYPE_TXRX_NOTIFY:
+		if (!mt76_is_mmio(mdev))
+			return false;
 		mt7615_mac_tx_free(dev, data, len);
 		return false;
 	case PKT_TYPE_TXS:
@@ -1634,6 +1636,10 @@ void mt7615_queue_rx_skb(struct mt76_dev *mdev, enum mt76_rxq_id q,
 		dev_kfree_skb(skb);
 		break;
 	case PKT_TYPE_TXRX_NOTIFY:
+		if (!mt76_is_mmio(mdev)) {
+			dev_kfree_skb(skb);
+			break;
+		}
 		mt7615_mac_tx_free(dev, skb->data, skb->len);
 		dev_kfree_skb(skb);
 		break;
-- 
2.54.0


^ permalink raw reply related

* [PATCH 2/3] wifi: mt76: mt7925: drop TXRX_NOTIFY on non-mmio buses
From: Devin Wittmayer @ 2026-06-27 19:13 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, linux-wireless
  Cc: Shayne Chen, Sean Wang, Ming Yen Hsieh, linux-kernel
In-Reply-To: <20260627191336.20223-1-lucid_duck@justthetip.ca>

PKT_TYPE_TXRX_NOTIFY is an mmio-only event, but mt7925_rx_check() and
mt7925_queue_rx_skb() dispatch it to mt7925_mac_tx_free() on every bus.
mt7925_mac_tx_free() cleans the DMA tx queues with
mt76_queue_tx_cleanup(), which calls queue_ops->tx_cleanup(). Only the
mmio queue ops implement that callback; on USB it is NULL, so a
TXRX_NOTIFY there calls a NULL pointer in the RX worker:

  BUG: kernel NULL pointer dereference, address: 0000000000000000
  RIP: 0010:0x0
  Call Trace:
   mt7925_mac_tx_free+0x58/0x350 [mt7925_common]
   mt7925_rx_check+0xe2/0x130 [mt7925_common]
   mt76u_rx_worker+0x1b9/0x620 [mt76_usb]

Drop the event on non-mmio buses via mt76_is_mmio(), as in
commit 5683e1488aa9 ("wifi: mt76: connac: do not check WED status for
non-mmio devices").

Fixes: c948b5da6bbe ("wifi: mt76: mt7925: add Mediatek Wi-Fi7 driver for mt7925 chips")
Cc: stable@vger.kernel.org
Signed-off-by: Devin Wittmayer <lucid_duck@justthetip.ca>
---
 drivers/net/wireless/mediatek/mt76/mt7925/mac.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7925/mac.c b/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
index 0641a7131d7c..2f9871792ea1 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7925/mac.c
@@ -1203,8 +1203,9 @@ bool mt7925_rx_check(struct mt76_dev *mdev, void *data, int len)
 
 	switch (type) {
 	case PKT_TYPE_TXRX_NOTIFY:
-		/* PKT_TYPE_TXRX_NOTIFY can be received only by mmio devices */
-		mt7925_mac_tx_free(dev, data, len); /* mmio */
+		if (!mt76_is_mmio(mdev))
+			return false;
+		mt7925_mac_tx_free(dev, data, len);
 		return false;
 	case PKT_TYPE_TXS:
 		for (rxd += 4; rxd + 12 <= end; rxd += 12)
@@ -1240,7 +1241,10 @@ void mt7925_queue_rx_skb(struct mt76_dev *mdev, enum mt76_rxq_id q,
 
 	switch (type) {
 	case PKT_TYPE_TXRX_NOTIFY:
-		/* PKT_TYPE_TXRX_NOTIFY can be received only by mmio devices */
+		if (!mt76_is_mmio(mdev)) {
+			napi_consume_skb(skb, 1);
+			break;
+		}
 		mt7925_mac_tx_free(dev, skb->data, skb->len);
 		napi_consume_skb(skb, 1);
 		break;
-- 
2.54.0


^ permalink raw reply related

* [PATCH 1/3] wifi: mt76: mt7921: drop TXRX_NOTIFY on non-mmio buses
From: Devin Wittmayer @ 2026-06-27 19:13 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, linux-wireless
  Cc: Shayne Chen, Sean Wang, Ming Yen Hsieh, linux-kernel
In-Reply-To: <20260627191336.20223-1-lucid_duck@justthetip.ca>

PKT_TYPE_TXRX_NOTIFY is an mmio-only event, but mt7921_rx_check() and
mt7921_queue_rx_skb() dispatch it to mt7921_mac_tx_free() on every bus.
mt7921_mac_tx_free() cleans the DMA tx queues with
mt76_queue_tx_cleanup(), which calls queue_ops->tx_cleanup(). Only the
mmio queue ops implement that callback; on USB and SDIO it is NULL, so
a TXRX_NOTIFY there calls a NULL pointer in the RX worker:

  BUG: kernel NULL pointer dereference, address: 0000000000000000
  RIP: 0010:0x0
  Call Trace:
   mt7921_mac_tx_free+0x64/0x310 [mt7921_common]
   mt7921_rx_check+0x5f/0xf0 [mt7921_common]
   mt76u_rx_worker+0x1b9/0x620 [mt76_usb]

Drop the event on non-mmio buses via mt76_is_mmio(), as in
commit 5683e1488aa9 ("wifi: mt76: connac: do not check WED status for
non-mmio devices").

Fixes: 48fab5bbef40 ("mt76: mt7921: introduce mt7921s support")
Cc: stable@vger.kernel.org
Signed-off-by: Devin Wittmayer <lucid_duck@justthetip.ca>
---
 drivers/net/wireless/mediatek/mt76/mt7921/mac.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7921/mac.c b/drivers/net/wireless/mediatek/mt76/mt7921/mac.c
index 1c2377d0a53d..f7d54472da1b 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7921/mac.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7921/mac.c
@@ -570,8 +570,9 @@ bool mt7921_rx_check(struct mt76_dev *mdev, void *data, int len)
 
 	switch (type) {
 	case PKT_TYPE_TXRX_NOTIFY:
-		/* PKT_TYPE_TXRX_NOTIFY can be received only by mmio devices */
-		mt7921_mac_tx_free(dev, data, len); /* mmio */
+		if (!mt76_is_mmio(mdev))
+			return false;
+		mt7921_mac_tx_free(dev, data, len);
 		return false;
 	case PKT_TYPE_TXS:
 		for (rxd += 2; rxd + 8 <= end; rxd += 8)
@@ -600,7 +601,10 @@ void mt7921_queue_rx_skb(struct mt76_dev *mdev, enum mt76_rxq_id q,
 
 	switch (type) {
 	case PKT_TYPE_TXRX_NOTIFY:
-		/* PKT_TYPE_TXRX_NOTIFY can be received only by mmio devices */
+		if (!mt76_is_mmio(mdev)) {
+			napi_consume_skb(skb, 1);
+			break;
+		}
 		mt7921_mac_tx_free(dev, skb->data, skb->len);
 		napi_consume_skb(skb, 1);
 		break;
-- 
2.54.0


^ permalink raw reply related

* [PATCH 0/3] wifi: mt76: drop TXRX_NOTIFY on non-mmio buses
From: Devin Wittmayer @ 2026-06-27 19:13 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, linux-wireless
  Cc: Shayne Chen, Sean Wang, Ming Yen Hsieh, linux-kernel

PKT_TYPE_TXRX_NOTIFY is an mmio-only event, but the mt7921, mt7925 and
mt7615 RX classifiers dispatch it to *_mac_tx_free() on every bus. That
cleans the DMA tx queues via queue_ops->tx_cleanup(), a callback only
the mmio queue ops implement, so on USB or SDIO the event calls a NULL
pointer in the RX worker.

Reproduced on mt7921u (Alfa AWUS036AXML) and mt7925u (NETGEAR A9000):
the unpatched driver oopses, the patched driver drops the event. mt7615
is the same dispatch, fixed by inspection.

Each patch guards the path with mt76_is_mmio(), as in
commit 5683e1488aa9 ("wifi: mt76: connac: do not check WED status for
non-mmio devices").

Devin Wittmayer (3):
  wifi: mt76: mt7921: drop TXRX_NOTIFY on non-mmio buses
  wifi: mt76: mt7925: drop TXRX_NOTIFY on non-mmio buses
  wifi: mt76: mt7615: drop TXRX_NOTIFY on non-mmio buses

 drivers/net/wireless/mediatek/mt76/mt7615/mac.c |  6 ++++++
 drivers/net/wireless/mediatek/mt76/mt7921/mac.c | 10 +++++++---
 drivers/net/wireless/mediatek/mt76/mt7925/mac.c | 10 +++++++---
 3 files changed, 20 insertions(+), 6 deletions(-)

-- 
2.54.0


^ permalink raw reply

* Re: [PATCH wireless] wifi: mac80211_hwsim: avoid division by zero in mac80211_hwsim_write_tsf()
From: Serhat Kumral @ 2026-06-26  7:37 UTC (permalink / raw)
  To: ghwns6743
  Cc: benjamin.berg, johannes, linux-kernel, linux-wireless,
	miriam.rachel.korenblit, serhatkumral1,
	syzbot+21629c14aa749636db9d
In-Reply-To: <20260627134827.12531-1-ghwns6743@gmail.com>

Thanks a lot for the independent confirmation and for tracking down the
exact mechanism -- the OOB read landing inside struct mac80211_hwsim_data
(via sband->bitrates = data->rates) explains precisely why KASAN stays
quiet. Appreciate you sharing the reproducer too.

On hardening ieee80211_get_tx_rate() centrally: I'd defer to Johannes on
the preferred direction, since it touches callers across several drivers
(ath5k, adm8211, and likely others) that currently dereference the
return value without a NULL check -- any change there needs an audit of
all of them, which felt out of scope for this fix. If it's considered
worth doing, I'd be happy to help with that audit.

^ permalink raw reply

* Re: [PATCH] wifi: mac80211_hwsim: avoid division by zero in mac80211_hwsim_write_tsf()
From: Hojun Choi @ 2026-06-27 13:48 UTC (permalink / raw)
  To: serhatkumral1, johannes, linux-wireless
  Cc: benjamin.berg, miriam.rachel.korenblit, linux-kernel,
	syzbot+21629c14aa749636db9d, Hojun Choi
In-Reply-To: <20260625215606.11049-1-serhatkumral1@gmail.com>

Tested-by: Hojun Choi <ghwns6743@gmail.com>

Thanks for the fix. I confirmed it with a standalone reproducer that
injects a beacon on a monitor interface with a radiotap MCS index past
the band's n_bitrates. The bitrates[] read then goes past the table but
stays inside struct mac80211_hwsim_data (so KASAN is quiet), lands on a
0 .bitrate, and write_tsf() ends up doing 1920 / 0.

Without the patch:

    Oops: divide error: 0000 [#1] SMP KASAN PTI
    RIP: 0010:mac80211_hwsim_write_tsf+0x476/0x5e0
    Call Trace:
     mac80211_hwsim_tx_frame_no_nl
     mac80211_hwsim_tx
     [...]
     ieee80211_monitor_start_xmit

With the patch applied, the same injection no longer crashes (tested on
mainline, 502d801f0ab0).

The commit message explains why ieee80211_get_tx_rate() itself isn't
changed (ath5k/adm8211 and others dereference it without a NULL check).
Is hardening those callers, so the index could be bounds-checked
centrally, worth doing as a follow-up -- or is the per-driver guard the
preferred direction? Happy to help with the legwork if it's useful.

Setup and build:

    iw phy phy0 interface add mon0 type monitor
    ip link set mon0 up
    iw dev mon0 set channel 6
    cc -o repro repro.c && ./repro mon0

(mon0 must be on a live channel, or the frame is dropped before xmit.)

The syzbot dashboard has no reproducer for this bug, so here is a
standalone one:

/*
 * repro.c -- divide error in mac80211_hwsim_write_tsf
 *
 * write_tsf does 1920 / txrate->bitrate for a beacon. The bitrate comes from
 * ieee80211_get_tx_rate(), which indexes bands[band]->bitrates[idx] checking
 * only idx >= 0 -- never idx < n_bitrates. A radiotap MCS index past the table
 * reads an adjacent slot in the same heap object (so KASAN is silent); when
 * that slot reads 0, the divide faults. We sweep 12..127 (8..127 on 5/6GHz).
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <endian.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netpacket/packet.h>
#include <linux/if_ether.h>

enum
{
	Rtmcs	= 0x00080000,
	Havemcs	= 0x02,
	Beacon	= 0x80,
	Burst	= 32,
};

typedef struct Frame Frame;
struct Frame
{
	unsigned char	rtversion;
	unsigned char	rtpad;
	unsigned short	rtlen;
	unsigned int	rtpresent;
	unsigned char	mcsknown;
	unsigned char	mcsflags;
	unsigned char	mcsindex;
	unsigned char	rtpadend;

	unsigned char	fc;
	unsigned char	flags;
	unsigned short	duration;
	unsigned char	da[6];
	unsigned char	sa[6];
	unsigned char	bssid[6];
	unsigned short	seq;

	/* beacon body -- tsf's end (32B) clears the write_tsf length gate */
	unsigned long long	tsf;
	unsigned short	interval;
	unsigned short	capab;
} __attribute__((packed));

void
die(char *s)
{
	perror(s);
	exit(1);
}

void
fill(Frame *f)
{
	memset(f, 0, sizeof *f);
	f->rtlen = htole16(12);
	f->rtpresent = htole32(Rtmcs);
	f->mcsknown = Havemcs;
	f->fc = Beacon;
	memset(f->da, 0xff, 6);
	memset(f->sa, 0x02, 6);
	memset(f->bssid, 0x02, 6);
	f->tsf = htole64(0x05e6b0);
	f->interval = htole16(100);
	f->capab = htole16(0x0021);
}

int
main(int argc, char **argv)
{
	char *iface, *colon;
	int fd, kmsg, lo, hi, idx, i;
	struct ifreq ifr;
	struct sockaddr_ll to;
	Frame f;

	iface = argc > 1 ? argv[1] : "mon0";
	lo = 12;
	hi = 127;
	if(argc > 2){
		colon = strchr(argv[2], ':');
		lo = atoi(argv[2]);
		hi = colon ? atoi(colon + 1) : lo;
	}

	fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	if(fd < 0)
		die("socket");

	memset(&ifr, 0, sizeof ifr);
	strncpy(ifr.ifr_name, iface, IFNAMSIZ - 1);
	if(ioctl(fd, SIOCGIFINDEX, &ifr) < 0)
		die(iface);

	memset(&to, 0, sizeof to);
	to.sll_family = AF_PACKET;
	to.sll_ifindex = ifr.ifr_ifindex;
	to.sll_protocol = htons(ETH_P_ALL);

	kmsg = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);

	fill(&f);

	/* the injection: send a beacon at each MCS index, sweeping past n_bitrates */
	for(idx = lo; idx <= hi; idx++){
		f.mcsindex = idx;
		if(kmsg >= 0)
			dprintf(kmsg, "repro: mcs=%d\n", idx);

		for(i = 0; i < Burst; i++)
			if(sendto(fd, &f, sizeof f, 0, (struct sockaddr*)&to, sizeof to) < 0)
				die("sendto");
	}
	return 0;
}

^ permalink raw reply

* [PATCH] wifi: brcmfmac: cyw: fix heap overflow on a short auth frame
From: Maoyi Xie @ 2026-06-27 13:13 UTC (permalink / raw)
  To: Arend van Spriel
  Cc: Maoyi Xie, linux-wireless, brcm80211, brcm80211-dev-list.pdl,
	linux-kernel, Kaixuan Li, stable

brcmf_notify_auth_frame_rx() takes the frame length from the firmware
event and copies the frame body with the management header offset
subtracted:

	u32 mgmt_frame_len = e->datalen - sizeof(struct brcmf_rx_mgmt_data);
	...
	memcpy(&mgmt_frame->u, frame,
	       mgmt_frame_len - offsetof(struct ieee80211_mgmt, u));

The only length check is e->datalen >= sizeof(*rxframe), so mgmt_frame_len
can be anything from 0 up. offsetof(struct ieee80211_mgmt, u) is 24. When
mgmt_frame_len is below that, the subtraction wraps as an unsigned value to
a huge length. The memcpy then runs far past the kzalloc'd buffer. A
malicious or malfunctioning AP can make the frame short during the
external SAE auth exchange, so this is a remotely triggered heap overflow.

Reject frames shorter than the management header offset before the copy.

Fixes: 66f909308a7c ("wifi: brcmfmac: cyw: support external SAE authentication in station mode")
Link: https://lore.kernel.org/r/178214417708.2368577.16740907093694208834@maoyixie.com
Cc: stable@vger.kernel.org
Co-developed-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Maoyi Xie <maoyixie.tju@gmail.com>
---
 drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c
index ce09d44fa73cf..873754be5174b 100644
--- a/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c
+++ b/drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c
@@ -293,6 +293,12 @@ brcmf_notify_auth_frame_rx(struct brcmf_if *ifp,
 		return -EINVAL;
 	}

+	if (mgmt_frame_len < offsetof(struct ieee80211_mgmt, u)) {
+		bphy_err(drvr, "Event %s (%d) frame too small. Ignore\n",
+			 brcmf_fweh_event_name(e->event_code), e->event_code);
+		return -EINVAL;
+	}
+
 	wdev = &ifp->vif->wdev;
 	WARN_ON(!wdev);


^ permalink raw reply related

* Re: brcmfmac: heap overflow in brcmf_notify_auth_frame_rx() on a short auth frame
From: Arend van Spriel @ 2026-06-27 12:09 UTC (permalink / raw)
  To: Maoyi Xie; +Cc: linux-wireless, brcm80211, linux-kernel
In-Reply-To: <178214417708.2368577.16740907093694208834@maoyixie.com>

On 22/06/2026 18:02, Maoyi Xie wrote:
> Hi all,
> 
> I think brcmf_notify_auth_frame_rx() in
> drivers/net/wireless/broadcom/brcm80211/brcmfmac/cyw/core.c can overflow the
> heap when the firmware reports a short external auth frame. I would
> appreciate it if you could take a look.
> 
> The handler takes the frame length from the event, then allocates a buffer
> for it.
> 
> 	u32 mgmt_frame_len = e->datalen - sizeof(struct brcmf_rx_mgmt_data);
> 	...
> 	if (e->datalen < sizeof(*rxframe)) {
> 		...
> 		return -EINVAL;
> 	}
> 	...
> 	mgmt_frame = kzalloc(mgmt_frame_len, GFP_KERNEL);
> 
> The only length check is e->datalen >= sizeof(*rxframe). So mgmt_frame_len
> can be anything from 0 up. The frame body is then copied with a length that
> subtracts the management header offset.
> 
> 	memcpy(&mgmt_frame->u, frame,
> 	       mgmt_frame_len - offsetof(struct ieee80211_mgmt, u));
> 
> offsetof(struct ieee80211_mgmt, u) is 24. If mgmt_frame_len is less than 24,
> the subtraction wraps around as an unsigned value to a huge number. The
> memcpy then runs far past the small kzalloc buffer. That is a heap overflow
> driven by the frame the firmware passes up. A malicious or malfunctioning AP
> can make the frame short during the external SAE auth exchange.
> 
> The p2p path in the same driver allocates with the header offset included,
> so it does not have this shape.
> 
> I reproduced the overflow on 7.1-rc7. With mgmt_frame_len set below the 24
> byte header offset, the subtracted length wraps to a huge value and the copy
> faults.
> 
>    BUG: unable to handle page fault ... in memcpy_orig
> 
> A check that mgmt_frame_len is at least offsetof(struct ieee80211_mgmt, u)
> before the copy would close it.
> 
> Does this look like a real bug to you, and is that the right place to bound
> it? If so I am happy to send a proper patch with a Fixes tag and Cc stable.
> 
> Kaixuan Li and I found this together.
Thanks for reaching out although it would have been fine to just send 
the patch straight away. I do agree with the assessment given so this is 
a real bug.

Regards,
Arend

^ permalink raw reply

* Re: [PATCH] wifi: cfg80211: Fix an error handling path in cfg80211_wext_siwscan()
From: Markus Elfring @ 2026-06-27 11:45 UTC (permalink / raw)
  To: Xiao Wu, linux-wireless, Christophe Jaillet, Johannes Berg,
	John W. Linville
  Cc: kernel-janitors, LKML
In-Reply-To: <tencent_D39C654EBCC3C95DC8C48841701D8D5D2808@qq.com>

…
> The PoC uses pthread barrier synchronization to force both paths through
> the lockless check simultaneously.  It runs on wlan0 created by
> mac80211_hwsim (modprobe mac80211_hwsim radios=2).
> 
> Full PoC (compile with:  gcc -Wall -o poc poc.c -lpthread):

Thanks for your test approach.


…
>      pthread_barrier_init(&barrier_go, NULL, 2);
>      pthread_create(&nt, NULL, nl_thread, NULL);
…

Can there be a need to improve error detection and corresponding exception handling?
https://cmu-sei.github.io/secure-coding-standards/sei-cert-c-coding-standard/rules/posix-pos/pos54-c

Regards,
Markus

^ permalink raw reply

* Re: [mt76] MT6639 (device 0x7927) lacks mainline support in kernel 7.0 — out-of-tree patches required
From: Nick @ 2026-06-27 10:41 UTC (permalink / raw)
  To: Radosław Poprawski; +Cc: linux-wireless, Lorenzo Bianconi, Felix Fietkau
In-Reply-To: <CAMe9DRPx8KTmSVPmb44VVE+Ebu_8isVncWFfwEHvSk=DLh8i9A@mail.gmail.com>

On Sat, Jun 27, 2026 at 3:07 AM Radosław Poprawski
<rj.poprawski@gmail.com> wrote:
>
> I'm reporting a missing mainline kernel support issue for the MediaTek MT6639 chip (marketed as MT7927/Wi-Fi 7), which requires out-of-tree DKMS patches to function on Linux kernel 7.0.

Hello Radoslaw,

You might want to go to the following repo:

https://github.com/morrownr/mt76

We recently set this repo up to help with various situations.
Hopefully it helps you,

Nick

^ permalink raw reply

* [PATCH] wifi: mac80211: defer link RX stats percpu free to RCU
From: Maoyi Xie @ 2026-06-27  8:30 UTC (permalink / raw)
  To: Johannes Berg; +Cc: Maoyi Xie, linux-wireless, linux-kernel, Kaixuan Li

sta_remove_link() frees a removed MLO link's RX stats percpu buffer right
away, but defers only the link container to RCU:

	sta_info_free_link(&alloc->info);
	kfree_rcu(alloc, rcu_head);

The RX fast path reads link_sta under rcu_read_lock and writes the percpu
stats. A reader that resolved link_sta before the removal keeps the
pointer. The container stays alive from the kfree_rcu, so the read still
works. But the percpu block it points to is already freed. This needs
uses_rss. That is when pcpu_rx_stats exists.

The full STA teardown frees the deflink stats only after
synchronize_net(). The link removal path had no such barrier. The race is
hard to win in practice, but the free should still wait for RCU.

Free the link together with its data from a single RCU callback, so the
percpu block is reclaimed only after readers drain.

Fixes: c71420db653a ("wifi: mac80211: RCU-ify link STA pointers")
Link: https://lore.kernel.org/r/20260626080158.3589711-1-maoyixie.tju@gmail.com
Suggested-by: Johannes Berg <johannes@sipsolutions.net>
Co-developed-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Kaixuan Li <kaixuan.li@ntu.edu.sg>
Signed-off-by: Maoyi Xie <maoyixie.tju@gmail.com>
---
The earlier inquiry pointed at cb71f1d136a6, which added sta_remove_link().
The regression is c71420db653a though. It moved the link pointers to RCU
and deferred the container free. The percpu free stayed synchronous.

 net/mac80211/sta_info.c | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 4c31ef8817ce0..6d58571a364d1 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -355,6 +355,15 @@ static void sta_info_free_link(struct link_sta_info *link_sta)
 	free_percpu(link_sta->pcpu_rx_stats);
 }

+static void sta_link_free_rcu(struct rcu_head *head)
+{
+	struct sta_link_alloc *alloc =
+		container_of(head, struct sta_link_alloc, rcu_head);
+
+	sta_info_free_link(&alloc->info);
+	kfree(alloc);
+}
+
 static void sta_accumulate_removed_link_stats(struct sta_info *sta, int link_id)
 {
 	struct link_sta_info *link_sta = wiphy_dereference(sta->local->hw.wiphy,
@@ -439,10 +448,8 @@ static void sta_remove_link(struct sta_info *sta, unsigned int link_id,

 	RCU_INIT_POINTER(sta->link[link_id], NULL);
 	RCU_INIT_POINTER(sta->sta.link[link_id], NULL);
-	if (alloc) {
-		sta_info_free_link(&alloc->info);
-		kfree_rcu(alloc, rcu_head);
-	}
+	if (alloc)
+		call_rcu(&alloc->rcu_head, sta_link_free_rcu);

 	ieee80211_sta_recalc_aggregates(&sta->sta);
 }

^ permalink raw reply related

* [mt76] MT6639 (device 0x7927) lacks mainline support in kernel 7.0 — out-of-tree patches required
From: Radosław Poprawski @ 2026-06-27  8:07 UTC (permalink / raw)
  To: linux-wireless; +Cc: Lorenzo Bianconi, Felix Fietkau


[-- Attachment #1.1: Type: text/plain, Size: 5215 bytes --]

Hello,

I'm reporting a missing mainline kernel support issue for the MediaTek
MT6639 chip (marketed as MT7927/Wi-Fi 7), which requires out-of-tree DKMS
patches to function on Linux kernel 7.0.

== Hardware ==

Chip:      MediaTek MT6639 (PCI device 14c3:7927)
Interface: PCIe
Bluetooth: Foxconn/Hon Hai (USB, device 0489:e13a / 13d3:3588)
System:    Linux Mint 22.3, kernel 7.0.0-14-generic (x86_64)
Board:     Gigabyte Z790 AORUS PRO X

On boot, the kernel correctly identifies and forces chip recognition:
  mt7925e 0000:03:00.0: MT6639 raw CHIPID=0x0000, forcing chip=0x7927
  mt7925e 0000:03:00.0: HW/SW Version: 0x8a108a10, Build Time:
20250926200913a

However, the mt7925e module (OE) is loaded only via DKMS — it is not part
of the mainline kernel 7.0 build. All mt76-related modules show (OE) flag
in dmesg, confirming out-of-tree status.

== Problem ==

The mainline mt7925e driver in kernel 7.0 does not include support for
MT6639 (0x7927). Getting the chip to work requires:

1. Manually downloading firmware from linux-firmware (mt7927/ directory)
2. Applying out-of-tree patches from
https://github.com/jetm/mediatek-mt7927-dkms
3. Building and installing via DKMS

The firmware was extracted from the Windows driver (mt7927_win_driver.zip,
Windows driver build date: 20250926).

== Patches required ==

The following patches were necessary to make the chip functional. I'm
attaching their content inline.

--- 1. mt6639-bt-6.19.patch (Bluetooth support) ---

Adds MT6639 (0x6639) device ID support to btmtk.c and btusb.c:
- New firmware filename path: mediatek/mt6639/BT_RAM_CODE_MT6639_2_1_hdr.bin
- Section download filter: only sections with dlmode byte0 == 0x01 are
downloaded
  (matches Windows driver behavior, avoids chip hang)
- Firmware persistence optimization: skip re-download on subsequent BT
power cycles
  (saves ~2.6s delay on reconnect)
- New USB device IDs added to btusb.c:
    0489:e13a, 0489:e0fa, 0489:e116, 13d3:3588

--- 2. mt6639-wifi-dma.patch (WiFi / DMA / EHT caps) ---

Key changes:
- init.c: explicit DBDC (dual-band) enable for MT6639 — without this,
firmware
  defaults to 2.4GHz only and ignores 5GHz scan requests
- main.c: proper 6GHz / 320MHz EHT capabilities advertisement
- mac.c: 320MHz RX bandwidth reporting (IEEE80211_STA_RX_BW_320)
- mt792x_dma.c: MT6639-specific WFDMA DMA prefetch layout
- mt792x_core.c: wfsys reset and mac_enable use is_connac2() instead of
  is_mt7921() — MT6639 is connac3 but shares the reset path
- mt792x_core.c: MCU restart + readiness poll before firmware download

--- 3. mt6639-wifi-init.patch (PCI probe / ring allocation) ---

Key changes:
- Dynamic MCU RX ring size from layout struct (mcu_rxdone_ring_size)
- Conditional MCU WA ring allocation (has_mcu_wa flag)
- MT7902 IRQ map fix (wm2_complete_mask = 0), also applies to MT6639 probe
path
- mt76_for_each_q_rx() loop replaces hardcoded napi_disable() calls

--- 4. mt7902-wifi-6.19.patch (MT7902 support, related chip) ---

Adds MT7902 device IDs and firmware paths as a companion fix.

== Current status ==

With all patches applied via DKMS, the chip is functional:
- WiFi works on 2.4GHz, 5GHz, and 6GHz bands
- Bluetooth works (btmtk, btusb modules)about:blank#blocked
- No kernel panics or firmware errors in dmesg

== Request ==

Please consider upstreaming support for MT6639 (0x7927) into the mainline
mt76 driver.
The patches above (from github.com/jetm/mediatek-mt7927-dkms) could serve
as a starting point.

I'm happy to test patches or provide additional dmesg/firmware dumps if
helpful.

== Regression:works on 6.x, broken on 7.0 ==

The firmware extraction from the Windows driver (mt7927_win_driver.zip) was
performed on kernel 6.x,
where the chip worked correctly after loading the extracted firmware — no
additional patching was required.
The issue appeared when upgrading to kernel 7.0.0-14-generic, where the
mt7925e module failed to load / initialize the chip correctly.
The DKMS patches listed below were necessary to restore functionality.
This suggests a kernel API change between 6.x and 7.0 broke compatibility
with the existing mt76/mt7925e driver.

== Core issue ==

Kernel 7.0 ships with the mt7925e driver in mainline, which gives users a
false impression that MT6639 (0x7927) is supported.
In reality, the mainline 7.0 driver lacks: - MT6639 device ID recognition -
Correct firmware paths for mt7927/mt6639 - Required DMA prefetch layout for
MT6639 - DBDC initialization
(chip defaults to 2.4GHz only without it) - Bluetooth USB device IDs for
the companion BT chip As a result, users are forced to:
1. Extract firmware from the Windows driver themselves
2. Rebuild the driver from kernel 6.x sources
3. Apply multiple out-of-tree patches via DKMS A correct mainline
implementation for 7.0 would eliminate all of this.
The chip works — the patches prove it — it just needs to be properly
integrated.

System info:
  uname -r: 7.0.0-14-generic
  modinfo mt7925e filename:
/lib/modules/7.0.0-14-generic/updates/dkms/mt7925e.ko.zst
  Firmware build: 20250926200913

Thank you.

Radoslaw Poprawski

[-- Attachment #1.2: Type: text/html, Size: 5666 bytes --]

[-- Attachment #2: mt6639-wifi-dma.patch --]
[-- Type: text/x-patch, Size: 16846 bytes --]

diff --git a/mt7925/init.c b/mt7925/init.c
index 3ce5d6f..d501a82 100644
--- a/mt7925/init.c
+++ b/mt7925/init.c
@@ -115,6 +115,18 @@ static int __mt7925_init_hardware(struct mt792x_dev *dev)
 	if (ret)
 		goto out;
 
+	/* MT6639: Enable DBDC (dual-band) mode. Without this, firmware
+	 * defaults to 2.4GHz only and ignores 5GHz scan requests.
+	 * MT7925 firmware handles DBDC automatically. */
+	if (is_mt6639(&dev->mt76)) {
+		ret = mt7925_mcu_set_dbdc(&dev->mphy, true);
+		if (ret) {
+			dev_warn(dev->mt76.dev,
+				 "MT6639 DBDC enable failed: %d\n", ret);
+			ret = 0;
+		}
+	}
+
 out:
 	return ret;
 }
diff --git a/mt7925/main.c b/mt7925/main.c
index 2d358a9..4a930c1 100644
--- a/mt7925/main.c
+++ b/mt7925/main.c
@@ -183,6 +183,10 @@ mt7925_init_eht_caps(struct mt792x_phy *phy, enum nl80211_band band,
 		IEEE80211_EHT_PHY_CAP0_SU_BEAMFORMER |
 		IEEE80211_EHT_PHY_CAP0_SU_BEAMFORMEE;
 
+	if (band == NL80211_BAND_6GHZ)
+		eht_cap_elem->phy_cap_info[0] |=
+			IEEE80211_EHT_PHY_CAP0_320MHZ_IN_6GHZ;
+
 	eht_cap_elem->phy_cap_info[0] |=
 		u8_encode_bits(u8_get_bits(sts - 1, BIT(0)),
 			       IEEE80211_EHT_PHY_CAP0_BEAMFORMEE_SS_80MHZ_MASK);
@@ -193,10 +197,20 @@ mt7925_init_eht_caps(struct mt792x_phy *phy, enum nl80211_band band,
 		u8_encode_bits(sts - 1,
 			       IEEE80211_EHT_PHY_CAP1_BEAMFORMEE_SS_160MHZ_MASK);
 
+	if (band == NL80211_BAND_6GHZ)
+		eht_cap_elem->phy_cap_info[1] |=
+			u8_encode_bits(sts - 1,
+				       IEEE80211_EHT_PHY_CAP1_BEAMFORMEE_SS_320MHZ_MASK);
+
 	eht_cap_elem->phy_cap_info[2] =
 		u8_encode_bits(sts - 1, IEEE80211_EHT_PHY_CAP2_SOUNDING_DIM_80MHZ_MASK) |
 		u8_encode_bits(sts - 1, IEEE80211_EHT_PHY_CAP2_SOUNDING_DIM_160MHZ_MASK);
 
+	if (band == NL80211_BAND_6GHZ)
+		eht_cap_elem->phy_cap_info[2] |=
+			u8_encode_bits(sts - 1,
+				       IEEE80211_EHT_PHY_CAP2_SOUNDING_DIM_320MHZ_MASK);
+
 	eht_cap_elem->phy_cap_info[3] =
 		IEEE80211_EHT_PHY_CAP3_NG_16_SU_FEEDBACK |
 		IEEE80211_EHT_PHY_CAP3_NG_16_MU_FEEDBACK |
@@ -217,7 +231,8 @@ mt7925_init_eht_caps(struct mt792x_phy *phy, enum nl80211_band band,
 		u8_encode_bits(u8_get_bits(0x11, GENMASK(1, 0)),
 			       IEEE80211_EHT_PHY_CAP5_MAX_NUM_SUPP_EHT_LTF_MASK);
 
-	val = width == NL80211_CHAN_WIDTH_160 ? 0x7 :
+	val = width == NL80211_CHAN_WIDTH_320 ? 0xf :
+	      width == NL80211_CHAN_WIDTH_160 ? 0x7 :
 	      width == NL80211_CHAN_WIDTH_80 ? 0x3 : 0x1;
 	eht_cap_elem->phy_cap_info[6] =
 		u8_encode_bits(u8_get_bits(0x11, GENMASK(4, 2)),
@@ -230,6 +245,11 @@ mt7925_init_eht_caps(struct mt792x_phy *phy, enum nl80211_band band,
 		IEEE80211_EHT_PHY_CAP7_MU_BEAMFORMER_80MHZ |
 		IEEE80211_EHT_PHY_CAP7_MU_BEAMFORMER_160MHZ;
 
+	if (band == NL80211_BAND_6GHZ)
+		eht_cap_elem->phy_cap_info[7] |=
+			IEEE80211_EHT_PHY_CAP7_NON_OFDMA_UL_MU_MIMO_320MHZ |
+			IEEE80211_EHT_PHY_CAP7_MU_BEAMFORMER_320MHZ;
+
 	val = u8_encode_bits(nss, IEEE80211_EHT_MCS_NSS_RX) |
 	      u8_encode_bits(nss, IEEE80211_EHT_MCS_NSS_TX);
 
@@ -239,6 +259,12 @@ mt7925_init_eht_caps(struct mt792x_phy *phy, enum nl80211_band band,
 	eht_nss->bw._160.rx_tx_mcs9_max_nss = val;
 	eht_nss->bw._160.rx_tx_mcs11_max_nss = val;
 	eht_nss->bw._160.rx_tx_mcs13_max_nss = val;
+
+	if (band == NL80211_BAND_6GHZ) {
+		eht_nss->bw._320.rx_tx_mcs9_max_nss = val;
+		eht_nss->bw._320.rx_tx_mcs11_max_nss = val;
+		eht_nss->bw._320.rx_tx_mcs13_max_nss = val;
+	}
 }
 
 int mt7925_init_mlo_caps(struct mt792x_phy *phy)
diff --git a/mt7925/mac.c b/mt7925/mac.c
index 871b671..a2070ef 100644
--- a/mt7925/mac.c
+++ b/mt7925/mac.c
@@ -336,10 +336,15 @@ mt7925_mac_fill_rx_rate(struct mt792x_dev *dev,
 	case IEEE80211_STA_RX_BW_80:
 		status->bw = RATE_INFO_BW_80;
 		break;
 	case IEEE80211_STA_RX_BW_160:
 		status->bw = RATE_INFO_BW_160;
 		break;
+	/* RXV can report 320 in two positions */
+	case IEEE80211_STA_RX_BW_320:
+	case IEEE80211_STA_RX_BW_320 + 1:
+		status->bw = RATE_INFO_BW_320;
+		break;
 	default:
 		return -EINVAL;
 	}
 
@@ -993,6 +998,10 @@ mt7925_mac_add_txs_skb(struct mt792x_dev *dev, struct mt76_wcid *wcid,
 	switch (FIELD_GET(MT_TXS0_BW, txs)) {
+	case IEEE80211_STA_RX_BW_320:
+		rate.bw = RATE_INFO_BW_320;
+		stats->tx_bw[4]++;
+		break;
 	case IEEE80211_STA_RX_BW_160:
 		rate.bw = RATE_INFO_BW_160;
 		stats->tx_bw[3]++;
 		break;
 	case IEEE80211_STA_RX_BW_80:
diff --git a/mt7925/mcu.c b/mt7925/mcu.c
index cf0fdea..d0001e0 100644
--- a/mt7925/mcu.c
+++ b/mt7925/mcu.c
@@ -1683,6 +1683,7 @@ mt7925_mcu_sta_eht_tlv(struct sk_buff *skb, struct ieee80211_link_sta *link_sta)
 		memcpy(eht->mcs_map_bw20, &mcs_map->only_20mhz, sizeof(eht->mcs_map_bw20));
 	memcpy(eht->mcs_map_bw80, &mcs_map->bw._80, sizeof(eht->mcs_map_bw80));
 	memcpy(eht->mcs_map_bw160, &mcs_map->bw._160, sizeof(eht->mcs_map_bw160));
+	memcpy(eht->mcs_map_bw320, &mcs_map->bw._320, sizeof(eht->mcs_map_bw320));
 }
 
 static void
@@ -2331,6 +2332,9 @@ void mt7925_mcu_bss_rlm_tlv(struct sk_buff *skb, struct mt76_phy *phy,
 	case NL80211_CHAN_WIDTH_160:
 		req->bw = CMD_CBW_160MHZ;
 		break;
+	case NL80211_CHAN_WIDTH_320:
+		req->bw = CMD_CBW_320MHZ;
+		break;
 	case NL80211_CHAN_WIDTH_5:
 		req->bw = CMD_CBW_5MHZ;
 		break;
diff --git a/mt7925/mt7925.h b/mt7925/mt7925.h
index 6b9bf1b..d476b13 100644
--- a/mt7925/mt7925.h
+++ b/mt7925/mt7925.h
@@ -126,6 +126,13 @@ enum mt7925_rxq_id {
 	MT7925_RXQ_MCU_WM2, /* for tx done */
 };
 
+/* MT6639 uses different RX ring indices than MT7925 */
+enum mt6639_rxq_id {
+	MT6639_RXQ_BAND0 = 4,
+	MT6639_RXQ_MCU_WM = 6,
+	MT6639_RXQ_DATA2 = 7,
+};
+
 enum {
 	MODE_OPEN = 0,
 	MODE_SHARED = 1,
diff --git a/mt7925/pci.c b/mt7925/pci.c
index db053bd..68383ac 100644
--- a/mt7925/pci.c
+++ b/mt7925/pci.c
@@ -324,6 +324,126 @@ static void mt6639_cbtop_remap(struct mt792x_dev *dev)
 	mt76_rr(dev, MT_CBINFRA_MISC0_REMAP_WF);
 }
 
+static int mt6639_dma_init(struct mt792x_dev *dev)
+{
+	int ret;
+
+	mt76_dma_attach(&dev->mt76);
+
+	/* Do SET_OWN -> CLR_OWN now that CBTOP and CBInfra are ready.
+	 * CLR_OWN triggers the ROM to initialize WFDMA properly. */
+	ret = mt792xe_mcu_fw_pmctrl(dev);
+	if (ret)
+		return ret;
+
+	ret = __mt792xe_mcu_drv_pmctrl(dev);
+	if (ret)
+		return ret;
+
+	/* Clear pending interrupts from previous state */
+	mt76_wr(dev, MT_WFDMA0_HOST_INT_STA, ~0);
+
+	/* Disable DMA */
+	mt76_clear(dev, MT_WFDMA0_GLO_CFG,
+		   MT_WFDMA0_GLO_CFG_TX_DMA_EN |
+		   MT_WFDMA0_GLO_CFG_RX_DMA_EN |
+		   MT_WFDMA0_GLO_CFG_CSR_DISP_BASE_PTR_CHAIN_EN |
+		   MT_WFDMA0_GLO_CFG_OMIT_TX_INFO |
+		   MT_WFDMA0_GLO_CFG_OMIT_RX_INFO_PFET2);
+	wmb();
+
+	if (!mt76_poll_msec_tick(dev, MT_WFDMA0_GLO_CFG,
+				 MT_WFDMA0_GLO_CFG_TX_DMA_BUSY |
+				 MT_WFDMA0_GLO_CFG_RX_DMA_BUSY, 0, 100, 1))
+		return -ETIMEDOUT;
+
+	/* Reset DMA descriptor pointers */
+	mt76_wr(dev, MT_WFDMA0_RST_DTX_PTR, ~0);
+	mt76_wr(dev, MT_WFDMA0_RST_DRX_PTR, ~0);
+	wmb();
+	msleep(10);
+
+	/* init tx queue - ring 0 */
+	ret = mt76_connac_init_tx_queues(dev->phy.mt76, MT7925_TXQ_BAND0,
+					 MT7925_TX_RING_SIZE,
+					 MT_TX_RING_BASE, NULL, 0);
+	if (ret)
+		return ret;
+
+	mt76_wr(dev, MT_WFDMA0_TX_RING0_EXT_CTRL, 0x4);
+
+	/* command to WM - ring 15 */
+	ret = mt76_init_mcu_queue(&dev->mt76, MT_MCUQ_WM,
+				  MT7925_TXQ_MCU_WM,
+				  MT7925_TX_MCU_RING_SIZE, MT_TX_RING_BASE);
+	if (ret)
+		return ret;
+
+	/* firmware download - ring 16 */
+	ret = mt76_init_mcu_queue(&dev->mt76, MT_MCUQ_FWDL,
+				  MT7925_TXQ_FWDL,
+				  MT7925_TX_FWDL_RING_SIZE, MT_TX_RING_BASE);
+	if (ret)
+		return ret;
+
+	/* rx MCU events - ring 6 (MT7925 uses ring 0) */
+	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MCU],
+			       MT6639_RXQ_MCU_WM, MT7925_RX_MCU_RING_SIZE,
+			       MT_RX_BUF_SIZE, MT_RX_EVENT_RING_BASE);
+	if (ret)
+		return ret;
+
+	/* rx data - ring 4 (MT7925 uses ring 2) */
+	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MAIN],
+			       MT6639_RXQ_BAND0, MT7925_RX_RING_SIZE,
+			       MT_RX_BUF_SIZE, MT_RX_DATA_RING_BASE);
+	if (ret)
+		return ret;
+
+	/* rx auxiliary - ring 7: management frames */
+	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MCU_WA],
+			       MT6639_RXQ_DATA2, MT7925_RX_MCU_RING_SIZE,
+			       MT_RX_BUF_SIZE, MT_RX_DATA_RING_BASE);
+	if (ret)
+		return ret;
+
+	ret = mt76_init_queues(dev, mt792x_poll_rx);
+	if (ret < 0)
+		return ret;
+
+	netif_napi_add_tx(dev->mt76.tx_napi_dev, &dev->mt76.tx_napi,
+			  mt792x_poll_tx);
+	napi_enable(&dev->mt76.tx_napi);
+
+	/* MT6639-specific GLO_CFG bits before DMA enable */
+	mt76_set(dev, MT_WFDMA0_GLO_CFG, MT_WFDMA0_GLO_CFG_ADDR_EXT_EN);
+	mt76_clear(dev, MT_WFDMA0_GLO_CFG, MT_WFDMA0_GLO_CFG_CSR_LBK_RX_Q_SEL_EN);
+	mt76_set(dev, MT_WFDMA0_GLO_CFG,
+		 MT_WFDMA0_GLO_CFG_FW_DWLD_BYPASS_DMASHDL);
+
+	ret = mt792x_dma_enable(dev);
+	if (ret)
+		return ret;
+
+	/* Enable interrupts synchronously */
+	mt76_wr(dev, MT_WFDMA0_HOST_INT_ENA, dev->mt76.mmio.irqmask);
+
+	return 0;
+}
+
+static const struct mt792x_irq_map mt6639_irq_map = {
+	.host_irq_enable = MT_WFDMA0_HOST_INT_ENA,
+	.tx = {
+		.all_complete_mask = MT_INT_TX_DONE_ALL,
+		.mcu_complete_mask = MT_INT_TX_DONE_MCU,
+	},
+	.rx = {
+		.data_complete_mask = HOST_RX_DONE_INT_ENA4,
+		.wm_complete_mask = HOST_RX_DONE_INT_ENA6,
+		.wm2_complete_mask = HOST_RX_DONE_INT_ENA7,
+	},
+};
+
 static int mt7925_pci_probe(struct pci_dev *pdev,
 			    const struct pci_device_id *id)
 {
@@ -396,7 +516,14 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 	if (ret)
 		goto err_free_pci_vec;
 
-	if (mt7925_disable_aspm)
+	/* MT6639 CONNINFRA power domain and WFDMA register access are
+	 * unreliable with PCIe L1 active, causing throughput to drop
+	 * from 1+ Gbps to ~200 Mbps.  Disable ASPM unconditionally
+	 * rather than only L1, since L0s savings are negligible for
+	 * a PCIe WLAN card and mt76_pci_disable_aspm() already
+	 * handles both device and parent bridge in one call.
+	 */
+	if (mt7925_disable_aspm || is_mt6639_hw)
 		mt76_pci_disable_aspm(pdev);
 
 	ops = mt792x_get_mac80211_ops(&pdev->dev, &mt7925_ops,
@@ -406,6 +533,16 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 		goto err_free_pci_vec;
 	}
 
+	/* MT6639 firmware lacks the connac2 feature trailer, so
+	 * mt792x_get_mac80211_ops() can't detect CNM support and
+	 * replaces chanctx/ROC/mgd_prepare_tx ops with stubs.
+	 * Force CNM and restore the original mt7925 ops. */
+	if ((pdev->device == 0x6639 || pdev->device == 0x7927) &&
+	    !(features & MT792x_FW_CAP_CNM)) {
+		features |= MT792x_FW_CAP_CNM;
+		memcpy(ops, &mt7925_ops, sizeof(*ops));
+	}
+
 	mdev = mt76_alloc_device(&pdev->dev, sizeof(*dev), ops, &drv_ops);
 	if (!mdev) {
 		ret = -ENOMEM;
@@ -417,7 +554,10 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 	dev = container_of(mdev, struct mt792x_dev, mt76);
 	dev->fw_features = features;
 	dev->hif_ops = &mt7925_pcie_ops;
-	dev->irq_map = &irq_map;
+	if (is_mt6639_hw)
+		dev->irq_map = &mt6639_irq_map;
+	else
+		dev->irq_map = &irq_map;
 	mt76_mmio_init(&dev->mt76, pcim_iomap_table(pdev)[0]);
 	tasklet_init(&mdev->irq_tasklet, mt792x_irq_tasklet, (unsigned long)dev);
 
@@ -487,7 +627,10 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 	if (ret)
 		goto err_free_dev;
 
-	ret = mt7925_dma_init(dev);
+	if (is_mt6639_hw)
+		ret = mt6639_dma_init(dev);
+	else
+		ret = mt7925_dma_init(dev);
 	if (ret)
 		goto err_free_irq;
 
@@ -627,7 +770,10 @@ static int _mt7925_pci_resume(struct device *device, bool restore)
 	mt76_wr(dev, MT_PCIE_MAC_INT_ENABLE, 0xff);
 	mt76_connac_irq_enable(&dev->mt76,
 			       dev->irq_map->tx.all_complete_mask |
-			       MT_INT_RX_DONE_ALL | MT_INT_MCU_CMD);
+			       dev->irq_map->rx.data_complete_mask |
+			       dev->irq_map->rx.wm_complete_mask |
+			       dev->irq_map->rx.wm2_complete_mask |
+			       MT_INT_MCU_CMD);
 	mt76_set(dev, MT_MCU2HOST_SW_INT_ENA, MT_MCU_CMD_WAKE_RX_PCIE);
 
 	/* put dma enabled */
diff --git a/mt7925/pci_mac.c b/mt7925/pci_mac.c
index 3072850..9c66e48 100644
--- a/mt7925/pci_mac.c
+++ b/mt7925/pci_mac.c
@@ -72,6 +72,14 @@ int mt7925e_mac_reset(struct mt792x_dev *dev)
 	const struct mt792x_irq_map *irq_map = dev->irq_map;
 	int i, err;
 
+	/* MT6639: CLR_OWN and WPDMA reset destroy DMA ring configuration.
+	 * A full reset requires re-running mt6639_dma_init() which is not
+	 * yet implemented in the recovery path. */
+	if (is_mt6639(&dev->mt76)) {
+		dev_warn(dev->mt76.dev, "MT6639 mac_reset not supported, reload module to recover\n");
+		return -EOPNOTSUPP;
+	}
+
 	mt792xe_mcu_drv_pmctrl(dev);
 
 	mt76_connac_free_pending_tx_skbs(&dev->pm, NULL);
@@ -118,7 +126,10 @@ int mt7925e_mac_reset(struct mt792x_dev *dev)
 
 	mt76_wr(dev, dev->irq_map->host_irq_enable,
 		dev->irq_map->tx.all_complete_mask |
-		MT_INT_RX_DONE_ALL | MT_INT_MCU_CMD);
+		dev->irq_map->rx.data_complete_mask |
+		dev->irq_map->rx.wm_complete_mask |
+		dev->irq_map->rx.wm2_complete_mask |
+		MT_INT_MCU_CMD);
 	mt76_wr(dev, MT_PCIE_MAC_INT_ENABLE, 0xff);
 
 	err = mt792xe_mcu_fw_pmctrl(dev);
diff --git a/mt7925/pci_mcu.c b/mt7925/pci_mcu.c
index 6cceff8..83f1723 100644
--- a/mt7925/pci_mcu.c
+++ b/mt7925/pci_mcu.c
@@ -35,13 +35,20 @@ int mt7925e_mcu_init(struct mt792x_dev *dev)
 
 	dev->mt76.mcu_ops = &mt7925_mcu_ops;
 
-	err = mt792xe_mcu_fw_pmctrl(dev);
-	if (err)
-		return err;
+	if (is_mt6639(&dev->mt76)) {
+		/* MT6639: CLR_OWN was already done in mt6639_dma_init().
+		 * The ROM re-initializes WFDMA on every CLR_OWN, wiping
+		 * ring and prefetch config. Skip SET_OWN/CLR_OWN here
+		 * to preserve DMA state. */
+	} else {
+		err = mt792xe_mcu_fw_pmctrl(dev);
+		if (err)
+			return err;
 
-	err = __mt792xe_mcu_drv_pmctrl(dev);
-	if (err)
-		return err;
+		err = __mt792xe_mcu_drv_pmctrl(dev);
+		if (err)
+			return err;
+	}
 
 	mt76_rmw_field(dev, MT_PCIE_MAC_PM, MT_PCIE_MAC_PM_L0S_DIS, 1);
 
diff --git a/mt792x_dma.c b/mt792x_dma.c
index 002aece..04c0663 100644
--- a/mt792x_dma.c
+++ b/mt792x_dma.c
@@ -90,7 +90,23 @@ EXPORT_SYMBOL_GPL(mt792x_rx_poll_complete);
 #define PREFETCH(base, depth)	((base) << 16 | (depth))
 static void mt792x_dma_prefetch(struct mt792x_dev *dev)
 {
-	if (is_mt7925(&dev->mt76)) {
+	if (is_mt6639(&dev->mt76)) {
+		/* Trigger prefetch controller reset before reprogramming */
+		mt76_wr(dev, MT_WFDMA_PREFETCH_CTRL,
+			mt76_rr(dev, MT_WFDMA_PREFETCH_CTRL));
+		/* MT6639 uses packed prefetch registers */
+		mt76_wr(dev, MT_WFDMA_PREFETCH_CFG0, 0x660077);
+		mt76_wr(dev, MT_WFDMA_PREFETCH_CFG1, 0x1100);
+		mt76_wr(dev, MT_WFDMA_PREFETCH_CFG2, 0x30004f);
+		mt76_wr(dev, MT_WFDMA_PREFETCH_CFG3, 0x542200);
+		/* per-ring EXT_CTRL */
+		mt76_wr(dev, MT_WFDMA0_RX_RING4_EXT_CTRL, PREFETCH(0x0000, 0x8));
+		mt76_wr(dev, MT_WFDMA0_RX_RING6_EXT_CTRL, PREFETCH(0x0080, 0x8));
+		mt76_wr(dev, MT_WFDMA0_RX_RING7_EXT_CTRL, PREFETCH(0x0100, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING16_EXT_CTRL, PREFETCH(0x0140, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING15_EXT_CTRL, PREFETCH(0x0180, 0x10));
+		mt76_wr(dev, MT_WFDMA0_TX_RING0_EXT_CTRL, PREFETCH(0x0280, 0x4));
+	} else if (is_mt7925(&dev->mt76)) {
 		/* rx ring */
 		mt76_wr(dev, MT_WFDMA0_RX_RING0_EXT_CTRL, PREFETCH(0x0000, 0x4));
 		mt76_wr(dev, MT_WFDMA0_RX_RING1_EXT_CTRL, PREFETCH(0x0040, 0x4));
@@ -166,6 +182,14 @@ int mt792x_dma_enable(struct mt792x_dev *dev)
 	mt76_set(dev, MT_WFDMA0_GLO_CFG,
 		 MT_WFDMA0_GLO_CFG_TX_DMA_EN | MT_WFDMA0_GLO_CFG_RX_DMA_EN);
 
+	if (is_mt6639(&dev->mt76)) {
+		mt76_set(dev, MT_WFDMA0_GLO_CFG,
+			 MT_WFDMA0_GLO_CFG_ADDR_EXT_EN |
+			 MT_WFDMA0_GLO_CFG_FW_DWLD_BYPASS_DMASHDL);
+		mt76_clear(dev, MT_WFDMA0_GLO_CFG,
+			   MT_WFDMA0_GLO_CFG_CSR_LBK_RX_Q_SEL_EN);
+	}
+
 	if (is_mt7925(&dev->mt76)) {
 		mt76_rmw(dev, MT_UWFDMA0_GLO_CFG_EXT1, BIT(28), BIT(28));
 		mt76_set(dev, MT_WFDMA0_INT_RX_PRI, 0x0F00);
diff --git a/mt792x_regs.h b/mt792x_regs.h
index d831713..621b6b3 100644
--- a/mt792x_regs.h
+++ b/mt792x_regs.h
@@ -301,9 +301,11 @@
 #define MT_WFDMA0_GLO_CFG_FIFO_LITTLE_ENDIAN	BIT(12)
 #define MT_WFDMA0_GLO_CFG_RX_WB_DDONE	BIT(13)
 #define MT_WFDMA0_GLO_CFG_CSR_DISP_BASE_PTR_CHAIN_EN BIT(15)
+#define MT_WFDMA0_GLO_CFG_CSR_LBK_RX_Q_SEL_EN	BIT(20)
 #define MT_WFDMA0_GLO_CFG_OMIT_RX_INFO_PFET2	BIT(21)
 #define MT_WFDMA0_GLO_CFG_OMIT_RX_INFO	BIT(27)
 #define MT_WFDMA0_GLO_CFG_OMIT_TX_INFO	BIT(28)
+#define MT_WFDMA0_GLO_CFG_ADDR_EXT_EN		BIT(26)
 #define MT_WFDMA0_GLO_CFG_CLK_GAT_DIS	BIT(30)
 
 #define HOST_RX_DONE_INT_ENA0		BIT(0)
@@ -365,6 +367,13 @@
 #define MT_WFDMA_EXT_CSR_HIF_MISC	MT_WFDMA_EXT_CSR(0x44)
 #define MT_WFDMA_EXT_CSR_HIF_MISC_BUSY	BIT(0)
 
+/* MT6639 packed prefetch registers */
+#define MT_WFDMA_PREFETCH_CTRL		MT_WFDMA_EXT_CSR(0x30)
+#define MT_WFDMA_PREFETCH_CFG0		MT_WFDMA_EXT_CSR(0xf0)
+#define MT_WFDMA_PREFETCH_CFG1		MT_WFDMA_EXT_CSR(0xf4)
+#define MT_WFDMA_PREFETCH_CFG2		MT_WFDMA_EXT_CSR(0xf8)
+#define MT_WFDMA_PREFETCH_CFG3		MT_WFDMA_EXT_CSR(0xfc)
+
 #define MT_SWDEF_BASE			0x41f200
 #define MT_SWDEF(ofs)			(MT_SWDEF_BASE + (ofs))
 #define MT_SWDEF_MODE			MT_SWDEF(0x3c)

[-- Attachment #3: mt6639-bt-6.19.patch --]
[-- Type: text/x-patch, Size: 3846 bytes --]

--- a/drivers/bluetooth/btmtk.c
+++ b/drivers/bluetooth/btmtk.c
@@ -112,7 +112,11 @@
 void btmtk_fw_get_filename(char *buf, size_t size, u32 dev_id, u32 fw_ver,
 			   u32 fw_flavor)
 {
-	if (dev_id == 0x7925)
+        if (dev_id == 0x6639)
+                snprintf(buf, size,
+                         "mediatek/mt%04x/BT_RAM_CODE_MT%04x_2_%x_hdr.bin",
+                         dev_id & 0xffff, dev_id & 0xffff, (fw_ver & 0xff) + 1);
+        else if (dev_id == 0x7925)
 		snprintf(buf, size,
 			 "mediatek/mt%04x/BT_RAM_CODE_MT%04x_1_%x_hdr.bin",
 			 dev_id & 0xffff, dev_id & 0xffff, (fw_ver & 0xff) + 1);
@@ -166,6 +170,15 @@
 		section_offset = le32_to_cpu(sectionmap->secoffset);
 		dl_size = le32_to_cpu(sectionmap->bin_info_spec.dlsize);
 
+                /* MT6639: only download sections where dlmode byte0 == 0x01,
+                 * matching the Windows driver behavior which skips WiFi/other
+                 * sections that would cause the chip to hang.
+                 */
+                if (dl_size > 0 &&
+                    (le32_to_cpu(sectionmap->bin_info_spec.dlmodecrctype) & 0xff) != 0x01) {
+                        continue;
+                }
+
 		if (dl_size > 0) {
 			retry = 20;
 			while (retry > 0) {
@@ -840,7 +853,7 @@
 		if (err < 0)
 			return err;
 		msleep(100);
-	} else if (dev_id == 0x7925) {
+	} else if (dev_id == 0x7925 || dev_id == 0x6639) {
 		err = btmtk_usb_uhw_reg_read(hdev, MTK_BT_RESET_REG_CONNV3, &val);
 		if (err < 0)
 			return err;
@@ -1324,23 +1337,38 @@
 		fwname = FIRMWARE_MT7668;
 		break;
 	case 0x7922:
+	case 0x6639:
 	case 0x7925:
 	case 0x7961:
 		btmtk_fw_get_filename(fw_bin_name, sizeof(fw_bin_name), dev_id,
 				      fw_version, fw_flavor);
 
+		/* MT6639: firmware persists across BT soft power cycles
+		 * (shutdown only sends WMT_FUNC_CTRL=0). Skip re-download
+		 * on subsequent setups to avoid ~2.6s delay.
+		 */
+		if (dev_id == 0x6639 &&
+		    test_bit(BTMTK_FIRMWARE_LOADED, &btmtk_data->flags)) {
+			bt_dev_info(hdev, "MT6639: firmware already loaded, skipping download");
+			goto skip_fw_setup_79xx;
+		}
+
 		err = btmtk_setup_firmware_79xx(hdev, fw_bin_name,
 						btmtk_usb_hci_wmt_sync);
 		if (err < 0) {
 			bt_dev_err(hdev, "Failed to set up firmware (%d)", err);
 			return err;
 		}
 
+		if (dev_id == 0x6639)
+			set_bit(BTMTK_FIRMWARE_LOADED, &btmtk_data->flags);
+
+skip_fw_setup_79xx:
 		/* It's Device EndPoint Reset Option Register */
 		err = btmtk_usb_uhw_reg_write(hdev, MTK_EP_RST_OPT,
 					      MTK_EP_RST_IN_OUT_OPT);
 		if (err < 0)
 			return err;
 
 		/* Enable Bluetooth protocol */
 		param = 1;
--- a/drivers/bluetooth/btmtk.h
+++ b/drivers/bluetooth/btmtk.h
@@ -7,6 +7,7 @@
 #define FIRMWARE_MT7922		"mediatek/BT_RAM_CODE_MT7922_1_1_hdr.bin"
 #define FIRMWARE_MT7961		"mediatek/BT_RAM_CODE_MT7961_1_2_hdr.bin"
 #define FIRMWARE_MT7925		"mediatek/mt7925/BT_RAM_CODE_MT7925_1_1_hdr.bin"
+#define FIRMWARE_MT7927		"mediatek/mt6639/BT_RAM_CODE_MT6639_2_1_hdr.bin"
 
 #define HCI_EV_WMT 0xe4
 #define HCI_WMT_MAX_EVENT_SIZE		64
--- a/drivers/bluetooth/btusb.c
+++ b/drivers/bluetooth/btusb.c
@@ -733,6 +733,14 @@
 						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x0489, 0xe139), .driver_info = BTUSB_MEDIATEK |
 						     BTUSB_WIDEBAND_SPEECH },
+	{ USB_DEVICE(0x0489, 0xe13a), .driver_info = BTUSB_MEDIATEK |
+						     BTUSB_WIDEBAND_SPEECH },
+	{ USB_DEVICE(0x0489, 0xe0fa), .driver_info = BTUSB_MEDIATEK |
+						     BTUSB_WIDEBAND_SPEECH },
+	{ USB_DEVICE(0x0489, 0xe116), .driver_info = BTUSB_MEDIATEK |
+						     BTUSB_WIDEBAND_SPEECH },
+	{ USB_DEVICE(0x13d3, 0x3588), .driver_info = BTUSB_MEDIATEK |
+						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x0489, 0xe14e), .driver_info = BTUSB_MEDIATEK |
 						     BTUSB_WIDEBAND_SPEECH },
 	{ USB_DEVICE(0x0489, 0xe14f), .driver_info = BTUSB_MEDIATEK |

[-- Attachment #4: mt6639-wifi-init.patch --]
[-- Type: text/x-patch, Size: 7245 bytes --]

diff --git a/mt76_connac.h b/mt76_connac.h
index 51423c7..fff498d 100644
--- a/mt76_connac.h
+++ b/mt76_connac.h
@@ -174,7 +174,12 @@ extern const struct wiphy_wowlan_support mt76_connac_wowlan_support;
 
 static inline bool is_mt7925(struct mt76_dev *dev)
 {
-	return mt76_chip(dev) == 0x7925;
+	return mt76_chip(dev) == 0x7925 || mt76_chip(dev) == 0x7927;
+}
+
+static inline bool is_mt6639(struct mt76_dev *dev)
+{
+	return mt76_chip(dev) == 0x7927;
 }
 
 static inline bool is_mt7920(struct mt76_dev *dev)
@@ -279,6 +284,7 @@ static inline bool is_mt76_fw_txp(struct mt76_dev *dev)
 	case 0x7922:
 	case 0x7902:
 	case 0x7925:
+	case 0x7927:
 	case 0x7663:
 	case 0x7622:
 		return false;
diff --git a/mt7925/pci.c b/mt7925/pci.c
index c416175..db053bd 100644
--- a/mt7925/pci.c
+++ b/mt7925/pci.c
@@ -16,6 +16,10 @@ static const struct pci_device_id mt7925_pci_device_table[] = {
 		.driver_data = (kernel_ulong_t)MT7925_FIRMWARE_WM },
 	{ PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x0717),
 		.driver_data = (kernel_ulong_t)MT7925_FIRMWARE_WM },
+	{ PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7927),
+		.driver_data = (kernel_ulong_t)MT7927_FIRMWARE_WM },
+	{ PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x6639),
+		.driver_data = (kernel_ulong_t)MT7927_FIRMWARE_WM },
 	{ },
 };
 
@@ -266,6 +270,60 @@ static int mt7925_dma_init(struct mt792x_dev *dev)
 	return mt792x_dma_enable(dev);
 }
 
+static int mt6639_chip_init(struct mt792x_dev *dev)
+{
+	struct mt76_dev *mdev = &dev->mt76;
+	u32 val;
+
+	/* EMI sleep protect */
+	mt76_rmw_field(dev, MT_HW_EMI_CTL, MT_HW_EMI_CTL_SLPPROT_EN, 1);
+
+	/* WF subsystem reset via CBInfra RGU */
+	mt76_set(dev, MT_CBINFRA_RGU_WF_RST, MT_CBINFRA_RGU_WF_RST_WF_SUBSYS);
+	msleep(1);
+	mt76_clear(dev, MT_CBINFRA_RGU_WF_RST,
+		   MT_CBINFRA_RGU_WF_RST_WF_SUBSYS);
+	msleep(5);
+
+	/* MCU ownership */
+	mt76_wr(dev, MT_CBINFRA_MCU_OWN_SET, BIT(0));
+
+	/* Poll ROMCODE_INDEX for MCU idle */
+	if (!__mt76_poll_msec(mdev, MT_ROMCODE_INDEX,
+			      0xffff, MT_MCU_IDLE_VALUE, 2000)) {
+		val = mt76_rr(dev, MT_ROMCODE_INDEX);
+		dev_err(mdev->dev,
+			"MT6639 MCU idle timeout (ROMCODE_INDEX=0x%04x)\n",
+			val & 0xffff);
+		return -ETIMEDOUT;
+	}
+
+	/* MCIF remap - MCU needs this to DMA to host memory */
+	mt76_wr(dev, MT_MCIF_REMAP_WF_1_BA, MT_MCIF_REMAP_WF_1_BA_VAL);
+
+	/* Disable PCIe sleep */
+	mt76_wr(dev, MT_CBINFRA_SLP_CTRL, 0xffffffff);
+
+	/* Clear CONNINFRA wakeup */
+	mt76_wr(dev, MT_CBINFRA_WAKEPU_TOP, 0x0);
+
+	return 0;
+}
+
+static void mt6639_cbtop_remap(struct mt792x_dev *dev)
+{
+	/* CONNINFRA wakeup - required before CBInfra register access */
+	mt76_wr(dev, MT_CBINFRA_WAKEPU_TOP, 0x1);
+	usleep_range(1000, 2000);
+
+	/* Configure CBTOP PCIe address remap for WF and BT */
+	mt76_wr(dev, MT_CBINFRA_MISC0_REMAP_WF, MT_CBINFRA_REMAP_WF_VAL);
+	mt76_wr(dev, MT_CBINFRA_MISC0_REMAP_BT, MT_CBINFRA_REMAP_BT_VAL);
+
+	/* Readback to push writes */
+	mt76_rr(dev, MT_CBINFRA_MISC0_REMAP_WF);
+}
+
 static int mt7925_pci_probe(struct pci_dev *pdev,
 			    const struct pci_device_id *id)
 {
@@ -310,6 +368,7 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 	struct mt76_bus_ops *bus_ops;
 	struct mt792x_dev *dev;
 	struct mt76_dev *mdev;
+	bool is_mt6639_hw = (pdev->device == 0x6639 || pdev->device == 0x7927);
 	u8 features;
 	int ret;
 	u16 cmd;
@@ -385,20 +444,39 @@ static int mt7925_pci_probe(struct pci_dev *pdev,
 	if (ret)
 		goto err_free_dev;
 
-	ret = __mt792xe_mcu_drv_pmctrl(dev);
-	if (ret)
-		goto err_free_dev;
+	if (!is_mt6639_hw) {
+		ret = __mt792xe_mcu_drv_pmctrl(dev);
+		if (ret)
+			goto err_free_dev;
+	}
+
+	if (is_mt6639_hw)
+		mt6639_cbtop_remap(dev);
 
 	mdev->rev = (mt76_rr(dev, MT_HW_CHIPID) << 16) |
 		    (mt76_rr(dev, MT_HW_REV) & 0xff);
 
 	dev_info(mdev->dev, "ASIC revision: %04x\n", mdev->rev);
 
-	mt76_rmw_field(dev, MT_HW_EMI_CTL, MT_HW_EMI_CTL_SLPPROT_EN, 1);
+	/* Force chip ID for MT6639 hardware if CHIPID read returns garbage */
+	if (is_mt6639_hw && (mdev->rev >> 16) != 0x7927) {
+		dev_info(mdev->dev,
+			 "MT6639 raw CHIPID=0x%04x, forcing chip=0x7927\n",
+			 (u16)(mdev->rev >> 16));
+		mdev->rev = (0x7927 << 16) | (mdev->rev & 0xff);
+	}
 
-	ret = mt792x_wfsys_reset(dev);
-	if (ret)
-		goto err_free_dev;
+	if (is_mt6639_hw) {
+		ret = mt6639_chip_init(dev);
+		if (ret)
+			goto err_free_dev;
+	} else {
+		mt76_rmw_field(dev, MT_HW_EMI_CTL,
+			       MT_HW_EMI_CTL_SLPPROT_EN, 1);
+		ret = mt792x_wfsys_reset(dev);
+		if (ret)
+			goto err_free_dev;
+	}
 
 	mt76_wr(dev, irq_map.host_irq_enable, 0);
 
@@ -633,6 +711,8 @@ module_pci_driver(mt7925_pci_driver);
 MODULE_DEVICE_TABLE(pci, mt7925_pci_device_table);
 MODULE_FIRMWARE(MT7925_FIRMWARE_WM);
 MODULE_FIRMWARE(MT7925_ROM_PATCH);
+MODULE_FIRMWARE(MT7927_FIRMWARE_WM);
+MODULE_FIRMWARE(MT7927_ROM_PATCH);
 MODULE_AUTHOR("Deren Wu <deren.wu@mediatek.com>");
 MODULE_AUTHOR("Lorenzo Bianconi <lorenzo@kernel.org>");
 MODULE_DESCRIPTION("MediaTek MT7925E (PCIe) wireless driver");
diff --git a/mt792x.h b/mt792x.h
index 1f381ab..bb308f1 100644
--- a/mt792x.h
+++ b/mt792x.h
@@ -46,12 +46,14 @@
 #define MT7921_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7961_1.bin"
 #define MT7922_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7922_1.bin"
 #define MT7925_FIRMWARE_WM	"mediatek/mt7925/WIFI_RAM_CODE_MT7925_1_1.bin"
+#define MT7927_FIRMWARE_WM	"mediatek/mt7927/WIFI_RAM_CODE_MT6639_2_1.bin"
 
 #define MT7902_ROM_PATCH	"mediatek/WIFI_MT7902_patch_mcu_1_1_hdr.bin"
 #define MT7920_ROM_PATCH	"mediatek/WIFI_MT7961_patch_mcu_1a_2_hdr.bin"
 #define MT7921_ROM_PATCH	"mediatek/WIFI_MT7961_patch_mcu_1_2_hdr.bin"
 #define MT7922_ROM_PATCH	"mediatek/WIFI_MT7922_patch_mcu_1_1_hdr.bin"
 #define MT7925_ROM_PATCH	"mediatek/mt7925/WIFI_MT7925_PATCH_MCU_1_1_hdr.bin"
+#define MT7927_ROM_PATCH	"mediatek/mt7927/WIFI_MT6639_PATCH_MCU_2_1_hdr.bin"
 
 #define MT792x_SDIO_HDR_TX_BYTES	GENMASK(15, 0)
 #define MT792x_SDIO_HDR_PKT_TYPE	GENMASK(17, 16)
@@ -458,6 +460,8 @@ static inline char *mt792x_ram_name(struct mt792x_dev *dev)
 		return MT7922_FIRMWARE_WM;
 	case 0x7925:
 		return MT7925_FIRMWARE_WM;
+	case 0x7927:
+		return MT7927_FIRMWARE_WM;
 	default:
 		return MT7921_FIRMWARE_WM;
 	}
@@ -474,6 +478,8 @@ static inline char *mt792x_patch_name(struct mt792x_dev *dev)
 		return MT7922_ROM_PATCH;
 	case 0x7925:
 		return MT7925_ROM_PATCH;
+	case 0x7927:
+		return MT7927_ROM_PATCH;
 	default:
 		return MT7921_ROM_PATCH;
 	}
diff --git a/mt792x_regs.h b/mt792x_regs.h
index 7ddde92..d831713 100644
--- a/mt792x_regs.h
+++ b/mt792x_regs.h
@@ -486,4 +486,22 @@
 #define WFSYS_SW_RST_B			BIT(0)
 #define WFSYS_SW_INIT_DONE		BIT(4)
 
+/* CBInfra registers - MT6639/MT7927 combo chip */
+#define MT_CBINFRA_WAKEPU_TOP		0xe01a0
+#define MT_CBINFRA_MISC0_REMAP_WF	0x1f6554
+#define MT_CBINFRA_MISC0_REMAP_BT	0x1f6558
+#define MT_CBINFRA_RGU_WF_RST		0x1f8600
+#define MT_CBINFRA_RGU_WF_RST_WF_SUBSYS	BIT(4)
+#define MT_CBINFRA_MCU_OWN_SET		0x1f5034
+#define MT_CBINFRA_SLP_CTRL		0x1f5018
+#define MT_ROMCODE_INDEX		0xc1604
+#define MT_MCU_IDLE_VALUE		0x1d1e
+#define MT_MCIF_REMAP_WF_1_BA		0xd1034
+
+/* CBInfra CBTOP remap values */
+#define MT_CBINFRA_REMAP_WF_VAL		0x74037001
+#define MT_CBINFRA_REMAP_BT_VAL		0x70007000
+
+#define MT_MCIF_REMAP_WF_1_BA_VAL	0x18051803
+
 #endif /* __MT792X_REGS_H */

[-- Attachment #5: mt7902-wifi-6.19.patch --]
[-- Type: text/x-patch, Size: 20093 bytes --]

diff --git a/mt76_connac.h b/mt76_connac.h
index 813d61bff..51423c774 100644
--- a/mt76_connac.h
+++ b/mt76_connac.h
@@ -182,14 +182,20 @@ static inline bool is_mt7920(struct mt76_dev *dev)
 	return mt76_chip(dev) == 0x7920;
 }
 
+static inline bool is_mt7902(struct mt76_dev *dev)
+{
+	return mt76_chip(dev) == 0x7902;
+}
+
 static inline bool is_mt7922(struct mt76_dev *dev)
 {
 	return mt76_chip(dev) == 0x7922;
 }
 
-static inline bool is_mt7921(struct mt76_dev *dev)
+static inline bool is_connac2(struct mt76_dev *dev)
 {
-	return mt76_chip(dev) == 0x7961 || is_mt7922(dev) || is_mt7920(dev);
+	return mt76_chip(dev) == 0x7961 || is_mt7922(dev) || is_mt7920(dev) ||
+				is_mt7902(dev);
 }
 
 static inline bool is_mt7663(struct mt76_dev *dev)
@@ -271,6 +277,7 @@ static inline bool is_mt76_fw_txp(struct mt76_dev *dev)
 	case 0x7961:
 	case 0x7920:
 	case 0x7922:
+	case 0x7902:
 	case 0x7925:
 	case 0x7663:
 	case 0x7622:
diff --git a/mt76_connac_mac.c b/mt76_connac_mac.c
index 3304b5971..da2ba03ba 100644
--- a/mt76_connac_mac.c
+++ b/mt76_connac_mac.c
@@ -173,7 +173,7 @@ void mt76_connac_write_hw_txp(struct mt76_dev *dev,
 
 	txp->msdu_id[0] = cpu_to_le16(id | MT_MSDU_ID_VALID);
 
-	if (is_mt7663(dev) || is_mt7921(dev) || is_mt7925(dev))
+	if (is_mt7663(dev) || is_connac2(dev) || is_mt7925(dev))
 		last_mask = MT_TXD_LEN_LAST;
 	else
 		last_mask = MT_TXD_LEN_AMSDU_LAST |
@@ -217,7 +217,7 @@ mt76_connac_txp_skb_unmap_hw(struct mt76_dev *dev,
 	u32 last_mask;
 	int i;
 
-	if (is_mt7663(dev) || is_mt7921(dev) || is_mt7925(dev))
+	if (is_mt7663(dev) || is_connac2(dev) || is_mt7925(dev))
 		last_mask = MT_TXD_LEN_LAST;
 	else
 		last_mask = MT_TXD_LEN_MSDU_LAST;
@@ -309,7 +309,7 @@ u16 mt76_connac2_mac_tx_rate_val(struct mt76_phy *mphy,
 	chandef = mvif->ctx ? &mvif->ctx->def : &mphy->chandef;
 	band = chandef->chan->band;
 
-	if (is_mt7921(mphy->dev)) {
+	if (is_connac2(mphy->dev)) {
 		rateidx = ffs(conf->basic_rates) - 1;
 		goto legacy;
 	}
@@ -547,7 +547,7 @@ void mt76_connac2_mac_write_txwi(struct mt76_dev *dev, __le32 *txwi,
 	val = MT_TXD1_LONG_FORMAT |
 	      FIELD_PREP(MT_TXD1_WLAN_IDX, wcid->idx) |
 	      FIELD_PREP(MT_TXD1_OWN_MAC, omac_idx);
-	if (!is_mt7921(dev))
+	if (!is_connac2(dev))
 		val |= MT_TXD1_VTA;
 	if (phy_idx || band_idx)
 		val |= MT_TXD1_TGID;
@@ -556,7 +556,7 @@ void mt76_connac2_mac_write_txwi(struct mt76_dev *dev, __le32 *txwi,
 	txwi[2] = 0;
 
 	val = FIELD_PREP(MT_TXD3_REM_TX_COUNT, 15);
-	if (!is_mt7921(dev))
+	if (!is_connac2(dev))
 		val |= MT_TXD3_SW_POWER_MGMT;
 	if (key)
 		val |= MT_TXD3_PROTECT_FRAME;
@@ -598,7 +598,7 @@ void mt76_connac2_mac_write_txwi(struct mt76_dev *dev, __le32 *txwi,
 		txwi[6] |= cpu_to_le32(val);
 		txwi[3] |= cpu_to_le32(MT_TXD3_BA_DISABLE);
 
-		if (!is_mt7921(dev)) {
+		if (!is_connac2(dev)) {
 			u8 spe_idx = mt76_connac_spe_idx(mphy->antenna_mask);
 
 			if (!spe_idx)
@@ -830,7 +830,7 @@ mt76_connac2_mac_decode_he_mu_radiotap(struct mt76_dev *dev, struct sk_buff *skb
 	};
 	struct ieee80211_radiotap_he_mu *he_mu;
 
-	if (is_mt7921(dev)) {
+	if (is_connac2(dev)) {
 		mu_known.flags1 |= HE_BITS(MU_FLAGS1_SIG_B_COMP_KNOWN);
 		mu_known.flags2 |= HE_BITS(MU_FLAGS2_PUNC_FROM_SIG_A_BW_KNOWN);
 	}
@@ -1046,7 +1046,7 @@ int mt76_connac2_mac_fill_rx_rate(struct mt76_dev *dev,
 		stbc = FIELD_GET(MT_PRXV_HT_STBC, v0);
 		gi = FIELD_GET(MT_PRXV_HT_SGI, v0);
 		*mode = FIELD_GET(MT_PRXV_TX_MODE, v0);
-		if (is_mt7921(dev))
+		if (is_connac2(dev))
 			dcm = !!(idx & MT_PRXV_TX_DCM);
 		else
 			dcm = FIELD_GET(MT_PRXV_DCM, v0);
diff --git a/mt76_connac_mcu.c b/mt76_connac_mcu.c
index 045771228..f032b4c76 100644
--- a/mt76_connac_mcu.c
+++ b/mt76_connac_mcu.c
@@ -4,6 +4,7 @@
 #include <linux/firmware.h>
 #include "mt76_connac2_mac.h"
 #include "mt76_connac_mcu.h"
+#include "mt792x_regs.h"
 
 int mt76_connac_mcu_start_firmware(struct mt76_dev *dev, u32 addr, u32 option)
 {
@@ -65,7 +66,7 @@ int mt76_connac_mcu_init_download(struct mt76_dev *dev, u32 addr, u32 len,
 	int cmd;
 
 	if ((!is_connac_v1(dev) && addr == MCU_PATCH_ADDRESS) ||
-	    (is_mt7921(dev) && addr == 0x900000) ||
+	    (is_connac2(dev) && addr == 0x900000) ||
 	    (is_mt7925(dev) && (addr == 0x900000 || addr == 0xe0002800)) ||
 	    (is_mt799x(dev) && addr == 0x900000))
 		cmd = MCU_CMD(PATCH_START_REQ);
@@ -402,7 +403,7 @@ void mt76_connac_mcu_sta_basic_tlv(struct mt76_dev *dev, struct sk_buff *skb,
 	switch (vif->type) {
 	case NL80211_IFTYPE_MESH_POINT:
 	case NL80211_IFTYPE_AP:
-		if (vif->p2p && !is_mt7921(dev))
+		if (vif->p2p && !is_connac2(dev))
 			conn_type = CONNECTION_P2P_GC;
 		else
 			conn_type = CONNECTION_INFRA_STA;
@@ -410,7 +411,7 @@ void mt76_connac_mcu_sta_basic_tlv(struct mt76_dev *dev, struct sk_buff *skb,
 		basic->aid = cpu_to_le16(link_sta->sta->aid);
 		break;
 	case NL80211_IFTYPE_STATION:
-		if (vif->p2p && !is_mt7921(dev))
+		if (vif->p2p && !is_connac2(dev))
 			conn_type = CONNECTION_P2P_GO;
 		else
 			conn_type = CONNECTION_INFRA_AP;
@@ -874,7 +875,7 @@ void mt76_connac_mcu_sta_tlv(struct mt76_phy *mphy, struct sk_buff *skb,
 		struct sta_rec_vht *vht;
 		int len;
 
-		len = is_mt7921(dev) ? sizeof(*vht) : sizeof(*vht) - 4;
+		len = is_connac2(dev) ? sizeof(*vht) : sizeof(*vht) - 4;
 		tlv = mt76_connac_mcu_add_tlv(skb, STA_REC_VHT, len);
 		vht = (struct sta_rec_vht *)tlv;
 		vht->vht_cap = cpu_to_le32(sta->deflink.vht_cap.cap);
@@ -885,7 +886,7 @@ void mt76_connac_mcu_sta_tlv(struct mt76_phy *mphy, struct sk_buff *skb,
 	/* starec uapsd */
 	mt76_connac_mcu_sta_uapsd(skb, vif, sta);
 
-	if (!is_mt7921(dev))
+	if (!is_connac2(dev))
 		return;
 
 	if (sta->deflink.ht_cap.ht_supported || sta->deflink.he_cap.has_he)
@@ -1774,7 +1775,7 @@ int mt76_connac_mcu_hw_scan(struct mt76_phy *phy, struct ieee80211_vif *vif,
 	req->ssid_type_ext = n_ssids ? BIT(0) : 0;
 	req->ssids_num = n_ssids;
 
-	duration = is_mt7921(phy->dev) ? 0 : MT76_CONNAC_SCAN_CHANNEL_TIME;
+	duration = is_connac2(phy->dev) ? 0 : MT76_CONNAC_SCAN_CHANNEL_TIME;
 	/* increase channel time for passive scan */
 	if (!sreq->n_ssids)
 		duration *= 2;
@@ -1817,7 +1818,7 @@ int mt76_connac_mcu_hw_scan(struct mt76_phy *phy, struct ieee80211_vif *vif,
 		req->ies_len = cpu_to_le16(sreq->ie_len);
 	}
 
-	if (is_mt7921(phy->dev))
+	if (is_connac2(phy->dev))
 		req->scan_func |= SCAN_FUNC_SPLIT_SCAN;
 
 	memcpy(req->bssid, sreq->bssid, ETH_ALEN);
@@ -1893,7 +1894,7 @@ int mt76_connac_mcu_sched_scan_req(struct mt76_phy *phy,
 		get_random_mask_addr(addr, sreq->mac_addr,
 				     sreq->mac_addr_mask);
 	}
-	if (is_mt7921(phy->dev)) {
+	if (is_connac2(phy->dev)) {
 		req->mt7921.bss_idx = mvif->idx;
 		req->mt7921.delay = cpu_to_le32(sreq->delay);
 	}
@@ -2033,7 +2034,7 @@ mt76_connac_mcu_build_sku(struct mt76_dev *dev, s8 *sku,
 			  struct mt76_power_limits *limits,
 			  enum nl80211_band band)
 {
-	int max_power = is_mt7921(dev) ? 127 : 63;
+	int max_power = is_connac2(dev) ? 127 : 63;
 	int i, offset = sizeof(limits->cck);
 
 	memset(sku, max_power, MT_SKU_POWER_LIMIT);
@@ -2061,7 +2062,7 @@ mt76_connac_mcu_build_sku(struct mt76_dev *dev, s8 *sku,
 		offset += 12;
 	}
 
-	if (!is_mt7921(dev))
+	if (!is_connac2(dev))
 		return;
 
 	/* he */
@@ -2117,7 +2118,7 @@ mt76_connac_mcu_rate_txpower_band(struct mt76_phy *phy,
 				  enum nl80211_band band)
 {
 	struct mt76_dev *dev = phy->dev;
-	int sku_len, batch_len = is_mt7921(dev) ? 8 : 16;
+	int sku_len, batch_len = is_connac2(dev) ? 8 : 16;
 	static const u8 chan_list_2ghz[] = {
 		1, 2,  3,  4,  5,  6,  7,
 		8, 9, 10, 11, 12, 13, 14
@@ -2158,7 +2159,7 @@ mt76_connac_mcu_rate_txpower_band(struct mt76_phy *phy,
 	if (!limits)
 		return -ENOMEM;
 
-	sku_len = is_mt7921(dev) ? sizeof(sku_tlbv) : sizeof(sku_tlbv) - 92;
+	sku_len = is_connac2(dev) ? sizeof(sku_tlbv) : sizeof(sku_tlbv) - 92;
 	tx_power = 2 * phy->hw->conf.power_level;
 	if (!tx_power)
 		tx_power = 127;
@@ -2242,6 +2243,9 @@ mt76_connac_mcu_rate_txpower_band(struct mt76_phy *phy,
 					    false);
 		if (err < 0)
 			goto out;
+
+		/* read a CR to avoid PSE buffer underflow */
+		mt76_connac_mcu_reg_rr(dev, MT_PSE_BASE);
 	}
 
 out:
@@ -3072,7 +3076,7 @@ static u32 mt76_connac2_get_data_mode(struct mt76_dev *dev, u32 info)
 {
 	u32 mode = DL_MODE_NEED_RSP;
 
-	if ((!is_mt7921(dev) && !is_mt7925(dev)) || info == PATCH_SEC_NOT_SUPPORT)
+	if ((!is_connac2(dev) && !is_mt7925(dev)) || info == PATCH_SEC_NOT_SUPPORT)
 		return mode;
 
 	switch (FIELD_GET(PATCH_SEC_ENC_TYPE_MASK, info)) {
diff --git a/mt76_connac_mcu.h b/mt76_connac_mcu.h
index 8d59cf43f..e9ec3b7c7 100644
--- a/mt76_connac_mcu.h
+++ b/mt76_connac_mcu.h
@@ -1865,7 +1865,7 @@ mt76_connac_mcu_gen_dl_mode(struct mt76_dev *dev, u8 feature_set, bool is_wa)
 
 	ret |= feature_set & FW_FEATURE_SET_ENCRYPT ?
 	       DL_MODE_ENCRYPT | DL_MODE_RESET_SEC_IV : 0;
-	if (is_mt7921(dev) || is_mt7925(dev))
+	if (is_connac2(dev) || is_mt7925(dev))
 		ret |= feature_set & FW_FEATURE_ENCRY_MODE ?
 		       DL_CONFIG_ENCRY_MODE_SEL : 0;
 	ret |= FIELD_PREP(DL_MODE_KEY_IDX,
diff --git a/mt7921/init.c b/mt7921/init.c
index 29732315a..8e7790702 100644
--- a/mt7921/init.c
+++ b/mt7921/init.c
@@ -302,7 +302,9 @@ int mt7921_register_device(struct mt792x_dev *dev)
 	dev->pm.idle_timeout = MT792x_PM_TIMEOUT;
 	dev->pm.stats.last_wake_event = jiffies;
 	dev->pm.stats.last_doze_event = jiffies;
-	if (!mt76_is_usb(&dev->mt76)) {
+
+	if (!mt76_is_usb(&dev->mt76) &&
+	    !is_mt7902(&dev->mt76)) {
 		dev->pm.enable_user = true;
 		dev->pm.enable = true;
 		dev->pm.ds_enable_user = true;
diff --git a/mt7921/mt7921.h b/mt7921/mt7921.h
index 83fc7f49f..d35e6d57c 100644
--- a/mt7921/mt7921.h
+++ b/mt7921/mt7921.h
@@ -15,6 +15,9 @@
 #define MT7921_RX_MCU_RING_SIZE		8
 #define MT7921_RX_MCU_WA_RING_SIZE	512
 
+/* MT7902 Rx Ring0 is for both Rx Event and Tx Done Event */
+#define MT7902_RX_MCU_RING_SIZE		512
+
 #define MT7921_EEPROM_SIZE		3584
 #define MT7921_TOKEN_SIZE		8192
 
@@ -117,6 +120,17 @@ enum mt7921_rxq_id {
 	MT7921_RXQ_MCU_WM = 0,
 };
 
+/* MT7902 assigns its MCU-WM TXQ at index 15 */
+enum mt7902_txq_id {
+	MT7902_TXQ_MCU_WM = 15,
+};
+
+struct mt7921_dma_layout {
+	u8 mcu_wm_txq;
+	u16 mcu_rxdone_ring_size;
+	bool has_mcu_wa;
+};
+
 enum {
 	MT7921_CLC_POWER,
 	MT7921_CLC_CHAN,
diff --git a/mt7921/pci.c b/mt7921/pci.c
index ec9686183..867b31308 100644
--- a/mt7921/pci.c
+++ b/mt7921/pci.c
@@ -26,6 +26,8 @@ static const struct pci_device_id mt7921_pci_device_table[] = {
 		.driver_data = (kernel_ulong_t)MT7922_FIRMWARE_WM },
 	{ PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7920),
 		.driver_data = (kernel_ulong_t)MT7920_FIRMWARE_WM },
+	{ PCI_DEVICE(PCI_VENDOR_ID_MEDIATEK, 0x7902),
+		.driver_data = (kernel_ulong_t)MT7902_FIRMWARE_WM },
 	{ },
 };
 
@@ -167,8 +169,29 @@ static u32 mt7921_rmw(struct mt76_dev *mdev, u32 offset, u32 mask, u32 val)
 
 static int mt7921_dma_init(struct mt792x_dev *dev)
 {
+	struct mt7921_dma_layout layout = {
+		/* General case: MT7921 / MT7922 /MT7920 */
+		.mcu_wm_txq            = MT7921_TXQ_MCU_WM,
+		.mcu_rxdone_ring_size  = MT7921_RX_MCU_RING_SIZE,
+		.has_mcu_wa            = true,
+	};
+	bool is_mt7902;
 	int ret;
 
+	is_mt7902 = mt7921_l1_rr(dev, MT_HW_CHIPID) == 0x7902;
+
+	/*
+	 * MT7902 special case:
+	 *   - MCU-WM TXQ uses index 15
+	 *   - RX Ring0 is larger and shared for event/TX-done
+	 *   - MT7902 does not use the MCU_WA ring
+	 */
+	if (is_mt7902) {
+		layout.mcu_wm_txq           = MT7902_TXQ_MCU_WM;
+		layout.mcu_rxdone_ring_size = MT7902_RX_MCU_RING_SIZE;
+		layout.has_mcu_wa           = false;
+	}
+
 	mt76_dma_attach(&dev->mt76);
 
 	ret = mt792x_dma_disable(dev, true);
@@ -185,7 +208,7 @@ static int mt7921_dma_init(struct mt792x_dev *dev)
 	mt76_wr(dev, MT_WFDMA0_TX_RING0_EXT_CTRL, 0x4);
 
 	/* command to WM */
-	ret = mt76_init_mcu_queue(&dev->mt76, MT_MCUQ_WM, MT7921_TXQ_MCU_WM,
+	ret = mt76_init_mcu_queue(&dev->mt76, MT_MCUQ_WM, layout.mcu_wm_txq,
 				  MT7921_TX_MCU_RING_SIZE, MT_TX_RING_BASE);
 	if (ret)
 		return ret;
@@ -199,18 +222,20 @@ static int mt7921_dma_init(struct mt792x_dev *dev)
 	/* event from WM before firmware download */
 	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MCU],
 			       MT7921_RXQ_MCU_WM,
-			       MT7921_RX_MCU_RING_SIZE,
+			       layout.mcu_rxdone_ring_size,
 			       MT_RX_BUF_SIZE, MT_RX_EVENT_RING_BASE);
 	if (ret)
 		return ret;
 
-	/* Change mcu queue after firmware download */
-	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MCU_WA],
-			       MT7921_RXQ_MCU_WM,
-			       MT7921_RX_MCU_WA_RING_SIZE,
-			       MT_RX_BUF_SIZE, MT_WFDMA0(0x540));
-	if (ret)
-		return ret;
+	if (layout.has_mcu_wa) {
+		/* Change mcu queue after firmware download */
+		ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MCU_WA],
+				       MT7921_RXQ_MCU_WM,
+				       MT7921_RX_MCU_WA_RING_SIZE,
+				       MT_RX_BUF_SIZE, MT_WFDMA0(0x540));
+		if (ret)
+			return ret;
+	}
 
 	/* rx data */
 	ret = mt76_queue_alloc(dev, &dev->mt76.q_rx[MT_RXQ_MAIN],
@@ -326,6 +351,20 @@ static int mt7921_pci_probe(struct pci_dev *pdev,
 	dev->hif_ops = &mt7921_pcie_ops;
 	dev->irq_map = &irq_map;
 	mt76_mmio_init(&dev->mt76, pcim_iomap_table(pdev)[0]);
+
+	if (id->device == 0x7902) {
+		struct mt792x_irq_map *map;
+
+		/* MT7902 needs a mutable copy because wm2_complete_mask differs */
+		map = devm_kmemdup(&pdev->dev, &irq_map,
+				   sizeof(irq_map), GFP_KERNEL);
+		if (!map)
+			return -ENOMEM;
+
+		map->rx.wm2_complete_mask = 0;
+		dev->irq_map = map;
+	}
+
 	tasklet_init(&mdev->irq_tasklet, mt792x_irq_tasklet, (unsigned long)dev);
 
 	dev->phy.dev = dev;
@@ -579,6 +618,8 @@ MODULE_FIRMWARE(MT7921_FIRMWARE_WM);
 MODULE_FIRMWARE(MT7921_ROM_PATCH);
 MODULE_FIRMWARE(MT7922_FIRMWARE_WM);
 MODULE_FIRMWARE(MT7922_ROM_PATCH);
+MODULE_FIRMWARE(MT7902_FIRMWARE_WM);
+MODULE_FIRMWARE(MT7902_ROM_PATCH);
 MODULE_AUTHOR("Sean Wang <sean.wang@mediatek.com>");
 MODULE_AUTHOR("Lorenzo Bianconi <lorenzo@kernel.org>");
 MODULE_DESCRIPTION("MediaTek MT7921E (PCIe) wireless driver");
diff --git a/mt7921/pci_mac.c b/mt7921/pci_mac.c
index 5ec084432..0db7acb3a 100644
--- a/mt7921/pci_mac.c
+++ b/mt7921/pci_mac.c
@@ -71,9 +71,9 @@ int mt7921e_mac_reset(struct mt792x_dev *dev)
 	mt76_txq_schedule_all(&dev->mphy);
 
 	mt76_worker_disable(&dev->mt76.tx_worker);
-	napi_disable(&dev->mt76.napi[MT_RXQ_MAIN]);
-	napi_disable(&dev->mt76.napi[MT_RXQ_MCU]);
-	napi_disable(&dev->mt76.napi[MT_RXQ_MCU_WA]);
+	mt76_for_each_q_rx(&dev->mt76, i) {
+		napi_disable(&dev->mt76.napi[i]);
+	}
 	napi_disable(&dev->mt76.tx_napi);
 
 	mt76_connac2_tx_token_put(&dev->mt76);
diff --git a/mt7921/sdio.c b/mt7921/sdio.c
index 3421e53dc..9150f1857 100644
--- a/mt7921/sdio.c
+++ b/mt7921/sdio.c
@@ -19,6 +19,8 @@
 static const struct sdio_device_id mt7921s_table[] = {
 	{ SDIO_DEVICE(SDIO_VENDOR_ID_MEDIATEK, 0x7901),
 		.driver_data = (kernel_ulong_t)MT7921_FIRMWARE_WM },
+	{ SDIO_DEVICE(SDIO_VENDOR_ID_MEDIATEK, 0x7902),
+		.driver_data = (kernel_ulong_t)MT7902_FIRMWARE_WM },
 	{ }	/* Terminating entry */
 };
 
@@ -317,6 +319,8 @@ static int mt7921s_resume(struct device *__dev)
 MODULE_DEVICE_TABLE(sdio, mt7921s_table);
 MODULE_FIRMWARE(MT7921_FIRMWARE_WM);
 MODULE_FIRMWARE(MT7921_ROM_PATCH);
+MODULE_FIRMWARE(MT7902_FIRMWARE_WM);
+MODULE_FIRMWARE(MT7902_ROM_PATCH);
 
 static DEFINE_SIMPLE_DEV_PM_OPS(mt7921s_pm_ops, mt7921s_suspend, mt7921s_resume);
 
diff --git a/mt792x.h b/mt792x.h
index 8388638ed..1f381ab35 100644
--- a/mt792x.h
+++ b/mt792x.h
@@ -41,11 +41,13 @@
 #define MT792x_MCU_INIT_RETRY_COUNT	10
 #define MT792x_WFSYS_INIT_RETRY_COUNT	2
 
+#define MT7902_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7902_1.bin"
 #define MT7920_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7961_1a.bin"
 #define MT7921_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7961_1.bin"
 #define MT7922_FIRMWARE_WM	"mediatek/WIFI_RAM_CODE_MT7922_1.bin"
 #define MT7925_FIRMWARE_WM	"mediatek/mt7925/WIFI_RAM_CODE_MT7925_1_1.bin"
 
+#define MT7902_ROM_PATCH	"mediatek/WIFI_MT7902_patch_mcu_1_1_hdr.bin"
 #define MT7920_ROM_PATCH	"mediatek/WIFI_MT7961_patch_mcu_1a_2_hdr.bin"
 #define MT7921_ROM_PATCH	"mediatek/WIFI_MT7961_patch_mcu_1_2_hdr.bin"
 #define MT7922_ROM_PATCH	"mediatek/WIFI_MT7922_patch_mcu_1_1_hdr.bin"
@@ -448,6 +450,8 @@ void mt792x_config_mac_addr_list(struct mt792x_dev *dev);
 static inline char *mt792x_ram_name(struct mt792x_dev *dev)
 {
 	switch (mt76_chip(&dev->mt76)) {
+	case 0x7902:
+		return MT7902_FIRMWARE_WM;
 	case 0x7920:
 		return MT7920_FIRMWARE_WM;
 	case 0x7922:
@@ -462,6 +466,8 @@ static inline char *mt792x_ram_name(struct mt792x_dev *dev)
 static inline char *mt792x_patch_name(struct mt792x_dev *dev)
 {
 	switch (mt76_chip(&dev->mt76)) {
+	case 0x7902:
+		return MT7902_ROM_PATCH;
 	case 0x7920:
 		return MT7920_ROM_PATCH;
 	case 0x7922:
diff --git a/mt792x_core.c b/mt792x_core.c
index f2ed16feb..8fca3ed62 100644
--- a/mt792x_core.c
+++ b/mt792x_core.c
@@ -151,7 +151,7 @@ void mt792x_stop(struct ieee80211_hw *hw, bool suspend)
 	cancel_work_sync(&dev->reset_work);
 	mt76_connac_free_pending_tx_skbs(&dev->pm, NULL);
 
-	if (is_mt7921(&dev->mt76)) {
+	if (is_connac2(&dev->mt76)) {
 		mt792x_mutex_acquire(dev);
 		mt76_connac_mcu_set_mac_enable(&dev->mt76, 0, false, false);
 		mt792x_mutex_release(dev);
@@ -691,7 +691,7 @@ int mt792x_init_wiphy(struct ieee80211_hw *hw)
 	ieee80211_hw_set(hw, SUPPORTS_MULTI_BSSID);
 	ieee80211_hw_set(hw, SUPPORTS_ONLY_HE_MULTI_BSSID);
 
-	if (is_mt7921(&dev->mt76)) {
+	if (is_connac2(&dev->mt76)) {
 		ieee80211_hw_set(hw, CHANCTX_STA_CSA);
 	}
 
@@ -927,6 +927,13 @@ int mt792x_load_firmware(struct mt792x_dev *dev)
 {
 	int ret;
 
+	mt76_connac_mcu_restart(&dev->mt76);
+
+	if (!mt76_poll_msec(dev, MT_CONN_ON_MISC, MT_TOP_MISC_FW_STATE,
+			    MT_TOP_MISC2_FW_PWR_ON, 1000))
+		dev_warn(dev->mt76.dev,
+			 "MCU is not ready for firmware download\n");
+
 	ret = mt76_connac2_load_patch(&dev->mt76, mt792x_patch_name(dev));
 	if (ret)
 		return ret;
diff --git a/mt792x_dma.c b/mt792x_dma.c
index 1ddec7788..002aece85 100644
--- a/mt792x_dma.c
+++ b/mt792x_dma.c
@@ -103,6 +103,22 @@ static void mt792x_dma_prefetch(struct mt792x_dev *dev)
 		mt76_wr(dev, MT_WFDMA0_TX_RING3_EXT_CTRL, PREFETCH(0x0400, 0x10));
 		mt76_wr(dev, MT_WFDMA0_TX_RING15_EXT_CTRL, PREFETCH(0x0500, 0x4));
 		mt76_wr(dev, MT_WFDMA0_TX_RING16_EXT_CTRL, PREFETCH(0x0540, 0x4));
+	} else if (is_mt7902(&dev->mt76)) {
+		/* rx ring */
+		mt76_wr(dev, MT_WFDMA0_RX_RING0_EXT_CTRL, PREFETCH(0x0000, 0x4));
+		mt76_wr(dev, MT_WFDMA0_RX_RING1_EXT_CTRL, PREFETCH(0x0040, 0x4));
+		mt76_wr(dev, MT_WFDMA0_RX_RING2_EXT_CTRL, PREFETCH(0x0080, 0x4));
+		mt76_wr(dev, MT_WFDMA0_RX_RING3_EXT_CTRL, PREFETCH(0x00c0, 0x4));
+		/* tx ring */
+		mt76_wr(dev, MT_WFDMA0_TX_RING0_EXT_CTRL, PREFETCH(0x0100, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING1_EXT_CTRL, PREFETCH(0x0140, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING2_EXT_CTRL, PREFETCH(0x0180, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING3_EXT_CTRL, PREFETCH(0x01c0, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING4_EXT_CTRL, PREFETCH(0x0200, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING5_EXT_CTRL, PREFETCH(0x0240, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING6_EXT_CTRL, PREFETCH(0x0280, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING15_EXT_CTRL, PREFETCH(0x02c0, 0x4));
+		mt76_wr(dev, MT_WFDMA0_TX_RING16_EXT_CTRL, PREFETCH(0x0300, 0x4));
 	} else {
 		/* rx ring */
 		mt76_wr(dev, MT_WFDMA0_RX_RING0_EXT_CTRL, PREFETCH(0x0, 0x4));
@@ -356,7 +372,7 @@ EXPORT_SYMBOL_GPL(mt792x_poll_rx);
 
 int mt792x_wfsys_reset(struct mt792x_dev *dev)
 {
-	u32 addr = is_mt7921(&dev->mt76) ? 0x18000140 : 0x7c000140;
+	u32 addr = is_connac2(&dev->mt76) ? 0x18000140 : 0x7c000140;
 
 	mt76_clear(dev, addr, WFSYS_SW_RST_B);
 	msleep(50);
diff --git a/mt792x_regs.h b/mt792x_regs.h
index acf627aed..7ddde9286 100644
--- a/mt792x_regs.h
+++ b/mt792x_regs.h
@@ -25,6 +25,8 @@
 #define MT_PLE_AC_QEMPTY(_n)		MT_PLE(0x500 + 0x40 * (_n))
 #define MT_PLE_AMSDU_PACK_MSDU_CNT(n)	MT_PLE(0x10e0 + ((n) << 2))
 
+#define MT_PSE_BASE			0x820c8000
+
 /* TMAC: band 0(0x21000), band 1(0xa1000) */
 #define MT_WF_TMAC_BASE(_band)		((_band) ? 0x820f4000 : 0x820e4000)
 #define MT_WF_TMAC(_band, ofs)		(MT_WF_TMAC_BASE(_band) + (ofs))

[-- Attachment #6: extract_firmware.py --]
[-- Type: text/x-python, Size: 2798 bytes --]

#!/usr/bin/env python3

import os
import struct
import sys

# All firmware blobs to extract from mtkwlan.dat
FIRMWARE_BLOBS = [
    "BT_RAM_CODE_MT6639_2_1_hdr.bin",
    "WIFI_MT6639_PATCH_MCU_2_1_hdr.bin",
    "WIFI_RAM_CODE_MT6639_2_1.bin",
]


def extract_by_name(data, name, output_path):
    """Extract a named firmware blob from a mtkwlan.dat container.

    The container format has entries with:
      - A null-terminated ASCII name
      - Padding nulls + a 14-digit numeric timestamp
      - 4-byte aligned offset and size fields (little-endian u32)
      - The firmware blob at the given offset
    """
    name_bytes = name.encode() if isinstance(name, str) else name
    idx = data.find(name_bytes)
    if idx == -1:
        raise RuntimeError(f"Firmware entry '{name}' not found in container")

    entry_pos = idx + len(name_bytes)

    # Skip null padding after name
    while entry_pos < len(data) and data[entry_pos] == 0x00:
        entry_pos += 1

    # Skip 14-digit numeric timestamp if present
    if all(48 <= b <= 57 for b in data[entry_pos : entry_pos + 14]):
        entry_pos += 14

    # Align to 4-byte boundary
    entry_pos = (entry_pos + 3) & ~3

    data_offset = struct.unpack_from("<I", data, entry_pos)[0]
    data_size = struct.unpack_from("<I", data, entry_pos + 4)[0]

    blob = data[data_offset : data_offset + data_size]

    if len(blob) != data_size:
        raise RuntimeError(
            f"Size mismatch for '{name}': expected {data_size}, got {len(blob)}"
        )

    out_dir = os.path.dirname(output_path)
    if out_dir:
        os.makedirs(out_dir, exist_ok=True)

    with open(output_path, "wb") as f:
        f.write(blob)

    print(f"Extracted {name}: {len(blob)} bytes -> {output_path}")


def extract_all(mtkwlan_path, output_dir):
    """Extract all known firmware blobs to a directory."""
    with open(mtkwlan_path, "rb") as f:
        data = f.read()

    os.makedirs(output_dir, exist_ok=True)

    for name in FIRMWARE_BLOBS:
        output_path = os.path.join(output_dir, name)
        extract_by_name(data, name, output_path)


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("usage: extract_firmware.py <mtkwlan.dat> <output-dir-or-file>")
        print(
            "  If output is a directory or has no extension: extract all firmware blobs"
        )
        print("  If output is a .bin file: extract BT firmware only (legacy mode)")
        sys.exit(1)

    mtkwlan_path = sys.argv[1]
    output = sys.argv[2]

    # Legacy mode: if output looks like a specific .bin file, extract BT only
    if output.endswith(".bin"):
        with open(mtkwlan_path, "rb") as f:
            data = f.read()
        extract_by_name(data, FIRMWARE_BLOBS[0], output)
    else:
        extract_all(mtkwlan_path, output)

^ permalink raw reply related

* [BUG] MT7902: Severe Wi-Fi latency during Bluetooth A2DP playback on 2.4 GHz
From: JZARGO @ 2026-06-27  7:58 UTC (permalink / raw)
  To: linux-wireless

Hello,

I'm using Linux 7.1.0-1-mainline with a MediaTek MT7902 (PCI ID:
14c3:7902), which is supported by the mt7921e driver.

I've noticed what appears to be a reproducible Wi-Fi/Bluetooth
coexistence issue.

When I connect my Bluetooth headphones (Soundcore P30i) and start
playing audio (A2DP), the Wi-Fi connection becomes almost unusable.
Ping to my local router (192.168.0.1) increases from around 1 ms to
over 1700 ms, and ping to external hosts can reach several seconds.

I also consistently receive duplicate ICMP replies (DUP!) while the
issue is occurring, even when pinging the local gateway. For example:

64 bytes from 192.168.0.1: icmp_seq=18 ttl=64 time=116 ms
64 bytes from 192.168.0.1: icmp_seq=18 ttl=64 time=116 ms (DUP!)
64 bytes from 192.168.0.1: icmp_seq=18 ttl=64 time=117 ms (DUP!)

The problem is fully reproducible. As soon as Bluetooth audio starts,
Wi-Fi latency increases dramatically. As soon as I stop playback or
disconnect the Bluetooth device, Wi-Fi immediately returns to normal.

My router only supports 2.4 GHz, so I cannot test the same setup on a
5 GHz network.

I checked dmesg, journalctl and captured Bluetooth traffic using
btmon, but I couldn't find any obvious firmware crashes, driver
errors, or HCI errors while reproducing the issue.

Is this a known limitation of the current MT7902 support, or does it
sound like a driver issue? I'd be happy to provide additional logs or
test patches if needed.

Thanks.

^ permalink raw reply

* Re: [PATCH ath-next] wifi: ath12k: reset REOQ LUT addresses before firmware stop
From: Jeff Johnson @ 2026-06-26 22:29 UTC (permalink / raw)
  To: Tamizh Raja, Aishwarya R; +Cc: ath12k, linux-wireless, P Praneesh
In-Reply-To: <CABkEBKYmknpO09Yc_hM0XM2ck3F8zjmgCuyBo96usXtgkGeo_g@mail.gmail.com>

On 6/22/2026 2:10 AM, Tamizh Raja wrote:
> Reviewed-by:Tamizh Chelvam Raja <tamizh.raja@oss.qualcomm.com>

Reformatting so that 'b4' picks it up...
Reviewed-by: Tamizh Chelvam Raja <tamizh.raja@oss.qualcomm.com>


^ permalink raw reply

* Re: WARNING: at rate_control_rate_init, CPU: syz.NUM.NUM/NUM
From: Johannes Berg @ 2026-06-26 21:35 UTC (permalink / raw)
  To: sanan.hasanou, linux-wireless, linux-kernel; +Cc: syzkaller, contact
In-Reply-To: <6a3eedeb.87857ece.10923b.5b31@mx.google.com>

Dear people who run modified versions of syzkaller,

> Kernel Branch: 7.0-rc1

It might be a good idea not to use an old version.

> ------------[ cut here ]------------
> WARNING: at rate_control_rate_init+0x3a4/0x420 net/mac80211/rate.c:53, CPU#0: syz.7.23087/97240

and it might also be a good idea to check if syzbot already reported it:
https://syzkaller.appspot.com/bug?extid=9bdc0c5998ab45b05030

(FWIW, I'm not really entirely sure of the bisect)

Thanks for reducing (future) noise,
johannes

^ permalink raw reply

* WARNING: at rate_control_rate_init, CPU: syz.NUM.NUM/NUM
From: sanan.hasanou @ 2026-06-26 21:23 UTC (permalink / raw)
  To: johannes, linux-wireless, linux-kernel; +Cc: syzkaller, contact

Good day, dear maintainers,

We found a bug using a modified version of syzkaller.

Kernel Branch: 7.0-rc1
Kernel Config: <https://drive.google.com/open?id=173DLEAEPKPhhR1TcqofdnkLpdoK7PMFl>
Unfortunately, we don't have any reproducer for this bug yet.
Thank you!

Best regards,
Sanan Hasanov

------------[ cut here ]------------
WARNING: at rate_control_rate_init+0x3a4/0x420 net/mac80211/rate.c:53, CPU#0: syz.7.23087/97240
Modules linked in:
CPU: 0 UID: 0 PID: 97240 Comm: syz.7.23087 Tainted: G             L      7.0.0-rc1 #1 PREEMPT(full) 
Tainted: [L]=SOFTLOCKUP
Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
RIP: 0010:rate_control_rate_init+0x3a4/0x420 net/mac80211/rate.c:53
Code: 25 f7 f0 80 8b 0a 01 00 00 20 48 83 c4 18 5b 41 5c 41 5d 41 5e 41 5f 5d c3 e8 f8 ad b9 f6 90 0f 0b 90 eb e6 e8 ed ad b9 f6 90 <0f> 0b 90 e9 f3 fe ff ff 89 e9 80 e1 07 fe c1 38 c1 0f 8c 80 fd ff
RSP: 0018:ffffc9000238efb8 EFLAGS: 00010283
RAX: ffffffff8b08a3e3 RBX: ffff888046d9c050 RCX: 0000000000080000
RDX: ffffc9001071a000 RSI: 00000000000015af RDI: 00000000000015b0
RBP: 0000000000000000 R08: ffff888046d9c10f R09: 1ffff11008db3821
R10: dffffc0000000000 R11: ffffed1008db3822 R12: 0000000000000000
R13: ffff888041e30a00 R14: ffff88804121e660 R15: ffff888037ebe7e0
FS:  00007f10205186c0(0000) GS:ffff8880d98df000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f101f78beb8 CR3: 000000001d065000 CR4: 00000000000006f0
Call Trace:
 <TASK>
 rate_control_rate_init_all_links+0x52/0x380 net/mac80211/rate.c:86
 sta_apply_auth_flags+0x1c7/0x400 net/mac80211/cfg.c:1967
 sta_apply_parameters+0xe9a/0x1600 net/mac80211/cfg.c:2354
 ieee80211_add_station+0x38e/0x5d0 net/mac80211/cfg.c:2420
 rdev_add_station+0xa7/0x170 net/wireless/rdev-ops.h:201
 nl80211_new_station+0x1878/0x1cb0 net/wireless/nl80211.c:8904
 genl_family_rcv_msg_doit+0x20d/0x2f0 net/netlink/genetlink.c:1114
 genl_family_rcv_msg net/netlink/genetlink.c:1194 [inline]
 genl_rcv_msg+0x607/0x790 net/netlink/genetlink.c:1209
 netlink_rcv_skb+0x206/0x460 net/netlink/af_netlink.c:2550
 genl_rcv+0x2d/0x40 net/netlink/genetlink.c:1218
 netlink_unicast_kernel net/netlink/af_netlink.c:1318 [inline]
 netlink_unicast+0x930/0xae0 net/netlink/af_netlink.c:1344
 netlink_sendmsg+0x7e0/0xb10 net/netlink/af_netlink.c:1894
 sock_sendmsg_nosec net/socket.c:727 [inline]
 __sock_sendmsg net/socket.c:742 [inline]
 ____sys_sendmsg+0x603/0xa20 net/socket.c:2592
 ___sys_sendmsg+0x224/0x2a0 net/socket.c:2646
 __sys_sendmsg net/socket.c:2678 [inline]
 __do_sys_sendmsg net/socket.c:2683 [inline]
 __se_sys_sendmsg net/socket.c:2681 [inline]
 __x64_sys_sendmsg+0x1a0/0x260 net/socket.c:2681
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x19a/0x7b0 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x4b/0x53
RIP: 0033:0x7f101f5a3b6d
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 b0 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f1020518018 EFLAGS: 00000246 ORIG_RAX: 000000000000002e
RAX: ffffffffffffffda RBX: 00007f101f815fa0 RCX: 00007f101f5a3b6d
RDX: 0000000000000000 RSI: 0000200000001080 RDI: 0000000000000005
RBP: 00007f101f647c3e R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f101f816038 R14: 00007f101f815fa0 R15: 00007ffcdf1409d0
 </TASK>

<<<<<<<<<<<<<<< tail report >>>>>>>>>>>>>>>

^ permalink raw reply

* Re: [PATCH v8 02/14] firmware: qcom_scm: Migrate to generic PAS service
From: Julian Braha @ 2026-06-26 17:05 UTC (permalink / raw)
  To: Sumit Garg, andersson
  Cc: linux-arm-msm, dri-devel, freedreno, linux-media, netdev,
	linux-wireless, ath12k, linux-remoteproc, konradybcio, robh,
	krzk+dt, conor+dt, robin.clark, sean, akhilpo, lumag,
	abhinav.kumar, jesszhan0024, marijn.suijten, airlied, simona,
	vikash.garodia, bod, mchehab, elder, andrew+netdev, davem,
	edumazet, kuba, pabeni, jjohnson, mathieu.poirier,
	trilokkumar.soni, mukesh.ojha, pavan.kondeti, jorge.ramirez,
	tonyh, vignesh.viswanathan, srinivas.kandagatla, amirreza.zarrabi,
	jens.wiklander, op-tee, apurupa, skare, linux-kernel, Sumit Garg,
	Harshal Dev
In-Reply-To: <20260626133440.692849-3-sumit.garg@kernel.org>

Hi Sumit,

On 6/26/26 14:34, Sumit Garg wrote:

>  config QCOM_SCM
> +	tristate "Qualcomm PAS SCM interface driver"
> +	select QCOM_PAS
>  	select QCOM_TZMEM
> -	tristate
I think QCOM_SCM is missing a 'select IRQ_DOMAIN'. Right now I get a
build error without it:

drivers/firmware/qcom/qcom_scm.c: In function ‘qcom_scm_get_waitq_irq’:
  drivers/firmware/qcom/qcom_scm.c:2512:16: error: implicit declaration
of function ‘irq_create_fwspec_mapping’; did you mean
‘irq_create_of_mapping’? [-Wimplicit-function-declaration]
   2512 |         return irq_create_fwspec_mapping(&fwspec);
        |                ^~~~~~~~~~~~~~~~~~~~~~~~~
        |                irq_create_of_mapping

- Julian Braha

^ permalink raw reply

* [PATCH 1/1] wifi: mac80211: free ack status frame on TX header build failure
From: Ren Wei @ 2026-06-26 16:58 UTC (permalink / raw)
  To: linux-wireless
  Cc: johannes, linville, yuantan098, yifanwucs, tomapufckgml, bird,
	roxy520tt, n05ec
In-Reply-To: <cover.1782462409.git.roxy520tt@gmail.com>

From: Zhiling Zou <roxy520tt@gmail.com>

ieee80211_build_hdr() stores an ACK status frame before it has
finished all validation and header construction. If a later error path
is taken, the transmit skb is freed but the stored ACK status frame
remains in local->ack_status_frames.

This can happen for control port frames when the requested MLO link ID
does not match the link selected for a non-MLO station. Repeated
failures can fill the ACK status IDR and leave pending ACK frames until
hardware teardown.

Remove any stored ACK status frame before returning an error after it
has been inserted into the IDR.

Fixes: a729cff8ad51 ("mac80211: implement wifi TX status")
Cc: stable@vger.kernel.org
Reported-by: Yuan Tan <yuantan098@gmail.com>
Reported-by: Yifan Wu <yifanwucs@gmail.com>
Reported-by: Juefei Pu <tomapufckgml@gmail.com>
Reported-by: Xin Liu <bird@lzu.edu.cn>
Assisted-by: Codex:gpt-5.4
Signed-off-by: Zhiling Zou <roxy520tt@gmail.com>
Signed-off-by: Ren Wei <n05ec@lzu.edu.cn>
---
 net/mac80211/tx.c | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index ea7f63e1fc17..8e7f43ee10e5 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -2597,6 +2597,18 @@ static u16 ieee80211_store_ack_skb(struct ieee80211_local *local,
 	return info_id;
 }
 
+static void ieee80211_remove_ack_skb(struct ieee80211_local *local, u16 info_id)
+{
+	struct sk_buff *ack_skb;
+	unsigned long flags;
+
+	spin_lock_irqsave(&local->ack_status_lock, flags);
+	ack_skb = idr_remove(&local->ack_status_frames, info_id);
+	spin_unlock_irqrestore(&local->ack_status_lock, flags);
+
+	kfree_skb(ack_skb);
+}
+
 /**
  * ieee80211_build_hdr - build 802.11 header in the given frame
  * @sdata: virtual interface to build the header for
@@ -2972,7 +2984,8 @@ static struct sk_buff *ieee80211_build_hdr(struct ieee80211_sub_if_data *sdata,
 		if (ieee80211_skb_resize(sdata, skb, head_need, ENCRYPT_DATA)) {
 			ieee80211_free_txskb(&local->hw, skb);
 			skb = NULL;
-			return ERR_PTR(-ENOMEM);
+			ret = -ENOMEM;
+			goto free;
 		}
 	}
 
@@ -3040,6 +3053,8 @@ static struct sk_buff *ieee80211_build_hdr(struct ieee80211_sub_if_data *sdata,
 
 	return skb;
  free:
+	if (info_id)
+		ieee80211_remove_ack_skb(local, info_id);
 	kfree_skb(skb);
 	return ERR_PTR(ret);
 }
-- 
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