[PATCH net v3] net: stmmac: fix rx Scatter-Gather support
Lorenzo Bianconi
lorenzo.bianconi at oss.qualcomm.com
Wed Sep 23 02:14:28 PDT 2026
When a received frame is larger than dma_buf_sz, the DMA scatters it
across multiple RX descriptors (rx Scatter-Gather). The secondary RX
buffer (sec_page) was only allocated and programmed when split-header
(SPH) was active, so for regular frames buffer2 was neither allocated
nor backed by a valid mapping. As soon as an incoming frame overflowed
buffer1, the DMA wrote the overflow into the unmapped secondary-buffer
address, triggering an SMMU translation fault on IOMMU-based platforms:
arm-smmu 15000000.iommu: Unhandled context fault: fsr=0x402, iova=0x00000000, fsynr=0x7f0011, cbfrsynra=0x1c90, cb=11
arm-smmu 15000000.iommu: FSR = 00000402 [Format=2 TF], SID=0x1c90
arm-smmu 15000000.iommu: FSYNR0 = 007f0011 [S1CBNDX=127 WNR PLVL=1]
Enable scatter-gather for non-SPH frames on cores that can program an
independent secondary RX buffer (GMAC4/XGMAC): allocate and mark buffer2
as valid in stmmac_init_rx_buffers() and stmmac_rx_refill(), and account
for it in the buffer length computation. Legacy cores have no set_sec_addr
op, so they keep buffer2 disabled.
Since buffer2 is handed to the DMA at page offset 0, the page pool sync
window is widened to cover both buffers in every mode: offset is set to 0
and max_len to dma_buf_sz + stmmac_rx_offset().
The FCS can straddle the buffer1/buffer2 boundary and a descriptor
boundary, so it is now stripped from the tail of the assembled frame with
pskb_trim() instead of from a single buffer, avoiding an unsigned underflow
for frames that overflow a buffer by 1..3 bytes. For single-buffer frames
the XDP program must not see the FCS, so it is removed from the XDP buffer
before the program runs.
Native XDP currently only supports single-buffer (linear) frames. An
oversized frame accepted by the MAC (jumbo enabled) is received via
buffer2; the XDP program only sees buffer1, so on a TX/REDIRECT verdict
the frame is forwarded truncated. XDP multi-buffer support to handle
this case is planned as a follow-up.
AF_XDP zero-copy RX is not covered by this change: a ZC queue still
programs buffer2 at DMA address 0 (and XGMAC has no buffer2-valid bit),
so an oversized frame overflowing buffer1 can still trigger the same
SMMU translation fault. Handling is planned as a follow-up.
Fixes: 88ebe2cf7f3f ("net: stmmac: Rework stmmac_rx()")
Signed-off-by: Lorenzo Bianconi <lorenzo.bianconi at oss.qualcomm.com>
---
Changes in v3:
- Run pskb_trim() to remove the ETH FCS from skb tail to take into
account non-linear skb.
- Link to v2: https://lore.kernel.org/r/20260921-stmmac-rx-sg-fix-v2-1-b6d88c5ac2d7@oss.qualcomm.com
Changes in v2:
- Gate buf2 allocation by stmmac_rx_check_buf2_cap() routine.
- Always set pp_params.offset to 0 and pp_params.max_len to dma_buf_sz +
rx_offset.
- Do not remove fcs_len from buf1_len or buf2_len, just from len
counter.
- Link to v1: https://lore.kernel.org/r/20260916-stmmac-rx-sg-fix-v1-1-b49b7b8f725f@oss.qualcomm.com
---
drivers/net/ethernet/stmicro/stmmac/stmmac_main.c | 83 +++++++++++++----------
1 file changed, 49 insertions(+), 34 deletions(-)
diff --git a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
index 3f34d491c959..ec62fa7418f4 100644
--- a/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
+++ b/drivers/net/ethernet/stmicro/stmmac/stmmac_main.c
@@ -1629,6 +1629,16 @@ static void stmmac_clear_descriptors(struct stmmac_priv *priv,
stmmac_clear_tx_descriptors(priv, dma_conf, queue);
}
+static bool stmmac_rx_check_buf2_cap(struct stmmac_priv *priv)
+{
+ /* Only cores that can program an independent secondary RX buffer
+ * (used for scatter-gather overflow or split-header payload) back
+ * buffer2. Legacy cores have no set_sec_addr op, so buffer2 is
+ * never handed to the hardware there.
+ */
+ return priv->hw->desc && priv->hw->desc->set_sec_addr;
+}
+
/**
* stmmac_init_rx_buffers - init the RX descriptor buffer.
* @priv: driver private structure
@@ -1659,16 +1669,13 @@ static int stmmac_init_rx_buffers(struct stmmac_priv *priv,
buf->page_offset = stmmac_rx_offset(priv);
}
- if (priv->sph_active && !buf->sec_page) {
+ if (stmmac_rx_check_buf2_cap(priv) && !buf->sec_page) {
buf->sec_page = page_pool_alloc_pages(rx_q->page_pool, gfp);
if (!buf->sec_page)
return -ENOMEM;
buf->sec_addr = page_pool_get_dma_addr(buf->sec_page);
stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, true);
- } else {
- buf->sec_page = NULL;
- stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, false);
}
buf->addr = page_pool_get_dma_addr(buf->page) + buf->page_offset;
@@ -2256,13 +2263,8 @@ static int __alloc_dma_rx_desc_resources(struct stmmac_priv *priv,
pp_params.nid = dev_to_node(priv->device);
pp_params.dev = priv->device;
pp_params.dma_dir = xdp_prog ? DMA_BIDIRECTIONAL : DMA_FROM_DEVICE;
- pp_params.offset = stmmac_rx_offset(priv);
- pp_params.max_len = dma_conf->dma_buf_sz;
-
- if (priv->sph_active) {
- pp_params.offset = 0;
- pp_params.max_len += stmmac_rx_offset(priv);
- }
+ pp_params.offset = 0;
+ pp_params.max_len = dma_conf->dma_buf_sz + stmmac_rx_offset(priv);
rx_q->page_pool = page_pool_create(&pp_params);
if (IS_ERR(rx_q->page_pool)) {
@@ -5099,7 +5101,7 @@ static inline void stmmac_rx_refill(struct stmmac_priv *priv, u32 queue)
break;
}
- if (priv->sph_active && !buf->sec_page) {
+ if (stmmac_rx_check_buf2_cap(priv) && !buf->sec_page) {
buf->sec_page = page_pool_alloc_pages(rx_q->page_pool, gfp);
if (!buf->sec_page)
break;
@@ -5110,10 +5112,8 @@ static inline void stmmac_rx_refill(struct stmmac_priv *priv, u32 queue)
buf->addr = page_pool_get_dma_addr(buf->page) + buf->page_offset;
stmmac_set_desc_addr(priv, p, buf->addr);
- if (priv->sph_active)
- stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, true);
- else
- stmmac_set_desc_sec_addr(priv, p, buf->sec_addr, false);
+ stmmac_set_desc_sec_addr(priv, p, buf->sec_addr,
+ stmmac_rx_check_buf2_cap(priv));
stmmac_refill_desc3(priv, rx_q, p);
rx_q->rx_count_frames++;
@@ -5144,7 +5144,9 @@ static unsigned int stmmac_rx_buf1_len(struct stmmac_priv *priv,
unsigned int plen = 0, hlen = 0;
int coe = priv->hw->rx_csum;
- /* Not first descriptor, buffer is always zero */
+ /* Not first descriptor, SPH enabled: buffer1 only carries the
+ * split header of the first descriptor, so it is zero here.
+ */
if (priv->sph_active && len)
return 0;
@@ -5155,14 +5157,16 @@ static unsigned int stmmac_rx_buf1_len(struct stmmac_priv *priv,
return hlen;
}
- /* First descriptor, not last descriptor and not split header */
+ /* Not last descriptor and not split header: buffer1 is fully filled */
if (status & rx_not_ls)
return priv->dma_conf.dma_buf_sz;
plen = stmmac_get_rx_frame_len(priv, p, coe);
- /* First descriptor and last descriptor and not split header */
- return min_t(unsigned int, priv->dma_conf.dma_buf_sz, plen);
+ /* Last descriptor and not split header: buffer1 holds the remaining
+ * bytes of the frame, up to dma_buf_sz
+ */
+ return min_t(unsigned int, priv->dma_conf.dma_buf_sz, plen - len);
}
static unsigned int stmmac_rx_buf2_len(struct stmmac_priv *priv,
@@ -5172,8 +5176,7 @@ static unsigned int stmmac_rx_buf2_len(struct stmmac_priv *priv,
int coe = priv->hw->rx_csum;
unsigned int plen = 0;
- /* Not split header, buffer is not available */
- if (!priv->sph_active)
+ if (!stmmac_rx_check_buf2_cap(priv))
return 0;
/* For GMAC4, when split header is enabled, in some rare cases, the
@@ -5190,14 +5193,15 @@ static unsigned int stmmac_rx_buf2_len(struct stmmac_priv *priv,
* Thus 'plen - len' always gives the correct length of buf2.
*/
- /* Not GMAC4 and not last descriptor */
- if (priv->plat->core_type != DWMAC_CORE_GMAC4 && (status & rx_not_ls))
+ /* Not GMAC4, or non-SPH and not last descriptor */
+ if ((priv->plat->core_type != DWMAC_CORE_GMAC4 || !priv->sph_active) &&
+ (status & rx_not_ls))
return priv->dma_conf.dma_buf_sz;
/* GMAC4 or last descriptor */
plen = stmmac_get_rx_frame_len(priv, p, coe);
- return plen - len;
+ return plen > len ? plen - len : 0;
}
static int stmmac_xdp_xmit_xdpf(struct stmmac_priv *priv, int queue,
@@ -5720,6 +5724,7 @@ static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
unsigned int desc_size;
struct sk_buff *skb = NULL;
struct stmmac_xdp_buff ctx;
+ bool fcs_stripped = false;
int xdp_status = 0;
int bufsz;
@@ -5809,24 +5814,25 @@ static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
len += buf2_len;
/* ACS is disabled; strip manually. */
- if (likely(!(status & rx_not_ls))) {
- if (buf2_len) {
- buf2_len -= ETH_FCS_LEN;
- len -= ETH_FCS_LEN;
- } else if (buf1_len) {
- buf1_len -= ETH_FCS_LEN;
- len -= ETH_FCS_LEN;
- }
- }
+ if (likely(!(status & rx_not_ls)))
+ len -= ETH_FCS_LEN;
if (!skb) {
unsigned int pre_len, sync_len;
+ /* Each frame starts here: reset the FCS handling */
+ fcs_stripped = false;
+
dma_sync_single_for_cpu(priv->device, buf->addr,
buf1_len, dma_dir);
net_prefetch(page_address(buf->page) +
buf->page_offset);
+ if (stmmac_xdp_is_enabled(priv) && !buf2_len) {
+ buf1_len -= ETH_FCS_LEN;
+ fcs_stripped = true;
+ }
+
xdp_init_buff(&ctx.xdp, bufsz, &rx_q->xdp_rxq);
xdp_prepare_buff(&ctx.xdp, page_address(buf->page),
buf->page_offset, buf1_len, true);
@@ -5927,6 +5933,15 @@ static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
/* Got entire packet into SKB. Finish it. */
+ /* Remove FCS if needed */
+ if (!fcs_stripped && pskb_trim(skb, len)) {
+ dev_kfree_skb_any(skb);
+ skb = NULL;
+ rx_dropped++;
+ count++;
+ continue;
+ }
+
stmmac_get_rx_hwtstamp(priv, p, np, skb);
if (priv->hw->hw_vlan_en)
---
base-commit: 4d56986fe713d91889fec7e155131fcc2c99a2e3
change-id: 20260916-stmmac-rx-sg-fix-a8d2d8a3ba01
Best regards,
--
Lorenzo Bianconi <lorenzo.bianconi at oss.qualcomm.com>
More information about the linux-arm-kernel
mailing list