* Re: [PATCH ath-next v3] wifi: ath12k: avoid setting 320MHz support on non 6GHz band
From: Rameshkumar Sundaram @ 2026-06-26 4:59 UTC (permalink / raw)
To: Nicolas Escande, ath12k; +Cc: linux-wireless
In-Reply-To: <20260623151613.72113-1-nico.escande@gmail.com>
On 6/23/2026 8:46 PM, Nicolas Escande wrote:
> On a split phy qcn9274 (2.4GHz + 5GHz low), "iw phy" reports 320MHz
> related features on the 5GHz band while it should not:
>
> Wiphy phy1
> [...]
> Band 2:
> [...]
> EHT Iftypes: managed
> [...]
> EHT PHY Capabilities: (0xe2ffdbe018778000):
> 320MHz in 6GHz Supported
> [...]
> Beamformee SS (320MHz): 7
> [...]
> Number Of Sounding Dimensions (320MHz): 3
> [...]
> EHT MCS/NSS: (0x22222222222222222200000000):
>
> This is also reflected in the beacons sent by a mesh interface started on
> that band. They erroneously advertise 320MHz support too.
>
> This should not happen as IEEE Std 802.11-2024, subclause 9.4.2.323.3 says
> we should not set the 320MHz related fields when not operating on a 6GHz
> band. For example it says about Bit 0 "Support For 320 MHz In 6 GHz"
>
> "Reserved if the EHT Capabilities element is indicating capabilities for
> the 2.4 GHz or 5 GHz bands."
>
> Fix this by clearing the related bits when converting from WMI eht phy
> capabilities to mac80211 phy capabilities, for bands other than 6GHz.
>
> Tested-on: QCN9274 hw2.0 PCI WLAN.WBE.1.3.1-00218-QCAHKSWPL_SILICONZ-1
>
> Signed-off-by: Nicolas Escande <nico.escande@gmail.com>
Reviewed-by: Rameshkumar Sundaram <rameshkumar.sundaram@oss.qualcomm.com>
^ permalink raw reply
* [PATCHv2 ath-next] wifi: ath9k: return ath_buf to pool on A-MPDU subframe retry
From: Rosen Penev @ 2026-06-26 4:10 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, open list
When an A-MPDU subframe needs retransmission, its ath_buf descriptor was
moved to a local bf_head list that went out of scope without returning
the buffer to the free pool (sc->tx.txbuf). This progressively depletes
the 512-entry TX buffer pool under normal retransmission conditions,
eventually stalling all TX.
Unmap the DMA mapping (a new one will be created on retry), clear the
buffer references including fi->bf to prevent reuse of the freed
descriptor on retry, and return it to the pool via ath_tx_return_buffer.
Assisted-by: opencode:big-pickle
Signed-off-by: Rosen Penev <rosenp@gmail.com>
---
v2: set fi-bf to NULL.
drivers/net/wireless/ath/ath9k/xmit.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/drivers/net/wireless/ath/ath9k/xmit.c b/drivers/net/wireless/ath/ath9k/xmit.c
index 3334f570be50..31187753a819 100644
--- a/drivers/net/wireless/ath/ath9k/xmit.c
+++ b/drivers/net/wireless/ath/ath9k/xmit.c
@@ -665,6 +665,16 @@ 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)) {
+ dma_unmap_single(sc->dev, bf->bf_buf_addr,
+ skb->len, DMA_TO_DEVICE);
+ bf->bf_buf_addr = 0;
+ bf->bf_mpdu = NULL;
+ fi->bf = NULL;
+ list_del(&bf->list);
+ ath_tx_return_buffer(sc, bf);
+ }
}
bf = bf_next;
--
2.54.0
^ permalink raw reply related
* [PATCHv3 ath-next] wifi: ath9k: eeprom: drop static from local pdadc and vpdTable arrays
From: Rosen Penev @ 2026-06-26 4:06 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)
Assisted-by: opencode:big-pickle
Signed-off-by: Rosen Penev <rosenp@gmail.com>
---
v3: clean up based on review
v2: add bounds checks
drivers/net/wireless/ath/ath9k/eeprom.c | 111 ++++++++++++-------
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 | 10 +-
4 files changed, 109 insertions(+), 47 deletions(-)
diff --git a/drivers/net/wireless/ath/ath9k/eeprom.c b/drivers/net/wireless/ath/ath9k/eeprom.c
index df58dc02e104..05f0a0d70a50 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);
@@ -650,6 +682,11 @@ void ath9k_hw_get_gain_boundaries_pdadcs(struct ath_hw *ah,
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..043ee31f0d14 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];
+ } 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..3ef927f422dc 100644
--- a/drivers/net/wireless/ath/ath9k/eeprom_def.c
+++ b/drivers/net/wireless/ath/ath9k/eeprom_def.c
@@ -744,8 +744,14 @@ 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];
}
@@ -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
* Re: [PATCH ath-next v2] wifi: ath12k: advertise ieee_link_id in vdev start MLO params
From: Baochen Qiang @ 2026-06-26 1:43 UTC (permalink / raw)
To: Manish Dharanenthiran, ath12k
Cc: linux-wireless, Hari Naraayana Desikan Kannan, Karthik M
In-Reply-To: <20260623-ieee_link_id-v2-1-8a89d71baf58@oss.qualcomm.com>
On 6/23/2026 1:46 PM, Manish Dharanenthiran wrote:
> Firmware builds the AP MLD partner profile from the hw_link_id passed in
> the vdev start parameters. However, hw_link_id is not always the same as
> the logical per-MLD ieee_link_id, since ieee_link_id is assigned per MLD
> and not per pdev.
>
> This matters in mixed MLO and SLO setups. For example:
>
> MLD 1 - 5 GHz + 6 GHz (2-link MLO): ieee_link_id 0 and 1
> MLD 2 - 6 GHz only (1-link SLO): ieee_link_id 0
> MLD 3 - 5 GHz only (1-link SLO): ieee_link_id 0
>
> The same physical 6 GHz radio can use ieee_link_id 1 for one
> MLD and ieee_link_id 0 for another. Pass the correct ieee_link_id to
> firmware so it can build accurate per-STA profile elements.
>
> Add ieee_link_id to wmi_vdev_start_mlo_params for the self link and to
> wmi_partner_link_info for each partner link. Populate these fields in
> ath12k_mac_mlo_get_vdev_args() from the corresponding vdev link_id
> before encoding the WMI command.
>
> Introduce two new flags in ML params to indicate to firmware when
> the new fields are valid:
>
> ATH12K_WMI_FLAG_MLO_IEEE_LINK_IDX_VALID BIT(18) for the self link
> ATH12K_WMI_FLAG_MLO_IEEE_LINK_IDX_VALID_PARTNER BIT(19) for partner links
>
> Firmware parses ieee_link_id only when the matching flag is set.
>
> Also fix the debug message by using correct format specifiers and host-endian
> values instead of __le32 values.
>
> Tested-on: QCN9274 hw2.0 PCI WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1
>
> Co-developed-by: Hari Naraayana Desikan Kannan <hari.kannan@oss.qualcomm.com>
> Signed-off-by: Hari Naraayana Desikan Kannan <hari.kannan@oss.qualcomm.com>
> Co-developed-by: Karthik M <karthik.m@oss.qualcomm.com>
> Signed-off-by: Karthik M <karthik.m@oss.qualcomm.com>
> Signed-off-by: Manish Dharanenthiran <manish.dharanenthiran@oss.qualcomm.com>
Reviewed-by: Baochen Qiang <baochen.qiang@oss.qualcomm.com>
^ permalink raw reply
* [PATCH v4] wifi: ath6kl: fix OOB access from firmware ADDBA window size
From: Tristan Madani @ 2026-06-26 0:04 UTC (permalink / raw)
To: linux-wireless, linux-kernel
Cc: Vasanthakumar Thiagarajan, Jeff Johnson, Johannes Berg,
Tristan Madani
aggr_recv_addba_req_evt() logs a debug message when the firmware-supplied
win_sz is outside [AGGR_WIN_SZ_MIN, AGGR_WIN_SZ_MAX] but does not
return. The out-of-range win_sz is then used in TID_WINDOW_SZ() to
compute a kzalloc size and stored in rxtid->hold_q_sz, leading to
zero-size or overflowed allocations and subsequent out-of-bounds access.
Clean up any previously active aggregation session for the TID first,
then return early when win_sz is out of the valid range, instead of
proceeding with a broken allocation size.
Fixes: bdcd81707973 ("Add ath6kl cleaned up driver")
Cc: stable@vger.kernel.org
Suggested-by: Vasanthakumar Thiagarajan <vasanthakumar.thiagarajan@oss.qualcomm.com>
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
Changes in v4:
- Move aggregation session cleanup before the window size check so
that a previously active session is always torn down, even when the
firmware sends an ADDBA event with an out-of-range window size
(Vasanthakumar Thiagarajan).
Changes in v3:
- Regenerated from wireless-next with proper git format-patch to
produce valid index hashes (v2 had post-processed index lines).
Changes in v2:
- No code changes from v1.
drivers/net/wireless/ath/ath6kl/txrx.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/ath/ath6kl/txrx.c b/drivers/net/wireless/ath/ath6kl/txrx.c
index 80e66ac..a39c815 100644
--- a/drivers/net/wireless/ath/ath6kl/txrx.c
+++ b/drivers/net/wireless/ath/ath6kl/txrx.c
@@ -1722,13 +1722,15 @@ void aggr_recv_addba_req_evt(struct ath6kl_vif *vif, u8 tid_mux, u16 seq_no,
rxtid = &aggr_conn->rx_tid[tid];
- if (win_sz < AGGR_WIN_SZ_MIN || win_sz > AGGR_WIN_SZ_MAX)
- ath6kl_dbg(ATH6KL_DBG_WLAN_RX, "%s: win_sz %d, tid %d\n",
- __func__, win_sz, tid);
-
if (rxtid->aggr)
aggr_delete_tid_state(aggr_conn, tid);
+ if (win_sz < AGGR_WIN_SZ_MIN || win_sz > AGGR_WIN_SZ_MAX) {
+ ath6kl_dbg(ATH6KL_DBG_WLAN_RX, "%s: win_sz %d, tid %d\n",
+ __func__, win_sz, tid);
+ return;
+ }
+
rxtid->seq_next = seq_no;
hold_q_size = TID_WINDOW_SZ(win_sz) * sizeof(struct skb_hold_q);
rxtid->hold_q = kzalloc(hold_q_size, GFP_KERNEL);
--
2.47.3
^ permalink raw reply related
* Re: [PATCH] wifi: carl9170: clamp command response copy to the read buffer size
From: Tristan Madani @ 2026-06-25 23:46 UTC (permalink / raw)
To: Christian Lamparter
Cc: Doruk Tan Ozturk, Johannes Berg, Jeff Johnson, linux-wireless,
linux-kernel, stable, tristan
Hi Christian, Doruk,
No worries at all, these things happen. Both patches address the same
underlying issue from different angles -- mine returns early after
carl9170_restart() to skip the invalid response entirely, Doruk's
clamps the memcpy to ar->readlen.
Either approach works. If it helps, I'm happy to respin my 3-patch
series (the other two patches fix the TX status off-by-two and
rx_stream failover overflow in the same driver).
Let me know if you'd prefer a fresh resend or if you'd rather pick
from what's already on the list.
Thanks,
Tristan
^ permalink raw reply
* [PATCH wireless v4 3/3] wifi: ath6kl: fix OOB read from firmware num_msg in TX complete handler
From: Tristan Madani @ 2026-06-25 23:29 UTC (permalink / raw)
To: Jeff Johnson, Kalle Valo
Cc: Johannes Berg, linux-wireless, linux-kernel, tristan
From: Tristan Madani <tristan@talencesecurity.com>
The firmware-controlled num_msg field (u8, 0-255) drives the loop in
ath6kl_wmi_tx_complete_event_rx() without validation against the buffer
length. This allows out-of-bounds reads of up to 1020 bytes past the
WMI event buffer when the firmware sends an inflated num_msg.
Add a check that the buffer is large enough to hold the fixed struct
and the num_msg variable-length entries.
Fixes: bdcd81707973 ("Add ath6kl cleaned up driver")
Signed-off-by: Tristan Madani <tristan@talencesecurity.com>
---
Changes in v4:
- Split combined bounds check into two sequential checks to avoid
overreading evt->num_msg in the error log when the buffer is too
small for the fixed struct (Jeff Johnson).
Changes in v3:
- Regenerated from wireless-next with proper git format-patch to
produce valid index hashes (v2 had post-processed index lines).
Changes in v2:
- No code changes from v1.
drivers/net/wireless/ath/ath6kl/wmi.c | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/drivers/net/wireless/ath/ath6kl/wmi.c b/drivers/net/wireless/ath/ath6kl/wmi.c
index 3787b9f..a572952 100644
--- a/drivers/net/wireless/ath/ath6kl/wmi.c
+++ b/drivers/net/wireless/ath/ath6kl/wmi.c
@@ -484,6 +484,18 @@ static int ath6kl_wmi_tx_complete_event_rx(u8 *datap, int len)
evt = (struct wmi_tx_complete_event *) datap;
+ if (len < sizeof(*evt)) {
+ ath6kl_dbg(ATH6KL_DBG_WMI, "tx complete: invalid len %d\n",
+ len);
+ return -EINVAL;
+ }
+
+ if (len < sizeof(*evt) + evt->num_msg * sizeof(struct tx_complete_msg_v1)) {
+ ath6kl_dbg(ATH6KL_DBG_WMI, "tx complete: invalid len %d for %u msgs\n",
+ len, evt->num_msg);
+ return -EINVAL;
+ }
+
ath6kl_dbg(ATH6KL_DBG_WMI, "comp: %d %d %d\n",
evt->num_msg, evt->msg_len, evt->msg_type);
--
2.47.3
^ permalink raw reply related
* Re: [PATCH 2/6] remoteproc: qcom: Add M0 BTSS secure PIL driver
From: George Moussalem @ 2026-06-25 14:24 UTC (permalink / raw)
To: Philipp Zabel, 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, Bjorn Andersson,
Konrad Dybcio, Mathieu Poirier
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc
In-Reply-To: <439f76c3fcafdfb91cca426fcae17ef776776eab.camel@pengutronix.de>
Thanks, that was quick!
On 6/25/26 18:18, Philipp Zabel wrote:
> On Do, 2026-06-25 at 18:10 +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.
>>
>> 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/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;
>
> Why is this stored here? It doesn't seem to be used.
will remove it and use devm_reset_control_get_exclusive_deasserted as
suggested below.
>
> [...]
>> +static int m0_btss_pil_probe(struct platform_device *pdev)
>> +{
>> + // struct reset_control *btss_reset;
>
> It looks like this shouldn't be commented out.
>
>> + 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");
>
> Please use devm_reset_control_get_exclusive() directly.
>
>> + 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");
>
> Shouldn't this be asserted on remove? Otherwise after an unbind/bind
> cycle probe will enable the clock with reset already deasserted.
> That may or may not be problematic.
>
> Consider using devm_reset_control_get_exclusive_deasserted().
>
>
> regards
> Philipp
Regards,
George
^ permalink raw reply
* Re: [PATCH 2/6] remoteproc: qcom: Add M0 BTSS secure PIL driver
From: Philipp Zabel @ 2026-06-25 14:18 UTC (permalink / raw)
To: george.moussalem, 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, Bjorn Andersson,
Konrad Dybcio, Mathieu Poirier
Cc: 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 Do, 2026-06-25 at 18:10 +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.
>
> 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/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;
Why is this stored here? It doesn't seem to be used.
[...]
> +static int m0_btss_pil_probe(struct platform_device *pdev)
> +{
> + // struct reset_control *btss_reset;
It looks like this shouldn't be commented out.
> + 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");
Please use devm_reset_control_get_exclusive() directly.
> + 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");
Shouldn't this be asserted on remove? Otherwise after an unbind/bind
cycle probe will enable the clock with reset already deasserted.
That may or may not be problematic.
Consider using devm_reset_control_get_exclusive_deasserted().
regards
Philipp
^ permalink raw reply
* Re: [PATCH] wifi: cfg80211: replace BOOL_TO_STR macro with str_true_false()
From: serhat @ 2026-06-25 14:16 UTC (permalink / raw)
To: Johannes Berg; +Cc: linux-wireless, linux-kernel
In-Reply-To: <d1db2391fe528c4e79b1afd9c80e49f44b948beb.camel@sipsolutions.net>
Hi Johannes,
You are right. Replacing the macro breaks libtraceevent parsing in
trace-cmd, since it cannot evaluate the kernel function.
Please drop this patch. Thanks for catching this.
Best regards,
Serhat
Johannes Berg <johannes@sipsolutions.net>, 25 Haz 2026 Per, 14:22
tarihinde şunu yazdı:
>
> On Wed, 2026-06-24 at 23:49 +0300, Serhat Kumral wrote:
> > Remove the local BOOL_TO_STR macro and replace all its usages with
> > the kernel's str_true_false() helper from <linux/string_choices.h>.
> >
> > No functional change intended.
> >
>
> I believe this breaks trace-cmd reporting. Please check and resend
> indicating that you have.
>
> johannes
^ permalink raw reply
* [PATCH 5/6] Bluetooth: Introduce Qualcomm IPQ5018 IPC based HCI driver
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
From: George Moussalem <george.moussalem@outlook.com>
Add driver support for the Qualcomm IPQ5018 bluetooth chip.
The firmware runs on the M0 co-processor.
The host and the M0 core use a shared memory carveout for transport
using ring buffers. This driver implements the transport layer between
the HCI core and the Bluetooth subsystem running on the M0 core.
Notifications of host and M0 core events are triggered by an IPC
register BIT and an interrupt line respectfully.
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
drivers/bluetooth/Kconfig | 11 +
drivers/bluetooth/Makefile | 1 +
drivers/bluetooth/btqcomipc.c | 939 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 951 insertions(+)
diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig
index 4e8c24d757e9..6b8bed6a6ffd 100644
--- a/drivers/bluetooth/Kconfig
+++ b/drivers/bluetooth/Kconfig
@@ -413,6 +413,17 @@ config BT_MTKUART
Say Y here to compile support for MediaTek Bluetooth UART devices
into the kernel or say M to compile it as module (btmtkuart).
+config BT_QCOMIPC
+ tristate "Qualcomm IPQ5018 IPC based HCI support"
+ select BT_QCA
+ help
+ Qualcomm IPQ5018 IPC based HCI driver.
+ This driver is used to bridge HCI data onto shared memory between
+ the host and the M0 BTSS core.
+
+ Say Y here to compile support for HCI over Qualcomm IPC into the
+ kernel or say M to compile as a module.
+
config BT_QCOMSMD
tristate "Qualcomm SMD based HCI support"
depends on RPMSG || (COMPILE_TEST && RPMSG=n)
diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile
index e6b1c1180d1d..05f19047bed0 100644
--- a/drivers/bluetooth/Makefile
+++ b/drivers/bluetooth/Makefile
@@ -20,6 +20,7 @@ obj-$(CONFIG_BT_MRVL) += btmrvl.o
obj-$(CONFIG_BT_MRVL_SDIO) += btmrvl_sdio.o
obj-$(CONFIG_BT_MTKSDIO) += btmtksdio.o
obj-$(CONFIG_BT_MTKUART) += btmtkuart.o
+obj-$(CONFIG_BT_QCOMIPC) += btqcomipc.o
obj-$(CONFIG_BT_QCOMSMD) += btqcomsmd.o
obj-$(CONFIG_BT_BCM) += btbcm.o
obj-$(CONFIG_BT_RTL) += btrtl.o
diff --git a/drivers/bluetooth/btqcomipc.c b/drivers/bluetooth/btqcomipc.c
new file mode 100644
index 000000000000..662a75b6c4a9
--- /dev/null
+++ b/drivers/bluetooth/btqcomipc.c
@@ -0,0 +1,939 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2020 The Linux Foundation. All rights reserved.
+ */
+
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/firmware/qcom/qcom_scm.h>
+#include <linux/init.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/kernel.h>
+#include <linux/mfd/syscon.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_irq.h>
+#include <linux/of_reserved_mem.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/remoteproc.h>
+#include <linux/skbuff.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#include <net/bluetooth/bluetooth.h>
+#include <net/bluetooth/hci_core.h>
+
+#include "btqca.h"
+
+/** Message header format.
+ *
+ * ----------------------------------------------------------------
+ * BitPos | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 - 0 |
+ * ---------------------------------------------------------------
+ * Field | long_msg |ACK | RFU | pkt_type | cmd |
+ * ----------------------------------------------------------------
+ *
+ * - long_msg : If set, indicates that the payload is larger than the
+ * IPC_MSG_PLD_SZ. The payload instead contains a pointer to the
+ * long message buffer in the shared BTSS memory space.
+ *
+ * - ACK : Set if sending ACK if required by sending acknowledegement
+ * to sender i.e. send an ack IPC interrupt if set.
+ *
+ * - RFU : Reserved for future use.
+ *
+ * - pkt_type : IPC Packet Type
+ *
+ * - cmd : Contains unique command ID
+ */
+
+#define IPC_MSG_HDR_SZ 4
+#define IPC_MSG_PLD_SZ 40
+#define IPC_TOTAL_MSG_SZ (IPC_MSG_HDR_SZ + IPC_MSG_PLD_SZ)
+
+/* Message Header */
+#define IPC_HDR_LONG_MSG BIT(15)
+#define IPC_HDR_REQ_ACK BIT(14)
+#define IPC_HDR_PKT_TYPE_MASK GENMASK(9, 8)
+#define IPC_HDR_PKT_TYPE_CUST 0
+#define IPC_HDR_PKT_TYPE_HCI 1
+#define IPC_HDR_PKT_TYPE_AUDIO 2
+#define IPC_HDR_PKT_TYPE_RFU 3
+#define IPC_HDR_CMD_MASK GENMASK(7, 0)
+
+#define IPC_CMD_STOP 1
+#define IPC_CMD_SWITCH_TO_UART 2
+#define IPC_CMD_PREPARE_DUMP 3
+#define IPC_CMD_COLLECT_DUMP 4
+#define IPC_CMD_START 5
+
+#define IPC_TX_QSIZE 32
+
+#define TO_APPS_ADDR(a) (desc->mem_region + (int)(uintptr_t)a)
+#define TO_BT_ADDR(a) (a - desc->mem_region)
+#define IPC_LBUF_SZ(w, x, y, z) (((TO_BT_ADDR((void *)w) + w->x) - w->y) / w->z)
+
+#define GET_NO_OF_BLOCKS(a, b) ((a + b - 1) / b)
+
+#define GET_RX_INDEX_FROM_BUF(x, y) ((x - desc->rx_ctxt->lring_buf) / y)
+
+#define GET_TX_INDEX_FROM_BUF(x, y) ((x - desc->tx_ctxt->lring_buf) / y)
+
+#define IS_RX_MEM_NON_CONTIGIOUS(buf, len, sz) \
+ ((buf + len) > (desc->rx_ctxt->lring_buf + \
+ (sz * desc->rx_ctxt->lmsg_buf_cnt)))
+
+#define POWER_CONTROL_DELAY_MS 50
+
+#define BTSS_PAS_ID 0xc
+
+struct long_msg_info {
+ __le16 smsg_free_cnt;
+ __le16 lmsg_free_cnt;
+ u8 ridx;
+ u8 widx;
+} __packed;
+
+struct ipc_aux_ptr {
+ __le32 len;
+ __le32 buf;
+} __packed;
+
+struct ring_buffer {
+ __le16 msg_hdr;
+ __le16 len;
+ union {
+ u8 smsg_data[IPC_MSG_PLD_SZ];
+ __le32 lmsg_data;
+ } payload;
+} __packed;
+
+struct ring_buffer_info {
+ __le32 rbuf;
+ u8 ring_buf_cnt;
+ u8 ridx;
+ u8 widx;
+ u8 tidx;
+ __le32 next;
+} __packed;
+
+struct context_info {
+ __le16 total_size;
+ u8 lmsg_buf_cnt;
+ u8 smsg_buf_cnt;
+ struct ring_buffer_info sring_buf_info;
+ __le32 sring_buf;
+ __le32 lring_buf;
+ __le32 reserved;
+} __packed;
+
+struct qcom_btss {
+ struct device *dev;
+ struct rproc *rproc;
+ struct hci_dev *hdev;
+
+ struct regmap *regmap;
+ u32 offset;
+ u32 bit;
+ int irq;
+
+ void *mem_region;
+ phys_addr_t mem_phys;
+ phys_addr_t mem_reloc;
+ size_t mem_size;
+
+ struct sk_buff_head tx_q;
+ struct workqueue_struct *wq;
+ struct work_struct work;
+ wait_queue_head_t wait_q;
+ spinlock_t lock;
+
+ struct context_info *tx_ctxt;
+ struct context_info *rx_ctxt;
+ struct long_msg_info lmsg_ctxt;
+
+ bool running;
+};
+
+static void btqcomipc_update_stats(struct hci_dev *hdev, struct sk_buff *skb);
+
+static void *btss_alloc_lmsg(struct qcom_btss *desc, u32 len,
+ struct ipc_aux_ptr *aux_ptr, bool *is_lbuf_full)
+{
+ struct device *dev = desc->dev;
+ u8 idx, blks, blks_consumed;
+ void *ret_ptr;
+ u32 lsz;
+
+ if (desc->tx_ctxt->lring_buf == 0) {
+ dev_err(dev, "no long message buffer initialized\n");
+ return ERR_PTR(-ENODEV);
+ }
+
+ lsz = IPC_LBUF_SZ(desc->tx_ctxt, total_size, lring_buf, lmsg_buf_cnt);
+ blks = GET_NO_OF_BLOCKS(len, lsz);
+
+ if (!desc->lmsg_ctxt.lmsg_free_cnt ||
+ (blks > desc->lmsg_ctxt.lmsg_free_cnt))
+ return ERR_PTR(-EAGAIN);
+
+ idx = desc->lmsg_ctxt.widx;
+
+ if ((desc->lmsg_ctxt.widx + blks) > desc->tx_ctxt->lmsg_buf_cnt) {
+ blks_consumed = desc->tx_ctxt->lmsg_buf_cnt - idx;
+ aux_ptr->len = len - (blks_consumed * lsz);
+ aux_ptr->buf = desc->tx_ctxt->lring_buf;
+ }
+
+ desc->lmsg_ctxt.widx = (desc->lmsg_ctxt.widx + blks) %
+ desc->tx_ctxt->lmsg_buf_cnt;
+
+ desc->lmsg_ctxt.lmsg_free_cnt -= blks;
+
+ if (desc->lmsg_ctxt.lmsg_free_cnt <=
+ ((desc->tx_ctxt->lmsg_buf_cnt * 20) / 100))
+ *is_lbuf_full = true;
+
+ ret_ptr = TO_APPS_ADDR(desc->tx_ctxt->lring_buf) + (idx * lsz);
+
+ return ret_ptr;
+}
+
+static struct ring_buffer_info *btss_get_tx_rbuf(struct qcom_btss *desc,
+ bool *is_sbuf_full)
+{
+ u8 idx;
+ struct ring_buffer_info *rinfo;
+
+ for (rinfo = &(desc->tx_ctxt->sring_buf_info); rinfo != NULL;
+ rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) {
+ idx = (rinfo->widx + 1) % (desc->tx_ctxt->smsg_buf_cnt);
+
+ if (idx != rinfo->tidx) {
+ desc->lmsg_ctxt.smsg_free_cnt--;
+
+ if (desc->lmsg_ctxt.smsg_free_cnt <=
+ ((desc->tx_ctxt->smsg_buf_cnt * 20) / 100))
+ *is_sbuf_full = true;
+
+ return rinfo;
+ }
+ }
+
+ return ERR_PTR(-EAGAIN);
+}
+
+static int btss_send(struct qcom_btss *desc, u16 msg_hdr,
+ struct sk_buff *skb)
+{
+ struct hci_dev *hdev = desc->hdev;
+ struct ring_buffer_info *rinfo;
+ struct ipc_aux_ptr aux_ptr;
+ struct ring_buffer *rbuf;
+ bool is_lbuf_full = false;
+ bool is_sbuf_full = false;
+ u16 hdr = msg_hdr;
+ void *ptr_buf;
+ u32 len;
+
+ /* Account for HCI packet type as it's not included in the skb payload */
+ len = skb->len + 1;
+ memset(&aux_ptr, 0, sizeof(struct ipc_aux_ptr));
+
+ if (len > IPC_MSG_PLD_SZ) {
+ hdr |= IPC_HDR_LONG_MSG;
+
+ ptr_buf = btss_alloc_lmsg(desc, len,
+ &aux_ptr, &is_lbuf_full);
+ if (IS_ERR(ptr_buf)) {
+ bt_dev_err(hdev, "long msg buf full");
+ hdev->stat.err_tx++;
+ return PTR_ERR(ptr_buf);
+ }
+ }
+
+ rinfo = btss_get_tx_rbuf(desc, &is_sbuf_full);
+ if (IS_ERR(rinfo)) {
+ bt_dev_err(hdev, "short msg buf full");
+ hdev->stat.err_tx++;
+ return PTR_ERR(rinfo);
+ }
+
+ rbuf = &((struct ring_buffer *)(TO_APPS_ADDR(rinfo->rbuf)))[rinfo->widx];
+
+ if (len > IPC_MSG_PLD_SZ)
+ rbuf->payload.lmsg_data = cpu_to_le32(TO_BT_ADDR(ptr_buf));
+ else
+ ptr_buf = rbuf->payload.smsg_data;
+
+ /* if it's a short message, the aux len and buf are NULL */
+ memcpy_toio(ptr_buf, &hci_skb_pkt_type(skb), 1);
+ memcpy_toio((u8 *)ptr_buf + 1, skb->data, skb->len - aux_ptr.len);
+ if (aux_ptr.buf) {
+ memcpy_toio(TO_APPS_ADDR(aux_ptr.buf),
+ (skb->data + (skb->len - aux_ptr.len)), aux_ptr.len);
+ }
+
+ if (is_sbuf_full || is_lbuf_full)
+ hdr |= IPC_HDR_REQ_ACK;
+
+ rbuf->msg_hdr = cpu_to_le16(hdr);
+ rbuf->len = cpu_to_le16(len);
+
+ rinfo->widx = (rinfo->widx + 1) % desc->tx_ctxt->smsg_buf_cnt;
+
+ regmap_set_bits(desc->regmap, desc->offset, BIT(desc->bit));
+
+ return 0;
+}
+
+static void btss_process_tx_queue(struct qcom_btss *desc)
+{
+ struct sk_buff *skb;
+ u16 hdr;
+ int ret;
+
+ while ((skb = skb_dequeue(&desc->tx_q))) {
+ hdr = FIELD_PREP(IPC_HDR_PKT_TYPE_MASK, IPC_HDR_PKT_TYPE_HCI);
+
+ ret = btss_send(desc, hdr, skb);
+ if (ret) {
+ bt_dev_err(desc->hdev, "Failed to send message");
+ skb_queue_head(&desc->tx_q, skb);
+ break;
+ }
+
+ btqcomipc_update_stats(desc->hdev, skb);
+ kfree_skb(skb);
+ }
+}
+
+static void btss_free_lmsg(struct qcom_btss *desc, u32 lmsg, u16 len)
+{
+ u8 idx;
+ u8 blks;
+ u32 lsz = IPC_LBUF_SZ(desc->tx_ctxt, total_size, lring_buf,
+ lmsg_buf_cnt);
+
+ idx = GET_TX_INDEX_FROM_BUF(lmsg, lsz);
+
+ if (idx != desc->lmsg_ctxt.ridx)
+ return;
+
+ blks = GET_NO_OF_BLOCKS(len, lsz);
+
+ desc->lmsg_ctxt.ridx = (desc->lmsg_ctxt.ridx + blks) %
+ desc->tx_ctxt->lmsg_buf_cnt;
+
+ desc->lmsg_ctxt.lmsg_free_cnt += blks;
+}
+
+static int btss_send_ctrl(struct qcom_btss *desc, u16 msg_hdr)
+{
+ struct ring_buffer_info *rinfo;
+ struct ring_buffer *rbuf;
+
+ rinfo = btss_get_tx_rbuf(desc, NULL);
+ if (IS_ERR(rinfo))
+ return PTR_ERR(rinfo);
+
+ rbuf = &((struct ring_buffer *)TO_APPS_ADDR(rinfo->rbuf))[rinfo->widx];
+ rbuf->msg_hdr = cpu_to_le16(msg_hdr);
+ rbuf->len = 0;
+
+ rinfo->widx = (rinfo->widx + 1) % desc->tx_ctxt->smsg_buf_cnt;
+
+ regmap_set_bits(desc->regmap, desc->offset, BIT(desc->bit));
+
+ return 0;
+}
+
+static void btss_recv_cust_frame(struct qcom_btss *desc, u8 cmd)
+{
+ u16 msg_hdr = 0;
+ int ret;
+
+ msg_hdr |= cmd;
+
+ switch (cmd) {
+ case IPC_CMD_STOP:
+ bt_dev_info(desc->hdev, "BTSS stopped, gracefully stopping APSS IPC");
+ break;
+ case IPC_CMD_START:
+ desc->tx_ctxt = (struct context_info *)((void *)desc->rx_ctxt +
+ le16_to_cpu(desc->rx_ctxt->total_size));
+ desc->lmsg_ctxt.widx = 0;
+ desc->lmsg_ctxt.ridx = 0;
+ desc->lmsg_ctxt.smsg_free_cnt = desc->tx_ctxt->smsg_buf_cnt;
+ desc->lmsg_ctxt.lmsg_free_cnt = desc->tx_ctxt->lmsg_buf_cnt;
+ WRITE_ONCE(desc->running, true);
+ wake_up(&desc->wait_q);
+
+ bt_dev_info(desc->hdev, "BTSS started, initializing APSS BT IPC");
+ return;
+ default:
+ bt_dev_err(desc->hdev, "Unsupported CMD ID: %u", cmd);
+ return;
+ }
+
+ if (unlikely(!READ_ONCE(desc->running))) {
+ bt_dev_err(desc->hdev, "BTSS not initialized, no message sent");
+ return;
+ }
+
+ WRITE_ONCE(desc->running, false);
+
+ ret = btss_send_ctrl(desc, msg_hdr);
+ if (ret)
+ bt_dev_err(desc->hdev, "Failed to send control message");
+}
+
+static inline int btss_recv_hci_frame(struct qcom_btss *desc, const u8 *data, size_t len)
+{
+ unsigned char pkt_type;
+ struct sk_buff *skb;
+ size_t pkt_len;
+
+ if (len < 1)
+ return -EPROTO;
+
+ pkt_type = data[0];
+
+ switch (pkt_type) {
+ case HCI_EVENT_PKT:
+ {
+ if (len < HCI_EVENT_HDR_SIZE)
+ return -EILSEQ;
+ struct hci_event_hdr *hdr = (struct hci_event_hdr *)(data + 1);
+
+ pkt_len = HCI_EVENT_HDR_SIZE + hdr->plen;
+ break;
+ }
+ case HCI_COMMAND_PKT: {
+ if (len < HCI_COMMAND_HDR_SIZE)
+ return -EILSEQ;
+ struct hci_command_hdr *hdr = (struct hci_command_hdr *)(data + 1);
+
+ pkt_len = HCI_COMMAND_HDR_SIZE + le16_to_cpu(hdr->plen);
+ break;
+ }
+ case HCI_ACLDATA_PKT:
+ {
+ if (len < HCI_ACL_HDR_SIZE)
+ return -EILSEQ;
+ struct hci_acl_hdr *hdr = (struct hci_acl_hdr *)(data + 1);
+
+ pkt_len = HCI_ACL_HDR_SIZE + le16_to_cpu(hdr->dlen);
+ break;
+ }
+ case HCI_SCODATA_PKT:
+ {
+ if (len < HCI_SCO_HDR_SIZE)
+ return -EILSEQ;
+ struct hci_sco_hdr *hdr = (struct hci_sco_hdr *)(data + 1);
+
+ pkt_len = HCI_SCO_HDR_SIZE + hdr->dlen;
+ break;
+ }
+ default:
+ return -EPROTO;
+ }
+
+ if (pkt_len > len)
+ return -EINVAL;
+
+ skb = bt_skb_alloc(pkt_len, GFP_ATOMIC);
+ if (!skb) {
+ desc->hdev->stat.err_rx++;
+ return -ENOMEM;
+ }
+
+ skb->dev = (void *)desc->hdev;
+ hci_skb_pkt_type(skb) = pkt_type;
+ skb_put_data(skb, data + 1, pkt_len);
+
+ hci_recv_frame(desc->hdev, skb);
+ desc->hdev->stat.byte_rx += pkt_len;
+
+ return 0;
+}
+
+static inline int btss_process_rx(struct qcom_btss *desc,
+ struct ring_buffer_info *rinfo,
+ bool *ack, u8 *rx_count)
+{
+ u8 ridx, lbuf_idx, blks_consumed, pkt_type, cmd;
+ struct ipc_aux_ptr aux_ptr;
+ struct ring_buffer *rbuf;
+ uint8_t *rxbuf = NULL;
+ unsigned char *buf;
+ u32 lsz;
+ int ret;
+
+ ridx = rinfo->ridx;
+
+ while (ridx != rinfo->widx) {
+ memset(&aux_ptr, 0, sizeof(struct ipc_aux_ptr));
+
+ rbuf = &((struct ring_buffer *)(TO_APPS_ADDR(rinfo->rbuf)))[ridx];
+
+ if (rbuf->msg_hdr & IPC_HDR_LONG_MSG) {
+ rxbuf = TO_APPS_ADDR(rbuf->payload.lmsg_data);
+ lsz = IPC_LBUF_SZ(desc->rx_ctxt, total_size, lring_buf,
+ lmsg_buf_cnt);
+
+ if (IS_RX_MEM_NON_CONTIGIOUS(rbuf->payload.lmsg_data,
+ rbuf->len, lsz)) {
+ lbuf_idx = GET_RX_INDEX_FROM_BUF(
+ rbuf->payload.lmsg_data, lsz);
+
+ blks_consumed = desc->rx_ctxt->lmsg_buf_cnt -
+ lbuf_idx;
+ aux_ptr.len = rbuf->len - (blks_consumed * lsz);
+ aux_ptr.buf = desc->rx_ctxt->lring_buf;
+ }
+ } else {
+ rxbuf = rbuf->payload.smsg_data;
+ }
+
+ *ack = (rbuf->msg_hdr & IPC_HDR_REQ_ACK);
+
+ pkt_type = FIELD_GET(IPC_HDR_PKT_TYPE_MASK, rbuf->msg_hdr);
+
+ switch (pkt_type) {
+ case IPC_HDR_PKT_TYPE_HCI:
+ buf = kmalloc(rbuf->len, GFP_ATOMIC);
+ if (!buf) {
+ rinfo->ridx = ridx;
+ return -ENOMEM;
+ }
+
+ memcpy_fromio(buf, rxbuf, rbuf->len - aux_ptr.len);
+
+ if (aux_ptr.buf)
+ memcpy_fromio(buf + (rbuf->len - aux_ptr.len),
+ TO_APPS_ADDR(aux_ptr.buf), aux_ptr.len);
+
+ ret = btss_recv_hci_frame(desc, buf, rbuf->len);
+ if (ret)
+ bt_dev_err(desc->hdev, "Failed to process HCI frame: %d", ret);
+ kfree(buf);
+ break;
+ case IPC_HDR_PKT_TYPE_CUST:
+ cmd = FIELD_GET(IPC_HDR_CMD_MASK, rbuf->msg_hdr);
+ btss_recv_cust_frame(desc, cmd);
+ break;
+ default:
+ break;
+ }
+
+ ridx = (ridx + 1) % rinfo->ring_buf_cnt;
+
+ if (rx_count)
+ (*rx_count)++;
+
+ rinfo->ridx = ridx;
+ }
+
+ return 0;
+}
+
+static void btss_process_ack(struct qcom_btss *desc)
+{
+ struct ring_buffer_info *rinfo;
+ struct ring_buffer *rbuf;
+ u8 tidx;
+
+ for (rinfo = &desc->tx_ctxt->sring_buf_info; rinfo != NULL;
+ rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) {
+ tidx = rinfo->tidx;
+ rbuf = (struct ring_buffer *)TO_APPS_ADDR(rinfo->rbuf);
+
+ while (tidx != rinfo->ridx) {
+ if (rbuf[tidx].msg_hdr & IPC_HDR_LONG_MSG) {
+ btss_free_lmsg(desc,
+ rbuf[tidx].payload.lmsg_data,
+ rbuf[tidx].len);
+ }
+
+ tidx = (tidx + 1) % desc->tx_ctxt->smsg_buf_cnt;
+ desc->lmsg_ctxt.smsg_free_cnt++;
+ }
+
+ rinfo->tidx = tidx;
+
+ btss_process_tx_queue(desc);
+ }
+}
+
+static void btss_worker(struct work_struct *work)
+{
+ struct qcom_btss *desc = container_of(work, struct qcom_btss, work);
+ struct ring_buffer_info *rinfo;
+ bool ack = false;
+ u32 offset;
+ int ret;
+
+ if (desc->rproc->state != RPROC_RUNNING)
+ return;
+
+ spin_lock(&desc->lock);
+
+ if (unlikely(!READ_ONCE(desc->running))) {
+ // FW sets offset of RX context info at start of memory region upon boot
+ offset = readl(desc->mem_region);
+ dev_dbg(desc->dev, "offset after M0 boot: 0x%08x\n", offset);
+ desc->rx_ctxt = (struct context_info *)(desc->mem_region + offset);
+ } else {
+ btss_process_ack(desc);
+ }
+
+ for (rinfo = &(desc->rx_ctxt->sring_buf_info);
+ rinfo != NULL;
+ rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) {
+ ret = btss_process_rx(desc, rinfo, &ack,
+ &desc->rx_ctxt->smsg_buf_cnt);
+ if (ret) {
+ bt_dev_err(desc->hdev, "Failed to process peer msgs: %d", ret);
+ goto spin_unlock;
+ }
+ }
+
+ if (ack)
+ regmap_set_bits(desc->regmap, desc->offset, BIT(desc->bit));
+
+spin_unlock:
+ spin_unlock(&desc->lock);
+}
+
+static irqreturn_t btss_irq_handler(int irq, void *data)
+{
+ struct qcom_btss *desc = data;
+
+ queue_work(desc->wq, &desc->work);
+
+ return IRQ_HANDLED;
+}
+
+static int btss_init(struct qcom_btss *desc)
+{
+ struct device *dev = desc->dev;
+ int ret;
+
+ init_waitqueue_head(&desc->wait_q);
+ spin_lock_init(&desc->lock);
+ skb_queue_head_init(&desc->tx_q);
+
+ desc->wq = create_singlethread_workqueue("btss_wq");
+ if (!desc->wq) {
+ dev_err(dev, "Failed to initialize workqueue\n");
+ return -EAGAIN;
+ }
+
+ INIT_WORK(&desc->work, btss_worker);
+
+ ret = devm_request_threaded_irq(dev, desc->irq, NULL, btss_irq_handler,
+ IRQF_TRIGGER_RISING | IRQF_ONESHOT,
+ "btss_irq", desc);
+
+ if (ret)
+ dev_err(dev, "error registering irq[%d] ret = %d\n",
+ desc->irq, ret);
+
+ return ret;
+}
+
+static void btqcomipc_update_stats(struct hci_dev *hdev, struct sk_buff *skb)
+{
+ u8 pkt_type = hci_skb_pkt_type(skb);
+
+ hdev->stat.byte_tx += skb->len;
+ switch (pkt_type) {
+ case HCI_COMMAND_PKT:
+ hdev->stat.cmd_tx++;
+ break;
+ case HCI_ACLDATA_PKT:
+ hdev->stat.acl_tx++;
+ break;
+ case HCI_SCODATA_PKT:
+ hdev->stat.sco_tx++;
+ break;
+ default:
+ break;
+ }
+}
+
+static int btqcomipc_open(struct hci_dev *hdev)
+{
+ struct qcom_btss *desc = hci_get_drvdata(hdev);
+
+ int ret;
+
+ ret = btss_init(desc);
+ if (ret) {
+ bt_dev_err(hdev, "Failed initializing BTSS: %d", ret);
+ return ret;
+ }
+
+ /* wait 2 seconds for filesystem to become available */
+ msleep(2000);
+
+ /* Boot M0 firmware */
+ ret = rproc_boot(desc->rproc);
+ if (ret) {
+ bt_dev_err(hdev, "Failed to boot M0 processor: %d", ret);
+ return ret;
+ }
+
+ msleep(POWER_CONTROL_DELAY_MS);
+ ret = wait_event_timeout(desc->wait_q, READ_ONCE(desc->running),
+ msecs_to_jiffies(1000));
+
+ if (!ret) {
+ bt_dev_err(hdev, "Timeout waiting for BTSS start");
+ return -ETIMEDOUT;
+ }
+
+ return 0;
+}
+
+static int btqcomipc_close(struct hci_dev *hdev)
+{
+ struct qcom_btss *desc = hci_get_drvdata(hdev);
+
+ rproc_shutdown(desc->rproc);
+ msleep(POWER_CONTROL_DELAY_MS);
+
+ return 0;
+}
+
+static int btqcomipc_shutdown(struct hci_dev *hdev)
+{
+ struct qcom_btss *desc = hci_get_drvdata(hdev);
+
+ WRITE_ONCE(desc->running, false);
+ if (desc->wq != NULL) {
+ disable_irq(desc->irq);
+ flush_workqueue(desc->wq);
+ devm_free_irq(desc->dev, desc->irq, desc);
+ skb_queue_purge(&desc->tx_q);
+ destroy_workqueue(desc->wq);
+ desc->wq = NULL;
+ }
+
+ return 0;
+}
+
+static int btqcomipc_setup(struct hci_dev *hdev)
+{
+ struct qca_btsoc_version ver;
+ int ret;
+
+ /*
+ * Enable controller to do both LE scan and BR/EDR inquiry
+ * simultaneously.
+ */
+ hci_set_quirk(hdev, HCI_QUIRK_SIMULTANEOUS_DISCOVERY);
+
+ /*
+ * Enable NON_PERSISTENT_SETUP QUIRK to ensure to execute
+ * setup for every hci up.
+ */
+ hci_set_quirk(hdev, HCI_QUIRK_NON_PERSISTENT_SETUP);
+ ret = qca_read_soc_version(hdev, &ver, QCA_IPQ5018);
+ if (ret)
+ return -EINVAL;
+
+ ret = qca_uart_setup(hdev, 0, QCA_IPQ5018, ver, NULL);
+ if (ret) {
+ bt_dev_err(hdev, "Failed to setup UART: %d\n", ret);
+ return ret;
+ }
+
+ bt_dev_info(hdev, "QCA Build Info: %s", hdev->fw_info);
+
+ /* Obtain and set BD address from NVMEM cell */
+ hci_set_quirk(hdev, HCI_QUIRK_USE_BDADDR_NVMEM);
+ hci_set_quirk(hdev, HCI_QUIRK_BDADDR_NVMEM_BE);
+
+ return 0;
+}
+
+static int btqcomipc_send(struct hci_dev *hdev, struct sk_buff *skb)
+{
+ u16 hdr = FIELD_PREP(IPC_HDR_PKT_TYPE_MASK, IPC_HDR_PKT_TYPE_HCI);
+ struct qcom_btss *desc = hci_get_drvdata(hdev);
+ int ret;
+
+ if (unlikely(!READ_ONCE(desc->running))) {
+ bt_dev_err(hdev, "BTSS not initialized, failed to send message");
+ ret = -ENODEV;
+ goto free_skb;
+ }
+
+ ret = btss_send(desc, hdr, skb);
+ if (ret) {
+ if (ret == -EAGAIN) {
+ if (skb_queue_len(&desc->tx_q) >= IPC_TX_QSIZE) {
+ bt_dev_err(hdev, "TX queue full, dropping message");
+ hdev->stat.err_tx++;
+ ret = -ENOBUFS;
+ } else {
+ skb_queue_tail(&desc->tx_q, skb);
+ return 0;
+ }
+ } else {
+ bt_dev_err(hdev, "Failed to send message: %d", ret);
+ hdev->stat.err_tx++;
+ }
+ }
+
+ btqcomipc_update_stats(desc->hdev, skb);
+
+free_skb:
+ kfree_skb(skb);
+
+ return ret;
+}
+
+static int btqcomipc_flush(struct hci_dev *hdev)
+{
+ struct qcom_btss *desc = hci_get_drvdata(hdev);
+
+ skb_queue_purge(&desc->tx_q);
+ return 0;
+}
+
+static int btqcomipc_alloc_memory_region(struct qcom_btss *desc)
+{
+ struct device *dev = desc->dev;
+ struct resource res;
+ int ret;
+
+ /* lookup reserved-memory region of the remoteproc node */
+ ret = of_reserved_mem_region_to_resource(desc->rproc->dev.parent->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 void btqcomipc_rproc_put(void *data)
+{
+ struct rproc *rproc = data;
+
+ rproc_put(rproc);
+}
+
+static int btqcomipc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct qcom_btss *desc;
+ phandle rproc_phandle;
+ struct hci_dev *hdev;
+ unsigned int args[2];
+ int ret;
+
+ desc = devm_kzalloc(dev, sizeof(*desc), GFP_KERNEL);
+ if (!desc)
+ return -ENOMEM;
+
+ desc->dev = dev;
+
+ if (of_property_read_u32(dev->of_node, "qcom,rproc", &rproc_phandle))
+ return dev_err_probe(dev, -ENOENT, "Failed to get remoteproc handle\n");
+
+ desc->rproc = rproc_get_by_phandle(rproc_phandle);
+ if (!desc->rproc)
+ return dev_err_probe(dev, -EPROBE_DEFER, "Failed to get remoteproc\n");
+
+ devm_add_action_or_reset(dev, btqcomipc_rproc_put, desc->rproc);
+
+ ret = btqcomipc_alloc_memory_region(desc);
+ if (ret)
+ return ret;
+
+ desc->regmap = syscon_regmap_lookup_by_phandle_args(dev->of_node, "qcom,ipc", 2, args);
+ if (IS_ERR(desc->regmap))
+ return PTR_ERR(desc->regmap);
+
+ desc->offset = args[0];
+ desc->bit = args[1];
+
+ desc->irq = platform_get_irq(pdev, 0);
+ if (desc->irq < 0)
+ return dev_err_probe(dev, desc->irq, "Failed to acquire IRQ\n");
+
+ hdev = hci_alloc_dev();
+ if (!hdev)
+ return -ENOMEM;
+
+ hci_set_drvdata(hdev, desc);
+ desc->hdev = hdev;
+ SET_HCIDEV_DEV(hdev, &pdev->dev);
+ hdev->bus = HCI_IPC;
+
+ hdev->open = btqcomipc_open;
+ hdev->close = btqcomipc_close;
+ hdev->shutdown = btqcomipc_shutdown;
+ hdev->setup = btqcomipc_setup;
+ hdev->send = btqcomipc_send;
+ hdev->flush = btqcomipc_flush;
+ hdev->set_bdaddr = qca_set_bdaddr;
+
+ ret = hci_register_dev(hdev);
+ if (ret < 0) {
+ hci_free_dev(hdev);
+ return dev_err_probe(dev, -EBUSY, "Failed to register hdev\n");
+ }
+
+ platform_set_drvdata(pdev, desc);
+
+ return 0;
+}
+
+static void btqcomipc_remove(struct platform_device *pdev)
+{
+ struct qcom_btss *desc = platform_get_drvdata(pdev);
+
+ if (!desc || !desc->hdev)
+ return;
+
+ hci_unregister_dev(desc->hdev);
+ hci_free_dev(desc->hdev);
+}
+
+static const struct of_device_id btqcomipc_of_match[] = {
+ { .compatible = "qcom,ipq5018-bt" },
+ { /* sentinel */},
+};
+MODULE_DEVICE_TABLE(of, btqcomipc_of_match);
+
+static struct platform_driver btqcomipc_driver = {
+ .probe = btqcomipc_probe,
+ .remove = btqcomipc_remove,
+ .driver = {
+ .name = "btqcomipc",
+ .of_match_table = btqcomipc_of_match,
+ },
+};
+
+module_platform_driver(btqcomipc_driver);
+
+MODULE_DESCRIPTION("Qualcomm Bluetooth IPC Driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH 6/6] arm64: dts: qcom: ipq5018: add nodes required for Bluetooth support
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
From: George Moussalem <george.moussalem@outlook.com>
Add nodes for the M0 remoteproc, reserved memory carveout, and Bluetooth
to bring up the M0 core and enable the Bluetooth Subsystem.
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
arch/arm64/boot/dts/qcom/ipq5018.dtsi | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/boot/dts/qcom/ipq5018.dtsi b/arch/arm64/boot/dts/qcom/ipq5018.dtsi
index 6f8004a22a1f..4fdf20c87b0a 100644
--- a/arch/arm64/boot/dts/qcom/ipq5018.dtsi
+++ b/arch/arm64/boot/dts/qcom/ipq5018.dtsi
@@ -17,6 +17,17 @@ / {
#address-cells = <2>;
#size-cells = <2>;
+ bluetooth: bluetooth {
+ compatible = "qcom,ipq5018-bt";
+
+ qcom,ipc = <&apcs_glb 8 23>;
+ interrupts = <GIC_SPI 162 IRQ_TYPE_EDGE_RISING>;
+
+ qcom,rproc = <&m0_btss>;
+
+ status = "disabled";
+ };
+
clocks {
gephy_rx_clk: gephy-rx-clk {
compatible = "fixed-clock";
@@ -131,11 +142,31 @@ psci {
method = "smc";
};
+ m0_btss: remoteproc {
+ compatible = "qcom,ipq5018-btss-pil";
+
+ firmware-name = "qca/bt_fw_patch.mbn";
+
+ clocks = <&gcc GCC_BTSS_LPO_CLK>;
+ clock-names = "btss_lpo_clk";
+ resets = <&gcc GCC_BTSS_BCR>;
+ reset-names = "btss_reset";
+
+ memory-region = <&btss_region>;
+
+ status = "disabled";
+ };
+
reserved-memory {
#address-cells = <2>;
#size-cells = <2>;
ranges;
+ btss_region: bluetooth@7000000 {
+ reg = <0x0 0x07000000 0x0 0x58000>;
+ no-map;
+ };
+
bootloader@4a800000 {
reg = <0x0 0x4a800000 0x0 0x200000>;
no-map;
@@ -647,7 +678,8 @@ watchdog: watchdog@b017000 {
apcs_glb: mailbox@b111000 {
compatible = "qcom,ipq5018-apcs-apps-global",
- "qcom,ipq6018-apcs-apps-global";
+ "qcom,ipq6018-apcs-apps-global",
+ "syscon";
reg = <0x0b111000 0x1000>;
#clock-cells = <1>;
clocks = <&a53pll>, <&xo_board_clk>, <&gcc GPLL0>;
--
2.53.0
^ permalink raw reply related
* [PATCH 4/6] dt-bindings: net: bluetooth: Document Qualcomm IPQ5018 Bluetooth controller
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
From: George Moussalem <george.moussalem@outlook.com>
Document the Qualcomm IPQ5018 Bluetooth controller.
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
.../bindings/net/bluetooth/qcom,ipq5018-bt.yaml | 63 ++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/Documentation/devicetree/bindings/net/bluetooth/qcom,ipq5018-bt.yaml b/Documentation/devicetree/bindings/net/bluetooth/qcom,ipq5018-bt.yaml
new file mode 100644
index 000000000000..afd33f851858
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/bluetooth/qcom,ipq5018-bt.yaml
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/bluetooth/qcom,ipq5018-bt.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Qualcomm IPQ5018 Bluetooth
+
+maintainers:
+ - George Moussalem <george.moussalem@outlook.com>
+
+properties:
+ compatible:
+ enum:
+ - qcom,ipq5018-bt
+
+ interrupts:
+ items:
+ - description:
+ Interrupt line from the M0 Bluetooth Subsystem to the host processor
+ to notify it of events such as re
+
+ qcom,ipc:
+ $ref: /schemas/types.yaml#/definitions/phandle-array
+ items:
+ - items:
+ - description: phandle to a syscon node representing the APCS registers
+ - description: u32 representing offset to the register within the syscon
+ - description: u32 representing the ipc bit within the register
+ description: |
+ These entries specify the outgoing IPC bit used for signaling the remote
+ M0 BTSS core of a host event or for sending an ACK if the remote processor
+ expects it.
+
+ qcom,rproc:
+ $ref: /schemas/types.yaml#/definitions/phandle
+ description:
+ Phandle to the remote processor node representing the M0 BTSS core.
+
+required:
+ - compatible
+ - interrupts
+ - qcom,ipc
+ - qcom,rproc
+
+allOf:
+ - $ref: bluetooth-controller.yaml#
+ - $ref: qcom,bluetooth-common.yaml
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ bluetooth: bluetooth {
+ compatible = "qcom,ipq5018-bt";
+
+ qcom,ipc = <&apcs_glb 8 23>;
+ interrupts = <GIC_SPI 162 IRQ_TYPE_EDGE_RISING>;
+
+ qcom,rproc = <&m0_btss>;
+ };
--
2.53.0
^ permalink raw reply related
* [PATCH 3/6] Bluetooth: btqca: Add IPQ5018 support
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
From: George Moussalem <george.moussalem@outlook.com>
Add the IPQ5018 SoC type and support for loading its firmware.
The firmware tested has been taken from GPL sources of various router
boards. Firmware files needed are:
- qca/bt_fw_patch.mbn
- qca/mpnv10.bin
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
drivers/bluetooth/btqca.c | 16 ++++++++++++++++
drivers/bluetooth/btqca.h | 3 +++
2 files changed, 19 insertions(+)
diff --git a/drivers/bluetooth/btqca.c b/drivers/bluetooth/btqca.c
index 04ebe290bc78..e136e91976cf 100644
--- a/drivers/bluetooth/btqca.c
+++ b/drivers/bluetooth/btqca.c
@@ -380,6 +380,9 @@ static int qca_tlv_check_data(struct hci_dev *hdev,
break;
case TLV_TYPE_NVM:
+ if (soc_type == QCA_IPQ5018)
+ break;
+
if (fw_size < sizeof(struct tlv_type_hdr))
return -EINVAL;
@@ -794,6 +797,9 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
else
rom_ver = ((soc_ver & 0x00000f00) >> 0x04) | (soc_ver & 0x0000000f);
+ if (soc_type == QCA_IPQ5018)
+ goto download_nvm;
+
if (soc_type == QCA_WCN6750)
qca_send_patch_config_cmd(hdev);
@@ -881,6 +887,7 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
if (soc_type == QCA_QCA2066 || soc_type == QCA_WCN7850)
qca_read_fw_board_id(hdev, &boardid);
+download_nvm:
/* Download NVM configuration */
config.type = TLV_TYPE_NVM;
if (firmware_name) {
@@ -939,6 +946,10 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
qca_get_nvm_name_by_board(config.fwname, sizeof(config.fwname),
"hmtnv", soc_type, ver, rom_ver, boardid);
break;
+ case QCA_IPQ5018:
+ snprintf(config.fwname, sizeof(config.fwname),
+ "qca/mpnv%02x.bin", rom_ver);
+ break;
default:
snprintf(config.fwname, sizeof(config.fwname),
"qca/nvm_%08x.bin", soc_ver);
@@ -958,6 +969,9 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
return err;
}
+ if (soc_type == QCA_IPQ5018)
+ msleep(NVM_READY_DELAY_MS);
+
switch (soc_type) {
case QCA_QCA2066:
case QCA_QCA6390:
@@ -965,6 +979,7 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
case QCA_WCN6750:
case QCA_WCN6855:
case QCA_WCN7850:
+ case QCA_IPQ5018:
err = qca_disable_soc_logging(hdev);
if (err < 0)
return err;
@@ -1001,6 +1016,7 @@ int qca_uart_setup(struct hci_dev *hdev, uint8_t baudrate,
case QCA_WCN6750:
case QCA_WCN6855:
case QCA_WCN7850:
+ case QCA_IPQ5018:
/* get fw build info */
err = qca_read_fw_build_info(hdev);
if (err < 0)
diff --git a/drivers/bluetooth/btqca.h b/drivers/bluetooth/btqca.h
index 8f3c1b1c77b3..343cd62d1137 100644
--- a/drivers/bluetooth/btqca.h
+++ b/drivers/bluetooth/btqca.h
@@ -54,6 +54,8 @@
#define QCA_HSP_GF_SOC_ID 0x1200
#define QCA_HSP_GF_SOC_MASK 0x0000ff00
+#define NVM_READY_DELAY_MS 1500
+
enum qca_baudrate {
QCA_BAUDRATE_115200 = 0,
QCA_BAUDRATE_57600,
@@ -158,6 +160,7 @@ enum qca_btsoc_type {
QCA_WCN6750,
QCA_WCN6855,
QCA_WCN7850,
+ QCA_IPQ5018,
};
#if IS_ENABLED(CONFIG_BT_QCA)
--
2.53.0
^ permalink raw reply related
* [PATCH 0/6] Add support for IPQ5018 Bluetooth
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
Hello,
This patch series introduces Bluetooth support for IPQ5018.
Bluetooth firmware on the IPQ5018 platform runs on the M0 co-processor.
The firmware is loaded by the host into a dedicated reserved memory
carveout and authenticated by TrustZone. A Secure Channel Manager (SCM)
call safely brings the peripheral core out of reset.
A shared memory ring buffer topology handles runtime data frame
transport between the host APSS and the M0 core. An outgoing APCS IPC
bit and an incoming GIC interrupt handle host/guest signaling.
This series has been tested and verified on various IPQ5018 router
boards utilizing firmware extracted from GPL distributions, using both
mdt and mbn file formats.
[ 14.781511] Bluetooth: hci0: QCA Product ID :0x00000016
[ 14.781583] Bluetooth: hci0: QCA SOC Version :0x20180100
[ 14.785926] Bluetooth: hci0: QCA ROM Version :0x00000100
[ 14.791546] Bluetooth: hci0: QCA Patch Version:0x00003ded
[ 14.796698] Bluetooth: hci0: QCA controller version 0x01000100
[ 14.802217] Bluetooth: hci0: QCA Downloading qca/mpnv10.bin
[ 16.393850] Bluetooth: hci0: QCA Build Info: BTFW.MAPLE.1.0.0-00102-MPL_ROM_PATCHZ-1
Best regards,
George Moussalem
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
George Moussalem (6):
dt-bindings: remoteproc: document M0 Bluetooth Subsystem secure PIL
remoteproc: qcom: Add M0 BTSS secure PIL driver
Bluetooth: btqca: Add IPQ5018 support
dt-bindings: net: bluetooth: Document Qualcomm IPQ5018 Bluetooth controller
Bluetooth: Introduce Qualcomm IPQ5018 IPC based HCI driver
arm64: dts: qcom: ipq5018: add nodes required for Bluetooth support
.../bindings/net/bluetooth/qcom,ipq5018-bt.yaml | 63 ++
.../bindings/remoteproc/qcom,m0-btss-pil.yaml | 72 ++
arch/arm64/boot/dts/qcom/ipq5018.dtsi | 34 +-
drivers/bluetooth/Kconfig | 11 +
drivers/bluetooth/Makefile | 1 +
drivers/bluetooth/btqca.c | 16 +
drivers/bluetooth/btqca.h | 3 +
drivers/bluetooth/btqcomipc.c | 939 +++++++++++++++++++++
drivers/remoteproc/Kconfig | 12 +
drivers/remoteproc/Makefile | 1 +
drivers/remoteproc/qcom_m0_btss_pil.c | 261 ++++++
11 files changed, 1412 insertions(+), 1 deletion(-)
---
base-commit: 4d1ab324fcb7d20df5a071edb0304461846fdc12
change-id: 20260625-ipq5018-bluetooth-06ff66c9d753
prerequisite-message-id: <20260612-block-as-nvmem-v5-0-95e0b30fff90@oss.qualcomm.com>
prerequisite-patch-id: 6ce8686c1683f468d86b4502f5ec9d19c392a382
prerequisite-patch-id: e362f7fcbacff716b7ef720e6780786a7d88c013
prerequisite-patch-id: 9168930e40551e842c8171d5433a6f39ad4b78a4
prerequisite-patch-id: 64fecfbd1e085d7d2ab0ae23295ca34ec8e14c5e
prerequisite-patch-id: 566804aaa690ee9aa285d0fd75fd16d94fbadebf
prerequisite-patch-id: dc18bec338f54b3051f4523f9d1d3c0566a20ccd
prerequisite-patch-id: b6b3eb46429936ab49423d295433daf47981db0f
prerequisite-patch-id: 75caa99e3bbcdf41b6462b9f5f703bea1d4a65fa
prerequisite-patch-id: 35e9968f482f78ca233eb0306d9c5fdbff093175
Best regards,
--
George Moussalem <george.moussalem@outlook.com>
^ permalink raw reply
* [PATCH 1/6] dt-bindings: remoteproc: document M0 Bluetooth Subsystem secure PIL
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
From: George Moussalem <george.moussalem@outlook.com>
Document the M0 Bluetooth Subsystem remote processor core found in the
Qualcomm IPQ5018 SoC. Firmware loaded is authenticated via TrustZone.
The firmware running on the M0 core provides bluetooth functionality.
Signed-off-by: George Moussalem <george.moussalem@outlook.com>
---
.../bindings/remoteproc/qcom,m0-btss-pil.yaml | 72 ++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/Documentation/devicetree/bindings/remoteproc/qcom,m0-btss-pil.yaml b/Documentation/devicetree/bindings/remoteproc/qcom,m0-btss-pil.yaml
new file mode 100644
index 000000000000..397bb6815d71
--- /dev/null
+++ b/Documentation/devicetree/bindings/remoteproc/qcom,m0-btss-pil.yaml
@@ -0,0 +1,72 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/remoteproc/qcom,m0-btss-pil.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Qualcomm M0 BTSS Peripheral Image Loader
+
+maintainers:
+ - George Moussalem <george.moussalem@outlook.com>
+
+description:
+ Qualcomm M0 BTSS Peripheral Secure Image Loader loads firmware and powers up
+ the M0 BTSS remote processor core on the Qualcomm IPQ5018 SoC.
+
+properties:
+ compatible:
+ enum:
+ - qcom,ipq5018-btss-pil
+
+ firmware-name:
+ maxItems: 1
+ description: Firmware name for the M0 Bluetooth Subsystem core
+
+ clocks:
+ items:
+ - description: M0 BTSS low power oscillator clock
+
+ clock-names:
+ items:
+ - const: btss_lpo_clk
+
+ memory-region:
+ items:
+ - description: M0 BTSS reserved memory carveout
+
+ resets:
+ items:
+ - description: M0 BTSS reset
+
+ reset-names:
+ items:
+ - const: btss_reset
+
+required:
+ - compatible
+ - firmware-name
+ - clocks
+ - clock-names
+ - resets
+ - reset-names
+ - memory-region
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/qcom,gcc-ipq5018.h>
+ #include <dt-bindings/reset/qcom,gcc-ipq5018.h>
+
+ remoteproc {
+ compatible = "qcom,ipq5018-btss-pil";
+
+ firmware-name = "qca/bt_fw_patch.mbn";
+
+ clocks = <&gcc GCC_BTSS_LPO_CLK>;
+ clock-names = "btss_lpo_clk";
+ resets = <&gcc GCC_BTSS_BCR>;
+ reset-names = "btss_reset";
+
+ memory-region = <&btss_region>;
+ };
--
2.53.0
^ permalink raw reply related
* [PATCH 2/6] remoteproc: qcom: Add M0 BTSS secure PIL driver
From: George Moussalem via B4 Relay @ 2026-06-25 14:10 UTC (permalink / raw)
To: 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, Bjorn Andersson, Konrad Dybcio,
Mathieu Poirier, Philipp Zabel
Cc: linux-block, linux-kernel, linux-mmc, devicetree, linux-wireless,
ath10k, linux-arm-msm, linux-bluetooth, netdev, linux-remoteproc,
George Moussalem
In-Reply-To: <20260625-ipq5018-bluetooth-v1-0-d999be0e04f7@outlook.com>
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.
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 related
* Re: [PATCH v10] Add device-specific reset for Qualcomm devices
From: Manivannan Sadhasivam @ 2026-06-25 13:46 UTC (permalink / raw)
To: Jose Ignacio Tornos Martinez
Cc: bhelgaas, alex, jjohnson, linux-pci, linux-wireless, ath11k,
ath12k, mhi, linux-kernel
In-Reply-To: <20260623183115.1585273-1-jtornosm@redhat.com>
On Tue, Jun 23, 2026 at 08:31:15PM +0200, Jose Ignacio Tornos Martinez wrote:
> Some Qualcomm PCIe devices (WCN6855/WCN7850 WiFi cards, SDX62/SDX65 modems)
> lack working reset methods for VFIO passthrough scenarios. These devices
> have no FLR capability, advertise NoSoftRst+ (blocking PM reset), and have
> broken bus reset.
>
> The problem manifests in VFIO passthrough scenarios:
>
> - WCN6855 (17cb:1103) and WCN7850 (17cb:1107) WiFi devices:
> Normal VM operation works fine, including clean shutdown/reboot.
> However, when the VM terminates uncleanly (crash, force-off), VFIO
> attempts to reset the device before it can be assigned to another VM.
> Without a working reset method, the device remains in an undefined state,
> preventing reuse.
>
> - SDX62/SDX65 (17cb:0308) 5G modems: Never successfully initialize even
> on first VM assignment without proper reset capability.
>
> Add device-specific reset methods using BAR-space hardware reset registers
> that exist in these devices:
>
> - WCN6855/WCN7850 WiFi devices use SoC global reset via BAR0 (sequence from
> ath11k/ath12k driver: ath11k_pci_soc_global_reset(), ath11k_pci_sw_reset(),
> ath11k_mhi_set_mhictrl_reset()):
> - Write/clear reset bit at offset 0x3008
> - Wait for PCIe link recovery (up to 5 seconds)
> - Clear MHI controller SYSERR status at offset 0x38
>
> - SDX62/SDX65 modem devices use MHI SoC reset via BAR0 (sequence from MHI
> driver: mhi_soc_reset(), mhi_pci_reset_prepare()):
> - Write reset request to offset 0xb0
> - Wait 2 seconds for reset completion
>
> These are true hardware reset mechanisms (not power management or firmware
> error recovery), providing proper device reset for VFIO scenarios.
>
> Testing was performed on desktop platforms with M.2 WiFi and modem cards
> using M.2-to-PCIe adapters, including extensive force-reset cycling to
> verify stability.
>
> Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
> ---
> v10:
> - Complete redesign based on maintainer feedback (Manivannan Sadhasivam,
> Alex Williamson): use actual hardware reset registers from
> device drivers instead of D3hot power cycling
> v9: https://lore.kernel.org/all/20260612142638.1243895-1-jtornosm@redhat.com/
>
> drivers/pci/quirks.c | 118 +++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 118 insertions(+)
>
> diff --git a/drivers/pci/quirks.c b/drivers/pci/quirks.c
> index 431c021d7414..8ad3f214e520 100644
> --- a/drivers/pci/quirks.c
> +++ b/drivers/pci/quirks.c
> @@ -4240,6 +4240,121 @@ static int reset_hinic_vf_dev(struct pci_dev *pdev, bool probe)
> return 0;
> }
>
> +#define QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET 0x3008
> +#define QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET_V BIT(0)
> +#define QUALCOMM_WIFI_MHISTATUS 0x48
This macro is unused.
> +#define QUALCOMM_WIFI_MHICTRL 0x38
> +#define QUALCOMM_WIFI_MHICTRL_RESET_MASK 0x2
> +
> +/*
> + * Qualcomm WiFi device-specific reset using SoC global reset via BAR0
> + * registers.
> + */
> +static int reset_qualcomm_wifi(struct pci_dev *pdev, bool probe)
> +{
> + bool link_recovered = false;
> + unsigned long timeout;
> + void __iomem *bar;
> + u32 val;
> + u16 cmd;
> +
> + if (probe)
> + return 0;
> +
> + if (pdev->current_state != PCI_D0)
> + return -EINVAL;
> +
> + pci_read_config_word(pdev, PCI_COMMAND, &cmd);
> + pci_write_config_word(pdev, PCI_COMMAND, cmd | PCI_COMMAND_MEMORY);
> +
> + bar = pci_iomap(pdev, 0, 0);
> + if (!bar) {
> + pci_write_config_word(pdev, PCI_COMMAND, cmd);
> + return -ENODEV;
> + }
> +
> + val = ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> + val |= QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET_V;
> + iowrite32(val, bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> + ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> +
> + msleep(10);
> +
> + val &= ~QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET_V;
> + iowrite32(val, bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> + ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> +
> + msleep(10);
> +
> + timeout = jiffies + msecs_to_jiffies(5000);
> + while (time_before(jiffies, timeout)) {
> + val = ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
> + if (val != 0xffffffff) {
s/0xffffffff/PCI_ERROR_RESPONSE
> + link_recovered = true;
> + break;
> + }
> + msleep(20);
> + }
> +
> + if (!link_recovered) {
> + pci_err(pdev, "PCIe link failed to recover after reset\n");
> + goto out_restore;
> + }
> +
> + /* After SOC_GLOBAL_RESET, MHISTATUS may still have SYSERR bit set
> + * and thus need to set MHICTRL_RESET to clear SYSERR.
> + */
> + iowrite32(QUALCOMM_WIFI_MHICTRL_RESET_MASK, bar + QUALCOMM_WIFI_MHICTRL);
> + ioread32(bar + QUALCOMM_WIFI_MHICTRL);
> +
> + msleep(10);
> +
> +out_restore:
> + pci_iounmap(pdev, bar);
> + pci_write_config_word(pdev, PCI_COMMAND, cmd);
> +
> + return link_recovered ? 0 : -ETIMEDOUT;
> +}
> +
> +#define MHI_SOC_RESET_REQ_OFFSET 0xb0
> +#define MHI_SOC_RESET_REQ BIT(0)
> +
> +/*
> + * Qualcomm modem device-specific reset using MHI SoC reset via BAR0
> + * register.
> + */
> +static int reset_qualcomm_modem(struct pci_dev *pdev, bool probe)
> +{
> + void __iomem *bar;
> + u16 cmd;
> +
> + if (probe)
> + return 0;
> +
> + if (pdev->current_state != PCI_D0)
> + return -EINVAL;
> +
> + pci_read_config_word(pdev, PCI_COMMAND, &cmd);
> + pci_write_config_word(pdev, PCI_COMMAND, cmd | PCI_COMMAND_MEMORY);
> +
> + bar = pci_iomap(pdev, 0, 0);
> + if (!bar) {
> + pci_write_config_word(pdev, PCI_COMMAND, cmd);
> + return -ENODEV;
> + }
> +
> + iowrite32(MHI_SOC_RESET_REQ, bar + MHI_SOC_RESET_REQ_OFFSET);
> + ioread32(bar + MHI_SOC_RESET_REQ_OFFSET);
> +
> + /* Be sure device reset has been executed */
> + msleep(2000);
> +
> + pci_iounmap(pdev, bar);
> + pci_write_config_word(pdev, PCI_COMMAND, cmd);
> +
> + return 0;
> +}
> +
> static const struct pci_dev_reset_methods pci_dev_reset_methods[] = {
> { PCI_VENDOR_ID_INTEL, PCI_DEVICE_ID_INTEL_82599_SFP_VF,
> reset_intel_82599_sfp_virtfn },
> @@ -4255,6 +4370,9 @@ static const struct pci_dev_reset_methods pci_dev_reset_methods[] = {
> reset_chelsio_generic_dev },
> { PCI_VENDOR_ID_HUAWEI, PCI_DEVICE_ID_HINIC_VF,
> reset_hinic_vf_dev },
> + { PCI_VENDOR_ID_QCOM, 0x1103, reset_qualcomm_wifi }, /* WCN6855 WiFi */
> + { PCI_VENDOR_ID_QCOM, 0x1107, reset_qualcomm_wifi }, /* WCN7850 WiFi */
> + { PCI_VENDOR_ID_QCOM, 0x0308, reset_qualcomm_modem }, /* SDX62/SDX65 modems */
Not an issue and the existing entries are not sorted, but can you atleast keep
these IDs sorted in ascending order?
- Mani
--
மணிவண்ணன் சதாசிவம்
^ permalink raw reply
* Re: [PATCH v10] Add device-specific reset for Qualcomm devices
From: Manivannan Sadhasivam @ 2026-06-25 13:03 UTC (permalink / raw)
To: Baochen Qiang
Cc: Jose Ignacio Tornos Martinez, bhelgaas, alex, jjohnson, linux-pci,
linux-wireless, ath11k, ath12k, mhi, linux-kernel
In-Reply-To: <4cdfb71b-2ef8-4985-8294-c4a29e37faa3@oss.qualcomm.com>
On Wed, Jun 24, 2026 at 03:47:12PM +0800, Baochen Qiang wrote:
>
>
> On 6/24/2026 2:31 AM, Jose Ignacio Tornos Martinez wrote:
> > Some Qualcomm PCIe devices (WCN6855/WCN7850 WiFi cards, SDX62/SDX65 modems)
> > lack working reset methods for VFIO passthrough scenarios. These devices
> > have no FLR capability, advertise NoSoftRst+ (blocking PM reset), and have
> > broken bus reset.
> >
> > The problem manifests in VFIO passthrough scenarios:
> >
> > - WCN6855 (17cb:1103) and WCN7850 (17cb:1107) WiFi devices:
> > Normal VM operation works fine, including clean shutdown/reboot.
> > However, when the VM terminates uncleanly (crash, force-off), VFIO
> > attempts to reset the device before it can be assigned to another VM.
> > Without a working reset method, the device remains in an undefined state,
> > preventing reuse.
> >
> > - SDX62/SDX65 (17cb:0308) 5G modems: Never successfully initialize even
> > on first VM assignment without proper reset capability.
> >
> > Add device-specific reset methods using BAR-space hardware reset registers
> > that exist in these devices:
> >
> > - WCN6855/WCN7850 WiFi devices use SoC global reset via BAR0 (sequence from
> > ath11k/ath12k driver: ath11k_pci_soc_global_reset(), ath11k_pci_sw_reset(),
> > ath11k_mhi_set_mhictrl_reset()):
> > - Write/clear reset bit at offset 0x3008
> > - Wait for PCIe link recovery (up to 5 seconds)
> > - Clear MHI controller SYSERR status at offset 0x38
> >
> > - SDX62/SDX65 modem devices use MHI SoC reset via BAR0 (sequence from MHI
> > driver: mhi_soc_reset(), mhi_pci_reset_prepare()):
> > - Write reset request to offset 0xb0
> > - Wait 2 seconds for reset completion
> >
> > These are true hardware reset mechanisms (not power management or firmware
> > error recovery), providing proper device reset for VFIO scenarios.
> >
> > Testing was performed on desktop platforms with M.2 WiFi and modem cards
> > using M.2-to-PCIe adapters, including extensive force-reset cycling to
> > verify stability.
> >
> > Signed-off-by: Jose Ignacio Tornos Martinez <jtornosm@redhat.com>
> > ---
> > v10:
> > - Complete redesign based on maintainer feedback (Manivannan Sadhasivam,
> > Alex Williamson): use actual hardware reset registers from
> > device drivers instead of D3hot power cycling
> > v9: https://lore.kernel.org/all/20260612142638.1243895-1-jtornosm@redhat.com/
> >
> > drivers/pci/quirks.c | 118 +++++++++++++++++++++++++++++++++++++++++++
> > 1 file changed, 118 insertions(+)
> >
> > diff --git a/drivers/pci/quirks.c b/drivers/pci/quirks.c
> > index 431c021d7414..8ad3f214e520 100644
> > --- a/drivers/pci/quirks.c
> > +++ b/drivers/pci/quirks.c
> > @@ -4240,6 +4240,121 @@ static int reset_hinic_vf_dev(struct pci_dev *pdev, bool probe)
> > return 0;
> > }
> >
> > +#define QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET 0x3008
> > +#define QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET_V BIT(0)
> > +#define QUALCOMM_WIFI_MHISTATUS 0x48
> > +#define QUALCOMM_WIFI_MHICTRL 0x38
> > +#define QUALCOMM_WIFI_MHICTRL_RESET_MASK 0x2
> > +
> > +/*
> > + * Qualcomm WiFi device-specific reset using SoC global reset via BAR0
> > + * registers.
> > + */
> > +static int reset_qualcomm_wifi(struct pci_dev *pdev, bool probe)
> > +{
> > + bool link_recovered = false;
> > + unsigned long timeout;
> > + void __iomem *bar;
> > + u32 val;
> > + u16 cmd;
> > +
> > + if (probe)
> > + return 0;
> > +
> > + if (pdev->current_state != PCI_D0)
> > + return -EINVAL;
> > +
> > + pci_read_config_word(pdev, PCI_COMMAND, &cmd);
> > + pci_write_config_word(pdev, PCI_COMMAND, cmd | PCI_COMMAND_MEMORY);
> > +
> > + bar = pci_iomap(pdev, 0, 0);
> > + if (!bar) {
> > + pci_write_config_word(pdev, PCI_COMMAND, cmd);
> > + return -ENODEV;
> > + }
> > +
> > + val = ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET);
>
> QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET is beyond the first 4K bar area hence requires MHI
> wakeup before accessing, see [1]. the wakeup callback for WCN6855 is
> ath11k_pci_bus_wake_up() which calls mhi_device_get_sync(). Not sure how this can be done
> here. Maybe Mani can provide some hints?
>
I don't think the device needs to be waken up before
QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET. ath11k driver wakes up the device for
accessing the MHI interface I believe. Since this callback is not touching MHI,
there is no need to wakeup the device, AFAIK.
- Mani
--
மணிவண்ணன் சதாசிவம்
^ permalink raw reply
* [PATCH wireless-next] wifi: iwlwifi: mvm: demote "Unhandled alg" warning to debug
From: Tobias Klausmann @ 2026-06-25 11:53 UTC (permalink / raw)
To: Miri Korenblit
Cc: Johannes Berg, linux-wireless, linux-kernel, Tobias Klausmann
The RX crypto paths emit an IWL_WARN "Unhandled alg" whenever a frame
arrives that the firmware did not decrypt. This is expected and benign
(e.g. multicast frames received before the GTK is installed), is not
actionable, and floods the kernel log.
Keep the diagnostic but move it from IWL_WARN to the driver's existing
debug facility under the IWL_DL_DROP class, so it is silent by default
and can be re-enabled at runtime via the debug_level bitmask. The
multi-queue path uses IWL_DEBUG_DROP_LIMIT, which provides the same
rate limiting the open-coded net_ratelimit() check did.
Signed-off-by: Tobias Klausmann <klausman@schwarzvogel.de>
---
drivers/net/wireless/intel/iwlwifi/mvm/rx.c | 3 ++-
drivers/net/wireless/intel/iwlwifi/mvm/rxmq.c | 5 +++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/drivers/net/wireless/intel/iwlwifi/mvm/rx.c b/drivers/net/wireless/intel/iwlwifi/mvm/rx.c
index d0c0faae0122..8b037e52eae3 100644
--- a/drivers/net/wireless/intel/iwlwifi/mvm/rx.c
+++ b/drivers/net/wireless/intel/iwlwifi/mvm/rx.c
@@ -190,7 +190,8 @@ static u32 iwl_mvm_set_mac80211_rx_flag(struct iwl_mvm *mvm,
default:
/* Expected in monitor (not having the keys) */
if (!mvm->monitor_on)
- IWL_WARN(mvm, "Unhandled alg: 0x%x\n", rx_pkt_status);
+ IWL_DEBUG_DROP(mvm, "Unhandled alg: 0x%x\n",
+ rx_pkt_status);
}
return 0;
diff --git a/drivers/net/wireless/intel/iwlwifi/mvm/rxmq.c b/drivers/net/wireless/intel/iwlwifi/mvm/rxmq.c
index 7f0b4f5daa21..4eded44a104e 100644
--- a/drivers/net/wireless/intel/iwlwifi/mvm/rxmq.c
+++ b/drivers/net/wireless/intel/iwlwifi/mvm/rxmq.c
@@ -504,8 +504,9 @@ static int iwl_mvm_rx_crypto(struct iwl_mvm *mvm, struct ieee80211_sta *sta,
* Also drop un-decrypted frames in monitor mode.
*/
if (!is_multicast_ether_addr(hdr->addr1) &&
- !mvm->monitor_on && net_ratelimit())
- IWL_WARN(mvm, "Unhandled alg: 0x%x\n", status);
+ !mvm->monitor_on)
+ IWL_DEBUG_DROP_LIMIT(mvm, "Unhandled alg: 0x%x\n",
+ status);
}
return 0;
--
2.53.0
^ permalink raw reply related
* [PATCH 2/2] wifi: mt76: mt7996: bound the device EEPROM address before the EFUSE copy
From: Bryam Vargas via B4 Relay @ 2026-06-25 12:10 UTC (permalink / raw)
To: Lorenzo Bianconi, Ryder Lee, Felix Fietkau
Cc: Sean Wang, linux-mediatek, Shayne Chen, linux-wireless,
linux-kernel
In-Reply-To: <20260625-b4-disp-16f99062-v1-0-aee52ecf61b9@proton.me>
From: Bryam Vargas <hexlabsecurity@proton.me>
mt7996_mcu_get_eeprom() derives the destination of the EFUSE/EXT block
copy from the address reported by the MCU response (event->addr, a
device-controlled __le32) and clamps only the copy length, never the
destination offset into dev->mt76.eeprom.data. A malicious or
malfunctioning device can report an arbitrary address and drive an
out-of-bounds write of up to MT7996_EXT_EEPROM_BLOCK_SIZE bytes past
eeprom.data.
Reject a response whose address would place the copy outside eeprom.data
before deriving the destination pointer. Devices that echo the requested
in-bounds offset are unaffected.
Fixes: 98686cd21624 ("wifi: mt76: mt7996: add driver for MediaTek Wi-Fi 7 (802.11be) devices")
Cc: stable@vger.kernel.org
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
drivers/net/wireless/mediatek/mt76/mt7996/mcu.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
index f119f023bcd5..01c9adbca68b 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7996/mcu.c
@@ -4345,11 +4345,18 @@ int mt7996_mcu_get_eeprom(struct mt7996_dev *dev, u32 offset, u8 *buf, u32 buf_l
event = (struct mt7996_mcu_eeprom_access_event *)skb->data;
if (event->valid) {
u32 ret_len = le32_to_cpu(event->eeprom.ext_eeprom.data_len);
+ u32 block = mode == EEPROM_MODE_EXT ? MT7996_EXT_EEPROM_BLOCK_SIZE :
+ MT7996_EEPROM_BLOCK_SIZE;
addr = le32_to_cpu(event->addr);
- if (!buf)
+ if (!buf) {
+ if (addr > dev->mt76.eeprom.size - block) {
+ dev_kfree_skb(skb);
+ return -EINVAL;
+ }
buf = (u8 *)dev->mt76.eeprom.data + addr;
+ }
switch (mode) {
case EEPROM_MODE_EFUSE:
--
2.43.0
^ permalink raw reply related
* [PATCH 1/2] wifi: mt76: mt7915: bound the device EEPROM address before the EFUSE copy
From: Bryam Vargas via B4 Relay @ 2026-06-25 12:10 UTC (permalink / raw)
To: Lorenzo Bianconi, Ryder Lee, Felix Fietkau
Cc: Sean Wang, linux-mediatek, Shayne Chen, linux-wireless,
linux-kernel
In-Reply-To: <20260625-b4-disp-16f99062-v1-0-aee52ecf61b9@proton.me>
From: Bryam Vargas <hexlabsecurity@proton.me>
mt7915_mcu_get_eeprom() copies a fixed EFUSE block into the driver's
dev->mt76.eeprom.data buffer at the offset reported by the MCU response
(res->addr, a device-controlled __le32) without checking it against the
buffer size. A malicious or malfunctioning device can report an arbitrary
address and drive a 16-byte out-of-bounds write past eeprom.data.
Reject a response whose address would place the copy outside eeprom.data
before deriving the destination pointer. Devices that echo the requested
in-bounds offset are unaffected.
Fixes: e57b7901469f ("mt76: add mac80211 driver for MT7915 PCIe-based chipsets")
Cc: stable@vger.kernel.org
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
drivers/net/wireless/mediatek/mt76/mt7915/mcu.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
index 4a381d351e61..f39eae3c4c1c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/mcu.c
@@ -2909,8 +2909,15 @@ int mt7915_mcu_get_eeprom(struct mt7915_dev *dev, u32 offset, u8 *read_buf)
return ret;
res = (struct mt7915_mcu_eeprom_info *)skb->data;
- if (!buf)
- buf = dev->mt76.eeprom.data + le32_to_cpu(res->addr);
+ if (!buf) {
+ u32 addr = le32_to_cpu(res->addr);
+
+ if (addr > dev->mt76.eeprom.size - MT7915_EEPROM_BLOCK_SIZE) {
+ dev_kfree_skb(skb);
+ return -EINVAL;
+ }
+ buf = dev->mt76.eeprom.data + addr;
+ }
memcpy(buf, res->data, MT7915_EEPROM_BLOCK_SIZE);
dev_kfree_skb(skb);
--
2.43.0
^ permalink raw reply related
* [PATCH 0/2] wifi: mt76: bound the device-reported EEPROM address
From: Bryam Vargas via B4 Relay @ 2026-06-25 12:10 UTC (permalink / raw)
To: Lorenzo Bianconi, Ryder Lee, Felix Fietkau
Cc: Sean Wang, linux-mediatek, Shayne Chen, linux-wireless,
linux-kernel
Both mt76 get_eeprom handlers copy a device-reported EFUSE block into
dev->mt76.eeprom.data at an offset taken from the MCU response (res->addr /
event->addr, a device-controlled __le32). They clamp the copy length but
never the destination offset, so an adapter that reports an out-of-range
address drives an out-of-bounds write past eeprom.data -- 16 bytes on mt7915,
up to 1024 on mt7996. Both patches reject such an address before deriving the
pointer; a device that echoes the requested in-bounds offset is unaffected.
It is adapter-side only -- there is no unprivileged user path -- so this
hardens against a malicious or compromised device, not a remote attacker.
An out-of-tree KASAN module that reproduces each handler's destination
arithmetic faults the unpatched path (slab-out-of-bounds write past
eeprom.data) and runs clean both with the bound and on an in-range control.
---
Bryam Vargas (2):
wifi: mt76: mt7915: bound the device EEPROM address before the EFUSE copy
wifi: mt76: mt7996: bound the device EEPROM address before the EFUSE copy
drivers/net/wireless/mediatek/mt76/mt7915/mcu.c | 11 +++++++++--
drivers/net/wireless/mediatek/mt76/mt7996/mcu.c | 9 ++++++++-
2 files changed, 17 insertions(+), 3 deletions(-)
---
base-commit: 502d801f0ab03e4f32f9a33d203154ce84887921
change-id: 20260625-b4-disp-16f99062-0dd6169db97b
Best regards,
--
Bryam Vargas <hexlabsecurity@proton.me>
^ permalink raw reply
* Re: [PATCH] wifi: cfg80211: replace BOOL_TO_STR macro with str_true_false()
From: Johannes Berg @ 2026-06-25 11:22 UTC (permalink / raw)
To: Serhat Kumral, linux-wireless; +Cc: linux-kernel
In-Reply-To: <20260624204938.15222-1-serhatkumral1@gmail.com>
On Wed, 2026-06-24 at 23:49 +0300, Serhat Kumral wrote:
> Remove the local BOOL_TO_STR macro and replace all its usages with
> the kernel's str_true_false() helper from <linux/string_choices.h>.
>
> No functional change intended.
>
I believe this breaks trace-cmd reporting. Please check and resend
indicating that you have.
johannes
^ permalink raw reply
* Re: [PATCH v10] Add device-specific reset for Qualcomm devices
From: Jose Ignacio Tornos Martinez @ 2026-06-25 10:35 UTC (permalink / raw)
To: baochen.qiang
Cc: alex, ath11k, ath12k, bhelgaas, jjohnson, jtornosm, linux-kernel,
linux-pci, linux-wireless, mani, mhi
In-Reply-To: <4cdfb71b-2ef8-4985-8294-c4a29e37faa3@oss.qualcomm.com>
Hello Baochen and Mani,
> QUALCOMM_WIFI_PCIE_SOC_GLOBAL_RESET is beyond the first 4K bar area hence requires MHI
> wakeup before accessing, see [1]. the wakeup callback for WCN6855 is
> ath11k_pci_bus_wake_up() which calls mhi_device_get_sync(). Not sure how this can be done
> here. Maybe Mani can provide some hints?
I've analyzed the driver code and see that ath11k_pci_power_down() calls
ath11k_pci_force_wake() before sw_reset().
I can add the same force_wake sequence to the WiFi quirk before accessing
the reset register:
/* Force wake before accessing registers beyond 4K boundary */
iowrite32(1, bar + QUALCOMM_WIFI_PCIE_SOC_WAKE_PCIE_LOCAL_REG); // 0x3004
ioread32(bar + QUALCOMM_WIFI_PCIE_SOC_WAKE_PCIE_LOCAL_REG); // Flush
msleep(5);
With this addition, both WCN6855 (ath11k) and WCN7850 (ath12k) show successful
reset and shutdown cycles in VFIO scenarios, same stability as without it.
Do you consider this addition necessary, or is the current v10 implementation
sufficient given that testing shows direct register access works without
wakeup in VFIO scenarios (where no driver is loaded)?
If you recommend including it, I can send v11 with the force_wake sequence
added.
Thanks
Best regards
Jose Ignacio
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox