From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:40
When lan966x operates as a PCIe endpoint, the driver currently uses
register-based I/O for frame injection and extraction. This approach is
functional but slow, topping out at around 33 Mbps on an Intel x86 host
with a lan966x PCIe card.
This series adds FDMA (Frame DMA) support for the PCIe path. When
operating as a PCIe endpoint, the internal FDMA engine on lan966x cannot
directly access host memory, so DMA buffers are allocated as contiguous
coherent memory and mapped through the PCIe Address Translation Unit
(ATU). The ATU provides outbound windows that translate internal FDMA
addresses to PCIe bus addresses, allowing the FDMA engine to read and
write host memory. Because the ATU requires contiguous address regions,
page_pool and normal per-page DMA mappings cannot be used. Instead,
frames are transferred using memcpy between the ATU-mapped buffers and
the network stack. With this, throughput increases from ~33 Mbps to
~620 Mbps for default MTU.
Patch 1 adds the shared drivers/net/ethernet/microchip/fdma/ directory
to the Sparx5 SoC MAINTAINERS entry.
Patches 2-3 prepare the shared FDMA library: patch 2 renames the
contiguous dataptr helpers for clarity, and patch 3 adds PCIe ATU
region management and coherent DMA allocation with ATU mapping.
Patches 4-7 refactor the lan966x FDMA code to support both platform
and PCIe paths: extracting the LLP register write into a helper,
exporting shared functions, introducing a dedicated device for DMA
operations, and adding an ops dispatch table selected at probe time.
Patches 8-9 harden the existing FDMA path for the PCIe endpoint
lifecycle: patch 8 clears latched FDMA error/interrupt stickies after
the switch reset so they don't assert as soon as interrupts are
enabled, and patch 9 adds a shutdown() callback that quiesces the
FDMA engine on host warm reboot (on the PCIe card the FDMA survives
host reset and would otherwise keep the shared INTx asserted into
the next probe).
Patch 10 adds the core PCIe FDMA implementation with RX/TX using
contiguous ATU-mapped buffers. Patches 11 and 12 extend it with MTU
change and XDP support respectively. XDP_PASS, XDP_TX, XDP_DROP and
XDP_ABORTED are supported; XDP_REDIRECT is deliberately not, because
the PCIe data path does not use page_pool.
Patches 13-14 update the lan966x PCI device tree overlay to extend the
cpu register mapping to cover the ATU register space and add the FDMA
interrupt.
Patches 1-12 touch MAINTAINERS and drivers/net/ethernet/microchip/, and
are for the netdev tree.
Patches 13-14 touch drivers/misc/lan966x_pci.dtso, and are for the
char-misc tree.
To: Andrew Lunn <andrew+netdev@lunn.ch>
To: David S. Miller <davem@davemloft.net>
To: Eric Dumazet <redacted>
To: Jakub Kicinski <kuba@kernel.org>
To: Paolo Abeni <pabeni@redhat.com>
To: Horatiu Vultur <horatiu.vultur@microchip.com>
To: Steen Hegelund <steen.hegelund@microchip.com>
To: UNGLinuxDriver@microchip.com
To: Alexei Starovoitov <ast@kernel.org>
To: Daniel Borkmann <daniel@iogearbox.net>
To: Jesper Dangaard Brouer <hawk@kernel.org>
To: John Fastabend <john.fastabend@gmail.com>
To: Stanislav Fomichev <sdf@fomichev.me>
To: Herve Codina <herve.codina@bootlin.com>
To: Arnd Bergmann <arnd@arndb.de>
To: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
To: Mohsin Bashir <redacted>
Cc: Richard Cochran <richardcochran@gmail.com>
Cc: netdev@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Cc: bpf@vger.kernel.org
Cc: linux-arm-kernel@lists.infradead.org
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
Changes in v6:
The main change is a DMA bug on the PCIe path, found after enabling the
IOMMU on the test host. The rest is hardening and cleanup.
- New patch 6: use a dedicated device for DMA operations. On the PCIe
path lan966x->dev is the platform device created by
of_platform_default_populate(), which carries no iommus/dma-ranges of
its own, so it is not the device the IOMMU has a domain for. DMA
mapped against it lands outside the domain the endpoint's requester ID
is associated with. With the IOMMU enabled, AMD-Vi reported
IO_PAGE_FAULT for the endpoint and no traffic passed. All FDMA DMA now
targets the real PCIe endpoint device. Retested with the IOMMU in
translated (DMA-FQ) mode.
- ATU: serialise region allocation/free and ATU register access with a
mutex.
- ATU: program the region limit per mapping (base_addr + size - 1)
instead of the full region bound, and disable every region in
fdma_pci_atu_init().
- ATU: return -ERANGE instead of -E2BIG when the requested size exceeds
the region size, and clear fdma->atu_region on unmap.
- PCIe FDMA: check fdma_dcbs_init() and unwind the coherent allocation
and the ATU mapping on failure.
- PCIe FDMA: drop the WARN_ON() on an out-of-range src_port; a malformed
IFH should not be able to splat the host.
- Quiesce consistently in shutdown, deinit and the MTU reload: stop the
netdev queues and NAPI before disabling the FDMA channels, and share
one lan966x_fdma_tx_disable_netdev() helper instead of open-coding it
per path.
- shutdown: also mask ANA_ANAINTR, which shares the PCIe INTx with the
FDMA sources.
- MTU change: reject an MTU whose ring would not fit in a single
MAX_PAGE_ORDER coherent block, and reject one whose db_size would be
truncated by the 16-bit DCB DATAL field, rather than programming a
wrong buffer length.
- Assign lan966x->ops directly in the ops dispatch patch, so that patch
no longer adds a helper returning a single constant; PCI detection now
derives from dma_dev and arrives with the PCIe implementation.
- ATU: reject a target address that is not aligned to the 64KB ATU region
granularity, instead of programming a translation offset by the
misalignment.
- Drop the CONFIG_MCHP_LAN966X_PCI guard around the fdma_pci.h include in
fdma_api.h (Jakub).
- Use ifneq ($(CONFIG_MCHP_LAN966X_PCI),) instead of ifdef in both the fdma
and lan966x Makefiles; the symbol is tristate, so the condition has to
cover both y and m (Jakub).
- Reprogram the FDMA LLP registers on the platform reload restore path,
which stopped happening when the LLP write moved into the allocation
functions.
- XDP: drop the napi_synchronize() before freeing the old program on the
PCIe path; READ_ONCE() on port->xdp_prog plus the RCU-deferred
bpf_prog_put() already cover the in-flight poll.
- XDP: note that the ETH_ZLEN floor is deliberately not enforced on XDP_TX.
- PCIe FDMA: count tx_dropped when the DCB ring is full.
- PCIe FDMA: guard the deinit napi_disable() on fdma_ndev, as shutdown()
already does; NAPI is only initialised on the first ndo_open.
- ATU: program the region translation before enabling the region.
- ATU: pad the mapped allocation to the 64KB outbound region granularity,
so the window does not extend past the coherent allocation.
- ATU: reject mapping an fdma that already holds a region, instead of
overwriting the handle and leaking the old mapping. The MTU reload clears
the handle on the live rings first, since it keeps the old mapping alive
until the new rings are in place.
- PCIe FDMA: declare the RX DCB DATAL as the space actually available to the
extraction engine (db_size - XDP_PACKET_HEADROOM), since the data block
pointer starts that far into the slot.
- Move the XDP-unsupported guards from the XDP patch into the PCIe FDMA
patch, so that no intermediate commit advertises NETDEV_XDP_ACT_NDO_XMIT
for a PCIe instance while tx->dcbs_buf is NULL, and none lets an XDP
attach reach the platform page_pool reload. The final tree is unchanged.
- shutdown: take rtnl, so it cannot interleave with the rtnl-only reload
paths and double-disable the NAPI, which would spin forever in
napi_disable().
- Link to v5: https://lore.kernel.org/r/20260520-lan966x-pci-fdma-v5-0-ca56197ae05b@microchip.com
Changes in v5:
This version fixes a single AI review issue, flagged by Paolo. Other AI
issues for v4 has been classified as pre-existing or changes for
follow-ups.
- Fix premature napi_complete_done() in lan966x_fdma_pci_napi_poll() on
FDMA_ERROR and napi_alloc_skb() failure. Bailing out left DONE=1 DCBs
in the ring with no IRQ to drain them. Drop the frame and continue
the poll loop instead. Bump rx_dropped on memory-pressure drop.
(Paolo)
- Link to v4: https://lore.kernel.org/r/20260508-lan966x-pci-fdma-v4-0-14e0c89d8d63@microchip.com
Changes in v4:
- Consolidate rx size checks into lan966x_fdma_pci_rx_size_fits().
Subtract XDP_PACKET_HEADROOM on the max size check, and add ETH_HLEN
on the min size check. This fixes potential OOB reads/writes.
- On xdp_prepare_buff(), update comment to clarify that data is already
offset by XDP_PACKET_HEADROOM.
- Link to v3:
https://lore.kernel.org/r/20260504-lan966x-pci-fdma-v3-0-a56f5740d870@microchip.com
Changes in v3:
Version 3 fixes a number of issues reported by sashiko - mostly
hardening.
- Fix double use of XDP_PACKET_HEADROOM.
- Fix ERR_PTR persistence in fdma->atu_region and add missing
NULL/ERR_PTR guard in fdma_pci_atu_region_unmap().
- Reject size <= 0 in fdma_pci_atu_region_map() and return
-ENOSPC (was -ENOMEM) when no region is free.
- Introduce lan966x_fdma_pci_tx_size_fits() that accounts for
XDP_PACKET_HEADROOM; use it from both xmit paths to keep
bpf_xdp_adjust_tail from writing past the TX slot.
- Validate BLOCKL in rx_check_frame() (reject < IFH+FCS or
> db_size) before it feeds memcpy/XDP sizes.
- READ_ONCE(port->xdp_prog) inside lan966x_xdp_pci_run() to close
a TOCTOU on XDP detach that could deref NULL in
bpf_prog_run_xdp().
- Strip IFH and FCS pre-XDP in rx_check_frame(). After BPF runs
the driver cannot tell whether the tail was modified; drop the
unconditional skb_pull/skb_trim in rx_get_frame().
- Account tx_bytes/tx_packets on XDP_TX success and tx_dropped on
XDP_TX size reject.
- Add dma_wmb()/dma_rmb() around DCB status writes and reads in
xmit, xmit_xdpf, and napi_poll.
- Collected Tested-by: Hervé Codina.
- Link to v2: https://lore.kernel.org/r/20260428-lan966x-pci-fdma-v2-0-d3ec66e06202@microchip.com
Changes in v2:
Version 2 primarily addresses issues with module unload/load, where
traffic would stop working (Hervé), and XDP head/tail adjust that would be
discarded (Mohsin).
Apart from that, I ran through issues reported by Sashiko, and fixed a
number of other issues.
- New patch 1: add drivers/net/ethernet/microchip/fdma/ to the Sparx5
SoC MAINTAINERS entry.
- New patch 7: clear latched FDMA error/interrupt stickies after the
switch reset so they don't fire as soon as interrupts are enabled.
- New patch 8: shutdown() callback, quiescing FDMA on host warm reboot.
- Replaced the depth-2 dev_is_pci(parent->parent) backend selector
with a parent-chain walk.
- XDP: use xdp.data/xdp.data_end for the post-XDP frame length so that
bpf_xdp_adjust_head/tail are respected (Mohsin Bashir)
- MTU change: drain in-flight xmits with netif_tx_disable() on every
port before reallocating rings, waking them again on completion.
- MTU change: cap the PCIe DCB ring at 256 entries so a full-ring
coherent DMA allocation fits in a single MAX_PAGE_ORDER block at
jumbo MTU.
- PCIe ATU: disable the region before clearing its translation on
unmap.
- PCIe FDMA: hold tx_lock in napi_poll around the free-DCB check used
to wake stopped netdev queues.
- PCIe FDMA: return -ENOSPC (not -1) when the DCB ring is exhausted.
- Link to v1: https://lore.kernel.org/r/20260320-lan966x-pci-fdma-v1-0-ef54cb9b0c4b@microchip.com
---
Daniel Machon (14):
MAINTAINERS: add FDMA library to Sparx5 SoC entry
net: microchip: fdma: rename contiguous dataptr helpers
net: microchip: fdma: add PCIe ATU support
net: lan966x: add FDMA LLP register write helper
net: lan966x: export FDMA helpers for reuse
net: lan966x: use a dedicated device for DMA operations
net: lan966x: add FDMA ops dispatch for PCIe support
net: lan966x: clear FDMA interrupt stickies after switch reset
net: lan966x: add shutdown callback to stop FDMA on reboot
net: lan966x: add PCIe FDMA support
net: lan966x: add PCIe FDMA MTU change support
net: lan966x: add PCIe FDMA XDP support
misc: lan966x-pci: dts: extend cpu reg to cover PCIE DBI space
misc: lan966x-pci: dts: add fdma interrupt to overlay
MAINTAINERS | 1 +
drivers/misc/lan966x_pci.dtso | 5 +-
drivers/net/ethernet/microchip/fdma/Makefile | 4 +
drivers/net/ethernet/microchip/fdma/fdma_api.c | 44 ++
drivers/net/ethernet/microchip/fdma/fdma_api.h | 22 +-
drivers/net/ethernet/microchip/fdma/fdma_pci.c | 203 ++++++
drivers/net/ethernet/microchip/fdma/fdma_pci.h | 52 ++
drivers/net/ethernet/microchip/lan966x/Makefile | 4 +
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 97 +--
.../ethernet/microchip/lan966x/lan966x_fdma_pci.c | 692 +++++++++++++++++++++
.../net/ethernet/microchip/lan966x/lan966x_main.c | 88 ++-
.../net/ethernet/microchip/lan966x/lan966x_main.h | 42 ++
.../net/ethernet/microchip/lan966x/lan966x_regs.h | 25 +
.../net/ethernet/microchip/lan966x/lan966x_xdp.c | 6 +
14 files changed, 1229 insertions(+), 56 deletions(-)
---
base-commit: 548b86839f7fb819a4d6c83b71c73ec378d24275
change-id: 20260313-lan966x-pci-fdma-94ed485d23fa
Best regards,
--
Daniel Machon [off-list ref]
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:35
When lan966x or lan969x operates as a PCIe endpoint, the internal FDMA
engine cannot directly access host memory. Instead, DMA addresses must
be translated through the PCIe Address Translation Unit (ATU). The ATU
provides outbound windows that map internal addresses to PCIe bus
addresses.
The ATU outbound address space (0x10000000-0x1fffffff) is divided into
six equally-sized regions (~42MB each). When FDMA buffers are allocated,
a free ATU region is claimed and programmed with the DMA target address.
The FDMA engine then uses the region's base address in its descriptors,
and the ATU translates these to the actual DMA addresses on the PCIe bus.
Add the required functions and helpers that combine the DMA allocation
with the ATU region mapping, effectively adding support for PCIe FDMA.
The ATU cannot express a limit finer than its 64KB region granularity,
so pad the mapped allocation to that boundary; otherwise the outbound
window would extend past the memory the host allocated for DMA.
This implementation will also be used by the lan969x, when PCIe FDMA is
added for that platform in the future.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/net/ethernet/microchip/fdma/Makefile | 4 +
drivers/net/ethernet/microchip/fdma/fdma_api.c | 44 ++++++
drivers/net/ethernet/microchip/fdma/fdma_api.h | 14 ++
drivers/net/ethernet/microchip/fdma/fdma_pci.c | 203 +++++++++++++++++++++++++
drivers/net/ethernet/microchip/fdma/fdma_pci.h | 52 +++++++
5 files changed, 317 insertions(+)
@@ -127,6 +127,50 @@ void fdma_free_phys(struct fdma *fdma)}EXPORT_SYMBOL_GPL(fdma_free_phys);+#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+/* Allocate coherent DMA memory and map it in the ATU. */+intfdma_alloc_coherent_and_map(structdevice*dev,structfdma*fdma,+structfdma_pci_atu*atu)+{+structfdma_pci_atu_region*region;+interr;++if(WARN_ON(fdma->atu_region))+return-EBUSY;++/* The ATU cannot express a limit finer than the region granularity, so+*thehardwarewidenstheprogrammedlimittothatboundary.Padthe+*allocationtomatch,ortheoutboundwindowwouldextendpastthe+*memoryweown.+*/+fdma->size=ALIGN(fdma->size,FDMA_PCI_ATU_REGION_ALIGN);++err=fdma_alloc_coherent(dev,fdma);+if(err)+returnerr;++region=fdma_pci_atu_region_map(atu,fdma->dma,fdma->size);+if(IS_ERR(region)){+fdma_free_coherent(dev,fdma);+returnPTR_ERR(region);+}++fdma->atu_region=region;++return0;+}+EXPORT_SYMBOL_GPL(fdma_alloc_coherent_and_map);++/* Free coherent DMA memory and unmap the memory in the ATU. */+voidfdma_free_coherent_and_unmap(structdevice*dev,structfdma*fdma)+{+fdma_pci_atu_region_unmap(fdma->atu_region);+fdma->atu_region=NULL;+fdma_free_coherent(dev,fdma);+}+EXPORT_SYMBOL_GPL(fdma_free_coherent_and_unmap);+#endif+/* Get the size of the FDMA memory */u32fdma_get_size(structfdma*fdma){
@@ -7,6 +7,8 @@#include<linux/etherdevice.h>#include<linux/types.h>+#include"fdma_pci.h"+/* This provides a common set of functions and data structures for interacting*withtheFrameDMAengineonmultipleMicrochipswitchcores.*
@@ -109,6 +111,11 @@ struct fdma {u32channel_id;structfdma_opsops;++#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+/* PCI ATU region for this FDMA instance. */+structfdma_pci_atu_region*atu_region;+#endif};/* Advance the DCB index and wrap if required. */
@@ -233,9 +240,16 @@ int __fdma_dcb_add(struct fdma *fdma, int dcb_idx, u64 info, u64 status,intfdma_alloc_coherent(structdevice*dev,structfdma*fdma);intfdma_alloc_phys(structfdma*fdma);+#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+intfdma_alloc_coherent_and_map(structdevice*dev,structfdma*fdma,+structfdma_pci_atu*atu);+#endifvoidfdma_free_coherent(structdevice*dev,structfdma*fdma);voidfdma_free_phys(structfdma*fdma);+#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+voidfdma_free_coherent_and_unmap(structdevice*dev,structfdma*fdma);+#endifu32fdma_get_size(structfdma*fdma);u32fdma_get_size_contiguous(structfdma*fdma);
@@ -0,0 +1,203 @@+// SPDX-License-Identifier: GPL-2.0+++#include<linux/align.h>+#include<linux/errno.h>+#include<linux/io.h>+#include<linux/kernel.h>+#include<linux/types.h>++#include"fdma_pci.h"++/* When the switch operates as a PCIe endpoint, the FDMA engine needs to+*DMAto/fromhostmemory.TheFDMAwritestoaddresseswithintheendpoint's+*internalOutbound(OB)addressspace,andthePCIeATUtranslatestheseto+*DMAaddressesonthePCIebus,targetinghostmemory.+*+*TheATUsupportsuptosixoutboundregions.Thisimplementationdivides+*theOBaddressspaceintosixequallysizedchunks.+*+*+-------------+------------+------------+-----+------------++*|Index|Region0|Region1|...|Region5|+*+-------------+------------+------------+-----+------------++*|Baseaddr|0x10000000|0x12aa0000|...|0x1d520000|+*|Limitaddr|0x12a9ffff|0x1553ffff|...|0x1ffbffff|+*|Targetaddr|hostdma|hostdma|...|hostdma|+*+-------------+------------+------------+-----+------------++*+*BaseaddristhestartaddressoftheregionwithintheOBaddressspace.+*Limitaddriseachregion'sownupperbound.Thevalueactually+*programmedissetper-mappingasbase_addr+mappedsize-1,andis+*usuallysmallerthanthis.+*TargetaddristhehostDMAaddressthatthebaseaddrtranslatesto.+*/++#define FDMA_PCI_ATU_OB_START 0x10000000+#define FDMA_PCI_ATU_OB_END 0x1fffffff++#define FDMA_PCI_ATU_ADDR 0x300000+#define FDMA_PCI_ATU_IDX_SIZE 0x200+#define FDMA_PCI_ATU_ENA_REG 0x4+#define FDMA_PCI_ATU_ENA_BIT BIT(31)+#define FDMA_PCI_ATU_LWR_BASE_ADDR 0x8+#define FDMA_PCI_ATU_UPP_BASE_ADDR 0xc+#define FDMA_PCI_ATU_LIMIT_ADDR 0x10+#define FDMA_PCI_ATU_LWR_TARGET_ADDR 0x14+#define FDMA_PCI_ATU_UPP_TARGET_ADDR 0x18++staticu32fdma_pci_atu_region_size(void)+{+returnround_down((FDMA_PCI_ATU_OB_END-FDMA_PCI_ATU_OB_START)/+FDMA_PCI_ATU_REGION_MAX,FDMA_PCI_ATU_REGION_ALIGN);+}++staticvoid__iomem*fdma_pci_atu_addr_get(void__iomem*addr,intoffset,+intidx)+{+returnaddr+FDMA_PCI_ATU_ADDR+FDMA_PCI_ATU_IDX_SIZE*idx+offset;+}++staticvoidfdma_pci_atu_region_enable(structfdma_pci_atu_region*region)+{+writel(FDMA_PCI_ATU_ENA_BIT,+fdma_pci_atu_addr_get(region->atu->addr,FDMA_PCI_ATU_ENA_REG,+region->idx));+}++staticvoidfdma_pci_atu_region_disable(structfdma_pci_atu_region*region)+{+writel(0,fdma_pci_atu_addr_get(region->atu->addr,FDMA_PCI_ATU_ENA_REG,+region->idx));+}++/* Configure the address translation in the ATU. */+staticvoid+fdma_pci_atu_configure_translation(structfdma_pci_atu_region*region)+{+structfdma_pci_atu*atu=region->atu;+intidx=region->idx;++writel(lower_32_bits(region->base_addr),+fdma_pci_atu_addr_get(atu->addr,+FDMA_PCI_ATU_LWR_BASE_ADDR,idx));++writel(upper_32_bits(region->base_addr),+fdma_pci_atu_addr_get(atu->addr,+FDMA_PCI_ATU_UPP_BASE_ADDR,idx));++/* Upper limit register only needed with REGION_SIZE > 4GB. */+writel(region->limit_addr,+fdma_pci_atu_addr_get(atu->addr,FDMA_PCI_ATU_LIMIT_ADDR,idx));++writel(lower_32_bits(region->target_addr),+fdma_pci_atu_addr_get(atu->addr,+FDMA_PCI_ATU_LWR_TARGET_ADDR,idx));++writel(upper_32_bits(region->target_addr),+fdma_pci_atu_addr_get(atu->addr,+FDMA_PCI_ATU_UPP_TARGET_ADDR,idx));+}++/* Find an unused ATU region. */+staticstructfdma_pci_atu_region*+fdma_pci_atu_region_get_free(structfdma_pci_atu*atu)+{+structfdma_pci_atu_region*regions=atu->regions;++for(inti=0;i<FDMA_PCI_ATU_REGION_MAX;i++){+if(regions[i].in_use)+continue;++return®ions[i];+}++returnERR_PTR(-ENOSPC);+}++/* Unmap an ATU region, clearing its translation and disabling it. */+voidfdma_pci_atu_region_unmap(structfdma_pci_atu_region*region)+{+if(IS_ERR_OR_NULL(region))+return;++mutex_lock(®ion->atu->lock);++region->target_addr=0;+region->in_use=false;++fdma_pci_atu_region_disable(region);+fdma_pci_atu_configure_translation(region);++mutex_unlock(®ion->atu->lock);+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_region_unmap);++/* Map a host DMA address into a free outbound region. */+structfdma_pci_atu_region*+fdma_pci_atu_region_map(structfdma_pci_atu*atu,u64target_addr,intsize)+{+structfdma_pci_atu_region*region;++if(!atu)+returnERR_PTR(-EINVAL);++if(size<=0)+returnERR_PTR(-EINVAL);++if(size>fdma_pci_atu_region_size())+returnERR_PTR(-ERANGE);++/* The ATU region base is only ever aligned to FDMA_PCI_ATU_REGION_ALIGN;+*requirethesamealignmentofthehosttargetaddress,sincetheATU+*translatesaddr-target_addr+base_addrandanymisalignmenthere+*wouldshifteverytranslatedaddressbythesameamount.+*/+if(!IS_ALIGNED(target_addr,FDMA_PCI_ATU_REGION_ALIGN))+returnERR_PTR(-EINVAL);++mutex_lock(&atu->lock);++region=fdma_pci_atu_region_get_free(atu);+if(IS_ERR(region)){+mutex_unlock(&atu->lock);+returnregion;+}++region->target_addr=target_addr;+region->limit_addr=region->base_addr+size-1;+region->in_use=true;++fdma_pci_atu_configure_translation(region);+fdma_pci_atu_region_enable(region);++mutex_unlock(&atu->lock);++returnregion;+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_region_map);++/* Translate a host DMA address to the corresponding OB address. */+u64fdma_pci_atu_translate_addr(structfdma_pci_atu_region*region,u64addr)+{+returnregion->base_addr+(addr-region->target_addr);+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_translate_addr);++/* Initialize ATU, dividing the OB space into equally sized regions. */+voidfdma_pci_atu_init(structfdma_pci_atu*atu,void__iomem*addr)+{+structfdma_pci_atu_region*regions=atu->regions;+u32region_size=fdma_pci_atu_region_size();++atu->addr=addr;+mutex_init(&atu->lock);++for(inti=0;i<FDMA_PCI_ATU_REGION_MAX;i++){+regions[i].base_addr=+FDMA_PCI_ATU_OB_START+(i*region_size);+regions[i].idx=i;+regions[i].atu=atu;++fdma_pci_atu_region_disable(®ions[i]);+}+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_init);
@@ -0,0 +1,52 @@+/* SPDX-License-Identifier: GPL-2.0+ */++#ifndef _FDMA_PCI_H_+#define _FDMA_PCI_H_++#include<linux/align.h>+#include<linux/bits.h>+#include<linux/mutex.h>+#include<linux/types.h>++#define FDMA_PCI_ATU_REGION_MAX 6++/* Outbound regions are 64KB granular (datasheet section 3.24.7.4.1), so both+*theregionbaseandthemappedsizemustbealignedtothis.+*/+#define FDMA_PCI_ATU_REGION_ALIGN BIT(16)++#define FDMA_PCI_DB_ALIGN 128+#define FDMA_PCI_DB_SIZE(mtu) ALIGN(mtu, FDMA_PCI_DB_ALIGN)++structfdma_pci_atu;++structfdma_pci_atu_region{+structfdma_pci_atu*atu;+u64base_addr;/* Base addr of the OB window */+u64limit_addr;/* End addr of the active mapping (base_addr + size - 1) */+u64target_addr;/* Host DMA address this region maps to */+intidx;+boolin_use;+};++structfdma_pci_atu{+void__iomem*addr;+structmutexlock;/* Protects region alloc/free and ATU register access */+structfdma_pci_atu_regionregions[FDMA_PCI_ATU_REGION_MAX];+};++/* Initialize ATU, dividing OB space into regions. */+voidfdma_pci_atu_init(structfdma_pci_atu*atu,void__iomem*addr);++/* Unmap an ATU region, clearing its translation and disabling it. */+voidfdma_pci_atu_region_unmap(structfdma_pci_atu_region*region);++/* Map a host DMA address into a free ATU region. */+structfdma_pci_atu_region*fdma_pci_atu_region_map(structfdma_pci_atu*atu,+u64target_addr,+intsize);++/* Translate a host DMA address to the OB address space. */+u64fdma_pci_atu_translate_addr(structfdma_pci_atu_region*region,u64addr);++#endif
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:39
The FDMA Link List Pointer (LLP) register points to the first DCB in the
chain and must be written before the channel is activated. This tells
the FDMA engine where to begin DMA transfers.
Move the LLP register writes from the channel start/activate functions
into the allocation functions and introduce a shared
lan966x_fdma_llp_configure() helper. This is needed because the upcoming
PCIe FDMA path writes ATU-translated addresses to the LLP registers
instead of DMA addresses. Keeping the writes in the shared
start/activate path would overwrite these translated addresses.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 30 ++++++++++------------
1 file changed, 14 insertions(+), 16 deletions(-)
@@ -109,6 +109,13 @@ static int lan966x_fdma_rx_alloc_page_pool(struct lan966x_rx *rx)return0;}+staticvoidlan966x_fdma_llp_configure(structlan966x*lan966x,u64addr,+u8channel_id)+{+lan_wr(lower_32_bits(addr),lan966x,FDMA_DCB_LLP(channel_id));+lan_wr(upper_32_bits(addr),lan966x,FDMA_DCB_LLP1(channel_id));+}+staticintlan966x_fdma_rx_alloc(structlan966x_rx*rx){structlan966x*lan966x=rx->lan966x;
@@ -128,6 +135,8 @@ static int lan966x_fdma_rx_alloc(struct lan966x_rx *rx)fdma_dcbs_init(fdma,FDMA_DCB_INFO_DATAL(fdma->db_size),FDMA_DCB_STATUS_INTR);+lan966x_fdma_llp_configure(lan966x,fdma->dma,fdma->channel_id);+return0;}
@@ -137,14 +146,6 @@ static void lan966x_fdma_rx_start(struct lan966x_rx *rx)structfdma*fdma=&rx->fdma;u32mask;-/* When activating a channel, first is required to write the first DCB-*addressandthentoactivateit-*/-lan_wr(lower_32_bits((u64)fdma->dma),lan966x,-FDMA_DCB_LLP(fdma->channel_id));-lan_wr(upper_32_bits((u64)fdma->dma),lan966x,-FDMA_DCB_LLP1(fdma->channel_id));-lan_wr(FDMA_CH_CFG_CH_DCB_DB_CNT_SET(fdma->n_dbs)|FDMA_CH_CFG_CH_INTR_DB_EOF_ONLY_SET(1)|FDMA_CH_CFG_CH_INJ_PORT_SET(0)|
@@ -215,6 +216,8 @@ static int lan966x_fdma_tx_alloc(struct lan966x_tx *tx)fdma_dcbs_init(fdma,0,0);+lan966x_fdma_llp_configure(lan966x,fdma->dma,fdma->channel_id);+return0;out:
@@ -236,14 +239,6 @@ static void lan966x_fdma_tx_activate(struct lan966x_tx *tx)structfdma*fdma=&tx->fdma;u32mask;-/* When activating a channel, first is required to write the first DCB-*addressandthentoactivateit-*/-lan_wr(lower_32_bits((u64)fdma->dma),lan966x,-FDMA_DCB_LLP(fdma->channel_id));-lan_wr(upper_32_bits((u64)fdma->dma),lan966x,-FDMA_DCB_LLP1(fdma->channel_id));-lan_wr(FDMA_CH_CFG_CH_DCB_DB_CNT_SET(fdma->n_dbs)|FDMA_CH_CFG_CH_INTR_DB_EOF_ONLY_SET(1)|FDMA_CH_CFG_CH_INJ_PORT_SET(0)|
@@ -876,6 +871,9 @@ static int lan966x_fdma_reload(struct lan966x *lan966x, int new_mtu)MEM_TYPE_PAGE_POOL,page_pool);}+lan966x_fdma_llp_configure(lan966x,lan966x->rx.fdma.dma,+lan966x->rx.fdma.channel_id);+lan966x_fdma_rx_start(&lan966x->rx);lan966x_fdma_wakeup_netdev(lan966x);
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:41
The FDMA library under drivers/net/ethernet/microchip/fdma/ is shared by
the lan966x, sparx5 and lan969x drivers, but is not covered by an entry
in the MAINTAINERS file. A subsequent patch will add new files to the
FDMA library, so let's make sure it's covered.
Add drivers/net/ethernet/microchip/fdma/ to the Sparx5 SoC entry, since
I am already listed there.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
MAINTAINERS | 1 +
1 file changed, 1 insertion(+)
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:41
When the FDMA library was introduced [1], two helpers to get the DMA and
virtual address of a data buffer, in contiguous memory, were added. These
helpers have had no callers until this series. I found the naming I
initially used confusing and inconsistent.
Rename fdma_dataptr_get_contiguous() and
fdma_dataptr_virt_get_contiguous() to fdma_dataptr_dma_addr_contiguous()
and fdma_dataptr_virt_addr_contiguous(). This makes the pair symmetric
and clarifies what type of address each returns.
[1]: commit 30e48a75df9c ("net: microchip: add FDMA library")
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/net/ethernet/microchip/fdma/fdma_api.h | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:43
Make shared FDMA helpers non-static, so they can be reused by the PCIe
FDMA implementation.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 22 +++++++++++-----------
.../net/ethernet/microchip/lan966x/lan966x_main.h | 11 +++++++++++
2 files changed, 22 insertions(+), 11 deletions(-)
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:48
In preparation for the PCIe FDMA implementation, add lan966x->dma_dev,
resolved once at probe time, and use it for every DMA operation the
FDMA path performs: coherent allocations, streaming mappings and the RX
page pool. Natively, dma_dev is lan966x->dev, so there is no functional
change.
Since dma_dev differs from dev only on the PCIe path, add
lan966x_is_pci() on top of it, for the later PCIe patches to key off.
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 30 +++++++++++-----------
.../net/ethernet/microchip/lan966x/lan966x_main.c | 16 ++++++++++++
.../net/ethernet/microchip/lan966x/lan966x_main.h | 10 ++++++++
3 files changed, 41 insertions(+), 15 deletions(-)
@@ -1081,6 +1082,20 @@ static int lan966x_reset_switch(struct lan966x *lan966x)return0;}+/* When enumerated over PCIe, dev is a platform device with no+*iommus/dma-rangesofitsown,soDMAmusttargetthePCIeendpointinstead.+*Theresultdiffersfromdevonlyinthatcase;lan966x_is_pci()reliesonit.+*/+staticstructdevice*lan966x_get_dma_dev(structdevice*dev)+{+for(structdevice*p=dev->parent;p;p=p->parent){+if(dev_is_pci(p))+returnp;+}++returndev;+}+staticintlan966x_probe(structplatform_device*pdev){structfwnode_handle*ports,*portnp;
@@ -1094,6 +1109,7 @@ static int lan966x_probe(struct platform_device *pdev)platform_set_drvdata(pdev,lan966x);lan966x->dev=&pdev->dev;+lan966x->dma_dev=lan966x_get_dma_dev(lan966x->dev);if(!device_get_mac_address(&pdev->dev,mac_addr)){ether_addr_copy(lan966x->base_mac,mac_addr);
@@ -270,6 +270,9 @@ struct lan966x_skb_cb {structlan966x{structdevice*dev;+/* Device used for DMA; the PCIe endpoint when enumerated over PCIe. */+structdevice*dma_dev;+u8num_phys_ports;structlan966x_port**ports;
@@ -573,6 +576,13 @@ void lan966x_fdma_wakeup_netdev(struct lan966x *lan966x);intlan966x_fdma_get_max_frame(structlan966x*lan966x);intlan966x_qsys_sw_status(structlan966x*lan966x);+/* dma_dev differs from dev only on the PCIe path. */+staticinlineboollan966x_is_pci(structlan966x*lan966x)+{+returnIS_ENABLED(CONFIG_MCHP_LAN966X_PCI)&&+lan966x->dma_dev!=lan966x->dev;+}+intlan966x_lag_port_join(structlan966x_port*port,structnet_device*brport_dev,structnet_device*bond,
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:52
Introduce lan966x_fdma_ops to support different FDMA implementations
for platform and PCIe. Plumb fdma_init, fdma_deinit, fdma_xmit,
fdma_poll and fdma_resize through the ops table, and select the
implementation at probe time. Only the platform implementation exists
at this point; the PCIe implementation is added in a later patch.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 2 +-
.../net/ethernet/microchip/lan966x/lan966x_main.c | 20 +++++++++++++++-----
.../net/ethernet/microchip/lan966x/lan966x_main.h | 13 +++++++++++++
3 files changed, 29 insertions(+), 6 deletions(-)
@@ -273,6 +284,8 @@ struct lan966x {/* Device used for DMA; the PCIe endpoint when enumerated over PCIe. */structdevice*dma_dev;+conststructlan966x_fdma_ops*ops;+u8num_phys_ports;structlan966x_port**ports;
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:00:56
When in PCI mode, the GCB soft reset issued by the reset controller
can latch spurious bits in the FDMA error stickies. The latched bits
sit in FDMA_INTR_ERR until the FDMA IRQ is requested later in probe,
at which point the handler fires immediately and WARNs.
Clear FDMA_ERRORS, FDMA_INTR_ERR and FDMA_INTR_DB right after the
switch reset so the FDMA comes out clean and the IRQ handler does not
see ghost errors on probe.
The clear runs on both the PCI and platform paths. On the platform
path it has no effect — there are no spurious stickies to clear — but
keeping it unconditional avoids a PCI-specific code path here.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/net/ethernet/microchip/lan966x/lan966x_main.c | 9 +++++++++
1 file changed, 9 insertions(+)
@@ -1067,6 +1067,15 @@ static int lan966x_reset_switch(struct lan966x *lan966x)reset_control_reset(switch_reset);+/* When in PCI mode, the GCB soft reset issued by the reset+*controllercanlatchspuriousbitsintheFDMAerrorstickies.+*Clearthembeforerequest_irqhooksuptheFDMAIRQline,+*otherwisethehandlerfiresimmediatelyonprobe.+*/+lan_wr(lan_rd(lan966x,FDMA_ERRORS),lan966x,FDMA_ERRORS);+lan_wr(lan_rd(lan966x,FDMA_INTR_ERR),lan966x,FDMA_INTR_ERR);+lan_wr(lan_rd(lan966x,FDMA_INTR_DB),lan966x,FDMA_INTR_DB);+/* Don't reinitialize the switch core, if it is already initialized. In*caseitisinitializedtwice,somepointersinsidethequeuesystem*inHWwillgetcorruptedandthenafterawhilethequeuesystemgets
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:00
When lan966x is used as a PCIe endpoint, the FDMA engine runs on the
card and survives a host reboot. Without a shutdown callback, channels
stay active and interrupt sources stay armed across the reset, causing
the shared PCIe INTx to assert before the driver has re-probed.
Add a shutdown callback, shared by the platform and PCI paths, that
disables NAPI and stops the netdev TX queues, then disables the RX and
TX channels, and finally masks the interrupt sources that share the PCIe
INTx: FDMA (FDMA_INTR_ENA and FDMA_INTR_DB_ENA) and the analyzer
(ANA_ANAINTR), which lan966x_init() arms unconditionally.
Masking is only required on this path: on reboot the kernel calls
device_shutdown(), so shutdown() is the only callback that runs. On
unbind, fdma_deinit() disables both channels and waits for them to go
idle before lan966x_cleanup_ports() releases the FDMA interrupt handler,
so the engine cannot assert the shared line once the handler is gone.
FDMA_INTR_ENA persists on the card across a warm reboot, so also
restore the full enable in lan966x_fdma_rx_start() to re-arm interrupts
after a previous shutdown(). rx_start() runs after both the RX and TX
rings are allocated, so the same single-site re-arm works for both the
platform and PCIe backends.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../net/ethernet/microchip/lan966x/lan966x_fdma.c | 19 ++++++++++++++++
.../net/ethernet/microchip/lan966x/lan966x_main.c | 26 ++++++++++++++++++++++
.../net/ethernet/microchip/lan966x/lan966x_main.h | 1 +
.../net/ethernet/microchip/lan966x/lan966x_regs.h | 15 +++++++++++++
4 files changed, 61 insertions(+)
@@ -1324,9 +1324,35 @@ static void lan966x_remove(struct platform_device *pdev)debugfs_remove_recursive(lan966x->debugfs_root);}+staticvoidlan966x_shutdown(structplatform_device*pdev)+{+structlan966x*lan966x=platform_get_drvdata(pdev);++if(!lan966x->fdma)+return;++/* The reload paths disable this NAPI under rtnl; serialize with them. */+rtnl_lock();++if(lan966x->fdma_ndev)+napi_disable(&lan966x->napi);++lan966x_fdma_tx_disable_netdev(lan966x);++lan966x_fdma_rx_disable(&lan966x->rx);+lan966x_fdma_tx_disable(&lan966x->tx);++lan_wr(0,lan966x,FDMA_INTR_ENA);+lan_wr(0,lan966x,FDMA_INTR_DB_ENA);+lan_wr(0,lan966x,ANA_ANAINTR);++rtnl_unlock();+}+staticstructplatform_driverlan966x_driver={.probe=lan966x_probe,.remove=lan966x_remove,+.shutdown=lan966x_shutdown,.driver={.name="lan966x-switch",.of_match_table=lan966x_match,
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:04
Add PCIe FDMA support for lan966x. The PCIe FDMA path uses contiguous
DMA buffers mapped through the endpoint's ATU, with memcpy-based frame
transfer instead of per-page DMA mappings.
With PCIe FDMA, throughput increases from ~33 Mbps (register-based I/O)
to ~620 Mbps on an Intel x86 host with a lan966x PCIe card.
XDP is not supported on this path yet, so do not advertise xdp_features
and reject program attach for PCIe instances.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/net/ethernet/microchip/lan966x/Makefile | 4 +
.../ethernet/microchip/lan966x/lan966x_fdma_pci.c | 404 +++++++++++++++++++++
.../net/ethernet/microchip/lan966x/lan966x_main.c | 11 +-
.../net/ethernet/microchip/lan966x/lan966x_main.h | 7 +
.../net/ethernet/microchip/lan966x/lan966x_regs.h | 10 +
.../net/ethernet/microchip/lan966x/lan966x_xdp.c | 6 +
6 files changed, 439 insertions(+), 3 deletions(-)
@@ -0,0 +1,404 @@+// SPDX-License-Identifier: GPL-2.0+++#include"fdma_api.h"+#include"lan966x_main.h"++staticintlan966x_fdma_pci_dataptr_cb(structfdma*fdma,intdcb,intdb,+u64*dataptr)+{+u64addr;++addr=fdma_dataptr_dma_addr_contiguous(fdma,dcb,db);++*dataptr=fdma_pci_atu_translate_addr(fdma->atu_region,addr);++return0;+}++staticintlan966x_fdma_pci_nextptr_cb(structfdma*fdma,intdcb,u64*nextptr)+{+u64addr;++fdma_nextptr_cb(fdma,dcb,&addr);++*nextptr=fdma_pci_atu_translate_addr(fdma->atu_region,addr);++return0;+}++staticintlan966x_fdma_pci_rx_alloc(structlan966x_rx*rx)+{+structlan966x*lan966x=rx->lan966x;+structfdma*fdma=&rx->fdma;+interr;++err=fdma_alloc_coherent_and_map(lan966x->dma_dev,fdma,+&lan966x->atu);+if(err)+returnerr;++err=fdma_dcbs_init(fdma,+FDMA_DCB_INFO_DATAL(fdma->db_size-XDP_PACKET_HEADROOM),+FDMA_DCB_STATUS_INTR);+if(err){+fdma_free_coherent_and_unmap(lan966x->dma_dev,fdma);+returnerr;+}++lan966x_fdma_llp_configure(lan966x,+fdma->atu_region->base_addr,+fdma->channel_id);++return0;+}++staticintlan966x_fdma_pci_tx_alloc(structlan966x_tx*tx)+{+structlan966x*lan966x=tx->lan966x;+structfdma*fdma=&tx->fdma;+interr;++err=fdma_alloc_coherent_and_map(lan966x->dma_dev,fdma,+&lan966x->atu);+if(err)+returnerr;++err=fdma_dcbs_init(fdma,+FDMA_DCB_INFO_DATAL(fdma->db_size),+FDMA_DCB_STATUS_DONE);+if(err){+fdma_free_coherent_and_unmap(lan966x->dma_dev,fdma);+returnerr;+}++lan966x_fdma_llp_configure(lan966x,+fdma->atu_region->base_addr,+fdma->channel_id);++return0;+}++staticintlan966x_fdma_pci_get_next_dcb(structfdma*fdma)+{+structfdma_db*db;++for(inti=0;i<fdma->n_dcbs;i++){+db=fdma_db_get(fdma,i,0);++if(!fdma_db_is_done(db))+continue;+if(fdma_is_last(fdma,&fdma->dcbs[i]))+continue;++returni;+}++return-ENOSPC;+}++/* TX slot layout (sizes in bytes):+*+*+---------------------+-----+---------+-----++*|XDP_PACKET_HEADROOM|IFH|payload|FCS|+*|256|28|len|4|+*+---------------------+-----+---------+-----++*|<----------------db_size----------------->|+*+*Returntrueiftheframeplusrequiredoverheadfits.+*/+staticboollan966x_fdma_pci_tx_size_fits(structfdma*fdma,u32len)+{+returnXDP_PACKET_HEADROOM+IFH_LEN_BYTES+len+ETH_FCS_LEN<=+fdma->db_size;+}++/* Return true if blockl is a valid RX frame size. */+staticboollan966x_fdma_pci_rx_size_fits(structfdma*fdma,u32blockl)+{+returnblockl>=IFH_LEN_BYTES+ETH_HLEN+ETH_FCS_LEN&&+blockl<=fdma->db_size-XDP_PACKET_HEADROOM;+}++staticintlan966x_fdma_pci_rx_check_frame(structlan966x_rx*rx,u64*src_port)+{+structlan966x*lan966x=rx->lan966x;+structfdma*fdma=&rx->fdma;+structlan966x_port*port;+structfdma_db*db;+void*virt_addr;+u32blockl;++/* virt_addr points to the IFH. */+virt_addr=fdma_dataptr_virt_addr_contiguous(fdma,+fdma->dcb_index,+fdma->db_index);++lan966x_ifh_get_src_port(virt_addr,src_port);++if(*src_port>=lan966x->num_phys_ports)+returnFDMA_ERROR;++port=lan966x->ports[*src_port];+if(!port)+returnFDMA_ERROR;++db=fdma_db_next_get(fdma);++/* BLOCKL is a 16-bit HW-populated field; reject obviously-bad+*valuesbeforetheyfeedmemcpy/XDPsizes.+*/+blockl=FDMA_DCB_STATUS_BLOCKL(db->status);+if(!lan966x_fdma_pci_rx_size_fits(fdma,blockl))+returnFDMA_ERROR;++returnFDMA_PASS;+}++staticstructsk_buff*lan966x_fdma_pci_rx_get_frame(structlan966x_rx*rx,+u64src_port)+{+structlan966x*lan966x=rx->lan966x;+structfdma*fdma=&rx->fdma;+structsk_buff*skb;+structfdma_db*db;+u32data_len;++/* Get the received frame and create an SKB for it. */+db=fdma_db_next_get(fdma);+data_len=FDMA_DCB_STATUS_BLOCKL(db->status);++skb=napi_alloc_skb(&lan966x->napi,data_len);+if(unlikely(!skb))+returnNULL;++memcpy(skb->data,+fdma_dataptr_virt_addr_contiguous(fdma,+fdma->dcb_index,+fdma->db_index),+data_len);++skb_put(skb,data_len);++skb->dev=lan966x->ports[src_port]->dev;+skb_pull(skb,IFH_LEN_BYTES);++skb_trim(skb,skb->len-ETH_FCS_LEN);++skb->protocol=eth_type_trans(skb,skb->dev);++if(lan966x->bridge_mask&BIT(src_port)){+skb->offload_fwd_mark=1;++skb_reset_network_header(skb);+if(!lan966x_hw_offload(lan966x,src_port,skb))+skb->offload_fwd_mark=0;+}++skb->dev->stats.rx_bytes+=skb->len;+skb->dev->stats.rx_packets++;++returnskb;+}++staticintlan966x_fdma_pci_xmit(structsk_buff*skb,__be32*ifh,+structnet_device*dev)+{+structlan966x_port*port=netdev_priv(dev);+structlan966x*lan966x=port->lan966x;+structlan966x_tx*tx=&lan966x->tx;+structfdma*fdma=&tx->fdma;+intnext_to_use;+void*virt_addr;++next_to_use=lan966x_fdma_pci_get_next_dcb(fdma);++if(next_to_use<0){+netif_stop_queue(dev);+returnNETDEV_TX_BUSY;+}++if(skb_put_padto(skb,ETH_ZLEN)){+dev->stats.tx_dropped++;+returnNETDEV_TX_OK;+}++if(!lan966x_fdma_pci_tx_size_fits(fdma,skb->len)){+dev_kfree_skb_any(skb);+dev->stats.tx_dropped++;+returnNETDEV_TX_OK;+}++skb_tx_timestamp(skb);++/* virt_addr points to the IFH. */+virt_addr=fdma_dataptr_virt_addr_contiguous(fdma,next_to_use,0);+memcpy(virt_addr,ifh,IFH_LEN_BYTES);+memcpy(virt_addr+IFH_LEN_BYTES,skb->data,skb->len);++/* Order frame write before DCB status write below. */+dma_wmb();++fdma_dcb_add(fdma,+next_to_use,+0,+FDMA_DCB_STATUS_INTR|+FDMA_DCB_STATUS_SOF|+FDMA_DCB_STATUS_EOF|+FDMA_DCB_STATUS_BLOCKO(0)|+FDMA_DCB_STATUS_BLOCKL(IFH_LEN_BYTES+skb->len+ETH_FCS_LEN));++/* Start the transmission. */+lan966x_fdma_tx_start(tx);++dev->stats.tx_bytes+=skb->len;+dev->stats.tx_packets++;++/* Safe to free: PTP is not supported on the PCIe path yet,+*solan966x->ptpisalways0here.+*/+dev_consume_skb_any(skb);++returnNETDEV_TX_OK;+}++staticintlan966x_fdma_pci_napi_poll(structnapi_struct*napi,intweight)+{+structlan966x*lan966x=container_of(napi,structlan966x,napi);+structlan966x_rx*rx=&lan966x->rx;+structfdma*fdma=&rx->fdma;+intdcb_reload,old_dcb;+structsk_buff*skb;+intcounter=0;+u64src_port;++/* Wake any stopped TX queues if a TX DCB is available. */+spin_lock(&lan966x->tx_lock);+if(lan966x_fdma_pci_get_next_dcb(&lan966x->tx.fdma)>=0)+lan966x_fdma_wakeup_netdev(lan966x);+spin_unlock(&lan966x->tx_lock);++dcb_reload=fdma->dcb_index;++/* Get all received skbs. */+while(counter<weight){+if(!fdma_has_frames(fdma))+break;+/* Order DONE read before DCB/frame reads below. */+dma_rmb();+counter++;+switch(lan966x_fdma_pci_rx_check_frame(rx,&src_port)){+caseFDMA_PASS:+break;+caseFDMA_ERROR:+/* No rx_dropped increment here because src_port is+*invalid.+*/+fdma_dcb_advance(fdma);+continue;+}+skb=lan966x_fdma_pci_rx_get_frame(rx,src_port);+fdma_dcb_advance(fdma);+if(!skb){+lan966x->ports[src_port]->dev->stats.rx_dropped++;+continue;+}++napi_gro_receive(&lan966x->napi,skb);+}+while(dcb_reload!=fdma->dcb_index){+old_dcb=dcb_reload;+dcb_reload++;+dcb_reload&=fdma->n_dcbs-1;++fdma_dcb_add(fdma,+old_dcb,+FDMA_DCB_INFO_DATAL(fdma->db_size-XDP_PACKET_HEADROOM),+FDMA_DCB_STATUS_INTR);++lan966x_fdma_rx_reload(rx);+}++if(counter<weight&&napi_complete_done(napi,counter))+lan_wr(0xff,lan966x,FDMA_INTR_DB_ENA);++returncounter;+}++staticintlan966x_fdma_pci_init(structlan966x*lan966x)+{+structfdma*rx_fdma=&lan966x->rx.fdma;+structfdma*tx_fdma=&lan966x->tx.fdma;+interr;++if(!lan966x->fdma)+return0;++lan_wr(FDMA_CTRL_NRESET_SET(0),lan966x,FDMA_CTRL);+lan_wr(FDMA_CTRL_NRESET_SET(1),lan966x,FDMA_CTRL);++fdma_pci_atu_init(&lan966x->atu,lan966x->regs[TARGET_PCIE_DBI]);++lan966x->rx.lan966x=lan966x;+lan966x->rx.max_mtu=lan966x_fdma_get_max_frame(lan966x);+rx_fdma->channel_id=FDMA_XTR_CHANNEL;+rx_fdma->n_dcbs=FDMA_DCB_MAX;+rx_fdma->n_dbs=FDMA_RX_DCB_MAX_DBS;+rx_fdma->priv=lan966x;+rx_fdma->db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+rx_fdma->size=fdma_get_size_contiguous(rx_fdma);+rx_fdma->ops.nextptr_cb=&lan966x_fdma_pci_nextptr_cb;+rx_fdma->ops.dataptr_cb=&lan966x_fdma_pci_dataptr_cb;++lan966x->tx.lan966x=lan966x;+tx_fdma->channel_id=FDMA_INJ_CHANNEL;+tx_fdma->n_dcbs=FDMA_DCB_MAX;+tx_fdma->n_dbs=FDMA_TX_DCB_MAX_DBS;+tx_fdma->priv=lan966x;+tx_fdma->db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+tx_fdma->size=fdma_get_size_contiguous(tx_fdma);+tx_fdma->ops.nextptr_cb=&lan966x_fdma_pci_nextptr_cb;+tx_fdma->ops.dataptr_cb=&lan966x_fdma_pci_dataptr_cb;++err=lan966x_fdma_pci_rx_alloc(&lan966x->rx);+if(err)+returnerr;++err=lan966x_fdma_pci_tx_alloc(&lan966x->tx);+if(err){+fdma_free_coherent_and_unmap(lan966x->dma_dev,rx_fdma);+returnerr;+}++lan966x_fdma_rx_start(&lan966x->rx);++return0;+}++staticintlan966x_fdma_pci_resize(structlan966x*lan966x)+{+return-EOPNOTSUPP;+}++staticvoidlan966x_fdma_pci_deinit(structlan966x*lan966x)+{+if(!lan966x->fdma)+return;++if(lan966x->fdma_ndev)+napi_disable(&lan966x->napi);++lan966x_fdma_tx_disable_netdev(lan966x);+lan966x_fdma_rx_disable(&lan966x->rx);+lan966x_fdma_tx_disable(&lan966x->tx);++fdma_free_coherent_and_unmap(lan966x->dma_dev,&lan966x->rx.fdma);+fdma_free_coherent_and_unmap(lan966x->dma_dev,&lan966x->tx.fdma);+}++conststructlan966x_fdma_opslan966x_fdma_pci_ops={+.fdma_init=&lan966x_fdma_pci_init,+.fdma_deinit=&lan966x_fdma_pci_deinit,+.fdma_xmit=&lan966x_fdma_pci_xmit,+.fdma_poll=&lan966x_fdma_pci_napi_poll,+.fdma_resize=&lan966x_fdma_pci_resize,+};
@@ -873,7 +874,8 @@ static int lan966x_probe_port(struct lan966x *lan966x, u32 p,port->phylink=phylink;-if(lan966x->fdma)+/* XDP is not supported on the PCIe FDMA path. */+if(lan966x->fdma&&!lan966x_is_pci(lan966x))dev->xdp_features=NETDEV_XDP_ACT_BASIC|NETDEV_XDP_ACT_REDIRECT|NETDEV_XDP_ACT_NDO_XMIT;
@@ -1128,7 +1130,8 @@ static int lan966x_probe(struct platform_device *pdev)lan966x->dev=&pdev->dev;lan966x->dma_dev=lan966x_get_dma_dev(lan966x->dev);-lan966x->ops=&lan966x_fdma_ops;+lan966x->ops=lan966x_is_pci(lan966x)?&lan966x_fdma_pci_ops:+&lan966x_fdma_ops;if(!device_get_mac_address(&pdev->dev,mac_addr)){ether_addr_copy(lan966x->base_mac,mac_addr);
@@ -1187,7 +1190,9 @@ static int lan966x_probe(struct platform_device *pdev)if(err)returndev_err_probe(&pdev->dev,err,"Unable to use ptp irq");-lan966x->ptp=1;+/* PTP is not supported on the PCIe path yet. */+if(!lan966x_is_pci(lan966x))+lan966x->ptp=1;}lan966x->fdma_irq=platform_get_irq_byname(pdev,"fdma");
@@ -590,6 +595,8 @@ void lan966x_fdma_tx_disable_netdev(struct lan966x *lan966x);intlan966x_fdma_get_max_frame(structlan966x*lan966x);intlan966x_qsys_sw_status(structlan966x*lan966x);+externconststructlan966x_fdma_opslan966x_fdma_pci_ops;+/* dma_dev differs from dev only on the PCIe path. */staticinlineboollan966x_is_pci(structlan966x*lan966x){
@@ -20,6 +20,12 @@ static int lan966x_xdp_setup(struct net_device *dev, struct netdev_bpf *xdp)return-EOPNOTSUPP;}+if(lan966x_is_pci(lan966x)){+NL_SET_ERR_MSG_MOD(xdp->extack,+"XDP is not supported on the PCIe FDMA path");+return-EOPNOTSUPP;+}+old_xdp=lan966x_xdp_present(lan966x);old_prog=xchg(&port->xdp_prog,xdp->prog);new_xdp=lan966x_xdp_present(lan966x);
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:09
Add MTU change support for the PCIe FDMA path. When the MTU changes,
the contiguous ATU-mapped RX and TX buffers are reallocated with the
new size. On allocation failure, the existing buffers are reused
after being reset.
Cap the PCIe DCB ring at 256 (FDMA_PCI_DCB_MAX) to keep the entire
contiguous allocation under MAX_PAGE_ORDER at jumbo MTU, which 512
DCBs would overflow.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../ethernet/microchip/lan966x/lan966x_fdma_pci.c | 168 ++++++++++++++++++++-
1 file changed, 165 insertions(+), 3 deletions(-)
@@ -1,8 +1,15 @@// SPDX-License-Identifier: GPL-2.0++#include<linux/mmzone.h>+#include"fdma_api.h"#include"lan966x_main.h"+/* Ring must fit in one MAX_PAGE_ORDER DMA block; 512 DCBs overflows+*atjumboMTU.+*/+#define FDMA_PCI_DCB_MAX 256+staticintlan966x_fdma_pci_dataptr_cb(structfdma*fdma,intdcb,intdb,u64*dataptr){
@@ -341,7 +348,7 @@ static int lan966x_fdma_pci_init(struct lan966x *lan966x)lan966x->rx.lan966x=lan966x;lan966x->rx.max_mtu=lan966x_fdma_get_max_frame(lan966x);rx_fdma->channel_id=FDMA_XTR_CHANNEL;-rx_fdma->n_dcbs=FDMA_DCB_MAX;+rx_fdma->n_dcbs=FDMA_PCI_DCB_MAX;rx_fdma->n_dbs=FDMA_RX_DCB_MAX_DBS;rx_fdma->priv=lan966x;rx_fdma->db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);
@@ -351,7 +358,7 @@ static int lan966x_fdma_pci_init(struct lan966x *lan966x)lan966x->tx.lan966x=lan966x;tx_fdma->channel_id=FDMA_INJ_CHANNEL;-tx_fdma->n_dcbs=FDMA_DCB_MAX;+tx_fdma->n_dcbs=FDMA_PCI_DCB_MAX;tx_fdma->n_dbs=FDMA_TX_DCB_MAX_DBS;tx_fdma->priv=lan966x;tx_fdma->db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);
@@ -374,9 +381,164 @@ static int lan966x_fdma_pci_init(struct lan966x *lan966x)return0;}+/* Reset existing rx and tx buffers. */+staticvoidlan966x_fdma_pci_reset_mem(structlan966x*lan966x)+{+structlan966x_rx*rx=&lan966x->rx;+structlan966x_tx*tx=&lan966x->tx;++memset(rx->fdma.dcbs,0,rx->fdma.size);+memset(tx->fdma.dcbs,0,tx->fdma.size);++fdma_dcbs_init(&rx->fdma,+FDMA_DCB_INFO_DATAL(rx->fdma.db_size-XDP_PACKET_HEADROOM),+FDMA_DCB_STATUS_INTR);++fdma_dcbs_init(&tx->fdma,+FDMA_DCB_INFO_DATAL(tx->fdma.db_size),+FDMA_DCB_STATUS_DONE);++lan966x_fdma_llp_configure(lan966x,+tx->fdma.atu_region->base_addr,+tx->fdma.channel_id);+lan966x_fdma_llp_configure(lan966x,+rx->fdma.atu_region->base_addr,+rx->fdma.channel_id);+}++/* Wake all TX queues on every port (undoes lan966x_fdma_tx_disable_netdev). */+staticvoidlan966x_fdma_pci_wakeup_netdev(structlan966x*lan966x)+{+for(inti=0;i<lan966x->num_phys_ports;++i){+structlan966x_port*port=lan966x->ports[i];++if(port)+netif_tx_wake_all_queues(port->dev);+}+}++staticintlan966x_fdma_pci_reload(structlan966x*lan966x,intnew_mtu)+{+structfdmatx_fdma_old=lan966x->tx.fdma;+structfdmarx_fdma_old=lan966x->rx.fdma;+u32old_mtu=lan966x->rx.max_mtu;+interr;++napi_disable(&lan966x->napi);+lan966x_fdma_tx_disable_netdev(lan966x);+lan966x_fdma_rx_disable(&lan966x->rx);+lan966x_fdma_tx_disable(&lan966x->tx);++lan966x->rx.max_mtu=new_mtu;++/* Must be NULL'ed in order to realloc them. */+lan966x->rx.fdma.atu_region=NULL;+lan966x->tx.fdma.atu_region=NULL;++lan966x->tx.fdma.db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+lan966x->tx.fdma.size=fdma_get_size_contiguous(&lan966x->tx.fdma);+lan966x->rx.fdma.db_size=FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+lan966x->rx.fdma.size=fdma_get_size_contiguous(&lan966x->rx.fdma);++err=lan966x_fdma_pci_rx_alloc(&lan966x->rx);+if(err)+gotorestore;++err=lan966x_fdma_pci_tx_alloc(&lan966x->tx);+if(err){+fdma_free_coherent_and_unmap(lan966x->dma_dev,+&lan966x->rx.fdma);+gotorestore;+}++/* Free and unmap old memory. */+fdma_free_coherent_and_unmap(lan966x->dma_dev,&rx_fdma_old);+fdma_free_coherent_and_unmap(lan966x->dma_dev,&tx_fdma_old);++napi_enable(&lan966x->napi);+lan966x_fdma_rx_start(&lan966x->rx);+lan966x_fdma_pci_wakeup_netdev(lan966x);++returnerr;+restore:++/* No new buffers are allocated at this point. Use the old buffers,+*butresetthembeforestartingtheFDMAagain.+*/++memcpy(&lan966x->tx.fdma,&tx_fdma_old,sizeof(structfdma));+memcpy(&lan966x->rx.fdma,&rx_fdma_old,sizeof(structfdma));++lan966x->rx.max_mtu=old_mtu;++lan966x_fdma_pci_reset_mem(lan966x);++napi_enable(&lan966x->napi);+lan966x_fdma_rx_start(&lan966x->rx);+lan966x_fdma_pci_wakeup_netdev(lan966x);++returnerr;+}++staticint__lan966x_fdma_pci_reload(structlan966x*lan966x,intmax_mtu)+{+interr;+u32val;++/* Disable the CPU port. */+lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(0),+QSYS_SW_PORT_MODE_PORT_ENA,+lan966x,QSYS_SW_PORT_MODE(CPU_PORT));++/* Flush the CPU queues. */+readx_poll_timeout(lan966x_qsys_sw_status,+lan966x,+val,+!(QSYS_SW_STATUS_EQ_AVAIL_GET(val)),+READL_SLEEP_US,READL_TIMEOUT_US);++/* Add a sleep in case there are frames between the queues and the CPU+*port+*/+usleep_range(USEC_PER_MSEC,2*USEC_PER_MSEC);++err=lan966x_fdma_pci_reload(lan966x,max_mtu);++/* Enable back the CPU port. */+lan_rmw(QSYS_SW_PORT_MODE_PORT_ENA_SET(1),+QSYS_SW_PORT_MODE_PORT_ENA,+lan966x,QSYS_SW_PORT_MODE(CPU_PORT));++returnerr;+}+staticintlan966x_fdma_pci_resize(structlan966x*lan966x){-return-EOPNOTSUPP;+structfdmarx_fdma;+intmax_mtu;++max_mtu=lan966x_fdma_get_max_frame(lan966x);+if(max_mtu==lan966x->rx.max_mtu)+return0;++/* rx and tx have n_dbs == 1, so both rings need the same contiguous+*dma_alloc_coherent()block,whichcan'texceedMAX_PAGE_ORDER.The+*allocationispaddedtotheATUregiongranularity,sotestthe+*paddedsize.+*/+rx_fdma=lan966x->rx.fdma;+rx_fdma.db_size=FDMA_PCI_DB_SIZE(max_mtu);+if(ALIGN(fdma_get_size_contiguous(&rx_fdma),+FDMA_PCI_ATU_REGION_ALIGN)>(PAGE_SIZE<<MAX_PAGE_ORDER))+return-ERANGE;++/* db_size is also handed to the FDMA in the 16-bit DCB DATAL field,+*wherealargervaluewouldbesilentlytruncated.+*/+if(rx_fdma.db_size>GENMASK(15,0))+return-ERANGE;++return__lan966x_fdma_pci_reload(lan966x,max_mtu);}staticvoidlan966x_fdma_pci_deinit(structlan966x*lan966x)
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:13
Add XDP support for the PCIe FDMA path. The implementation operates on
contiguous ATU-mapped buffers with memcpy-based XDP_TX, unlike the
platform path which uses page_pool.
XDP sees the frame with IFH and FCS stripped. These are removed in
lan966x_fdma_pci_rx_check_frame() before the BPF program runs, because
after the program returns the driver cannot tell whether the tail
region was modified. The skb_pull/skb_trim previously done in
lan966x_fdma_pci_rx_get_frame() are removed for the same reason; the
frame pointer and length are pre-computed by rx_check_frame() and
passed through rx_get_frame() and lan966x_xdp_pci_run() to the caller.
lan966x_fdma_pci_xmit_xdpf() handles XDP_TX: it rebuilds a fresh IFH
in the TX slot, copies the post-XDP frame after it, and lets HW insert
a new FCS.
lan966x_xdp_setup() is extended so the PCIe path skips the page_pool
reload that the platform path needs.
Only XDP_ACT_BASIC is supported.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
.../ethernet/microchip/lan966x/lan966x_fdma_pci.c | 166 ++++++++++++++++++---
.../net/ethernet/microchip/lan966x/lan966x_main.c | 12 +-
.../net/ethernet/microchip/lan966x/lan966x_xdp.c | 12 +-
3 files changed, 159 insertions(+), 31 deletions(-)
@@ -126,7 +127,123 @@ static bool lan966x_fdma_pci_rx_size_fits(struct fdma *fdma, u32 blockl)blockl<=fdma->db_size-XDP_PACKET_HEADROOM;}-staticintlan966x_fdma_pci_rx_check_frame(structlan966x_rx*rx,u64*src_port)+staticintlan966x_fdma_pci_xmit_xdpf(structlan966x_port*port,+void*ptr,u32len)+{+structlan966x*lan966x=port->lan966x;+structlan966x_tx*tx=&lan966x->tx;+structfdma*fdma=&tx->fdma;+intnext_to_use,ret=0;+void*virt_addr;++spin_lock(&lan966x->tx_lock);++next_to_use=lan966x_fdma_pci_get_next_dcb(fdma);++if(next_to_use<0){+netif_stop_queue(port->dev);+port->dev->stats.tx_dropped++;+ret=NETDEV_TX_BUSY;+gotoout;+}++/* Only the upper bound is enforced: XDP owns the frame contents and+*length,soaprogramthatshrinksbelowETH_ZLENgetswhatitasked+*for.+*/+if(!lan966x_fdma_pci_tx_size_fits(fdma,len)){+port->dev->stats.tx_dropped++;+ret=-EINVAL;+gotoout;+}++/* virt_addr points to the IFH. */+virt_addr=fdma_dataptr_virt_addr_contiguous(fdma,next_to_use,0);++/* Construct a fresh IFH. */+memset(virt_addr,0,IFH_LEN_BYTES);+lan966x_ifh_set_bypass(virt_addr,1);+lan966x_ifh_set_port(virt_addr,BIT_ULL(port->chip_port));++/* Copy the (post-XDP) frame after the IFH. */+memcpy(virt_addr+IFH_LEN_BYTES,ptr,len);++/* Order frame write before DCB status write below. */+dma_wmb();++/* Reserve ETH_FCS_LEN for the HW-inserted FCS (len is FCS-stripped). */+fdma_dcb_add(fdma,+next_to_use,+0,+FDMA_DCB_STATUS_INTR|+FDMA_DCB_STATUS_SOF|+FDMA_DCB_STATUS_EOF|+FDMA_DCB_STATUS_BLOCKO(0)|+FDMA_DCB_STATUS_BLOCKL(IFH_LEN_BYTES+len+ETH_FCS_LEN));++/* Start the transmission. */+lan966x_fdma_tx_start(tx);++port->dev->stats.tx_bytes+=len;+port->dev->stats.tx_packets++;++out:+spin_unlock(&lan966x->tx_lock);++returnret;+}++staticintlan966x_xdp_pci_run(structlan966x_port*port,void*data,+u32data_len,void**xdp_data,u32*xdp_len)+{+/* Read once so the NULL check and bpf_prog_run_xdp() see the same+*pointer.+*/+structbpf_prog*xdp_prog=READ_ONCE(port->xdp_prog);+structlan966x*lan966x=port->lan966x;+structfdma*fdma=&lan966x->rx.fdma;+structxdp_buffxdp;+u32act;++if(!xdp_prog)+returnFDMA_PASS;++xdp_init_buff(&xdp,fdma->db_size,&port->xdp_rxq);++/* hard_start is set to slot start (virt_addr is XDP_PACKET_HEADROOM+*intotheslot).HeadroomincludestheIFH;BPFmaygrowintoit+*viaadjust_head.IFHisrebuiltonXDP_TXandunreadonXDP_PASS.+*/+xdp_prepare_buff(&xdp,+data-XDP_PACKET_HEADROOM,+XDP_PACKET_HEADROOM+IFH_LEN_BYTES,+data_len,+false);++act=bpf_prog_run_xdp(xdp_prog,&xdp);++*xdp_data=xdp.data;+*xdp_len=xdp.data_end-xdp.data;++switch(act){+caseXDP_PASS:+returnFDMA_PASS;+caseXDP_TX:+returnlan966x_fdma_pci_xmit_xdpf(port,*xdp_data,*xdp_len)?+FDMA_DROP:FDMA_TX;+default:+bpf_warn_invalid_xdp_action(port->dev,xdp_prog,act);+fallthrough;+caseXDP_ABORTED:+trace_xdp_exception(port->dev,xdp_prog,act);+fallthrough;+caseXDP_DROP:+returnFDMA_DROP;+}+}++staticintlan966x_fdma_pci_rx_check_frame(structlan966x_rx*rx,u64*src_port,+void**data,u32*data_len){structlan966x*lan966x=rx->lan966x;structfdma*fdma=&rx->fdma;
@@ -158,38 +275,33 @@ static int lan966x_fdma_pci_rx_check_frame(struct lan966x_rx *rx, u64 *src_port)if(!lan966x_fdma_pci_rx_size_fits(fdma,blockl))returnFDMA_ERROR;-returnFDMA_PASS;+/* Present the Ethernet frame (no IFH, no FCS). HW re-inserts the+*FCSonTX;seelan966x_fdma_pci_xmit_xdpf().Maybeoverridden+*byXDP.TheFCSstripisunconditionalbecauseNETIF_F_RXFCS+*isnotadvertisedinhw_features.+*/+*data=virt_addr+IFH_LEN_BYTES;+*data_len=blockl-IFH_LEN_BYTES-ETH_FCS_LEN;++returnlan966x_xdp_pci_run(port,virt_addr,*data_len,data,data_len);}staticstructsk_buff*lan966x_fdma_pci_rx_get_frame(structlan966x_rx*rx,-u64src_port)+u64src_port,void*data,+u32data_len){structlan966x*lan966x=rx->lan966x;-structfdma*fdma=&rx->fdma;structsk_buff*skb;-structfdma_db*db;-u32data_len;--/* Get the received frame and create an SKB for it. */-db=fdma_db_next_get(fdma);-data_len=FDMA_DCB_STATUS_BLOCKL(db->status);skb=napi_alloc_skb(&lan966x->napi,data_len);if(unlikely(!skb))returnNULL;-memcpy(skb->data,-fdma_dataptr_virt_addr_contiguous(fdma,-fdma->dcb_index,-fdma->db_index),-data_len);+memcpy(skb->data,data,data_len);skb_put(skb,data_len);skb->dev=lan966x->ports[src_port]->dev;-skb_pull(skb,IFH_LEN_BYTES);--skb_trim(skb,skb->len-ETH_FCS_LEN);skb->protocol=eth_type_trans(skb,skb->dev);
@@ -277,6 +389,8 @@ static int lan966x_fdma_pci_napi_poll(struct napi_struct *napi, int weight)structsk_buff*skb;intcounter=0;u64src_port;+u32data_len;+void*data;/* Wake any stopped TX queues if a TX DCB is available. */spin_lock(&lan966x->tx_lock);
@@ -293,7 +407,10 @@ static int lan966x_fdma_pci_napi_poll(struct napi_struct *napi, int weight)/* Order DONE read before DCB/frame reads below. */dma_rmb();counter++;-switch(lan966x_fdma_pci_rx_check_frame(rx,&src_port)){+switch(lan966x_fdma_pci_rx_check_frame(rx,+&src_port,+&data,+&data_len)){caseFDMA_PASS:break;caseFDMA_ERROR:
@@ -302,8 +419,17 @@ static int lan966x_fdma_pci_napi_poll(struct napi_struct *napi, int weight)*/fdma_dcb_advance(fdma);continue;+caseFDMA_TX:+fdma_dcb_advance(fdma);+continue;+caseFDMA_DROP:+fdma_dcb_advance(fdma);+continue;}-skb=lan966x_fdma_pci_rx_get_frame(rx,src_port);+skb=lan966x_fdma_pci_rx_get_frame(rx,+src_port,+data,+data_len);fdma_dcb_advance(fdma);if(!skb){lan966x->ports[src_port]->dev->stats.rx_dropped++;
@@ -874,11 +874,13 @@ static int lan966x_probe_port(struct lan966x *lan966x, u32 p,port->phylink=phylink;-/* XDP is not supported on the PCIe FDMA path. */-if(lan966x->fdma&&!lan966x_is_pci(lan966x))-dev->xdp_features=NETDEV_XDP_ACT_BASIC|-NETDEV_XDP_ACT_REDIRECT|-NETDEV_XDP_ACT_NDO_XMIT;+if(lan966x->fdma){+dev->xdp_features=NETDEV_XDP_ACT_BASIC;++if(!lan966x_is_pci(lan966x))+dev->xdp_features|=NETDEV_XDP_ACT_REDIRECT|+NETDEV_XDP_ACT_NDO_XMIT;+}err=register_netdev(dev);if(err){
@@ -20,16 +20,16 @@ static int lan966x_xdp_setup(struct net_device *dev, struct netdev_bpf *xdp)return-EOPNOTSUPP;}-if(lan966x_is_pci(lan966x)){-NL_SET_ERR_MSG_MOD(xdp->extack,-"XDP is not supported on the PCIe FDMA path");-return-EOPNOTSUPP;-}-old_xdp=lan966x_xdp_present(lan966x);old_prog=xchg(&port->xdp_prog,xdp->prog);new_xdp=lan966x_xdp_present(lan966x);+/* PCIe FDMA uses contiguous buffers, so no page_pool reload+*isneeded.+*/+if(lan966x_is_pci(lan966x))+gotoout;+if(old_xdp==new_xdp)gotoout;
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:18
The ATU outbound windows used by the FDMA engine are programmed through
registers at offset 0x400000+, which falls outside the current cpu reg
mapping. Extend the cpu reg size from 0x100000 (1MB) to 0x800000 (8MB)
to cover the full PCIE DBI and iATU register space.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/misc/lan966x_pci.dtso | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-09 13:01:23
Add the fdma interrupt (OIC interrupt 14) to the lan966x PCI device
tree overlay, enabling FDMA-based frame injection/extraction when
the switch is connected over PCIe.
Tested-by: Herve Codina <herve.codina@bootlin.com>
Signed-off-by: Daniel Machon <daniel.machon@microchip.com>
---
drivers/misc/lan966x_pci.dtso | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 3 · Low: 3
- [Medium] fdma_pci_atu_region_map() (EXPORT_SYMBOL_GPL) is the primitive
that programs the ATU limit register (region->limit_addr =…
- [Medium] fdma_pci_atu_region_enable() writes the region enable bit
(BIT(31) at offset 0x4) and returns immediately, and…
- [Medium] fdma_pci_atu_configure_translation() programs lower/upper base
(0x8/0xc), limit (0x10), lower/upper target (0x14/0x18) and the enable…
- [Low] The commit message says the patch "effectively add[s] support for
PCIe FDMA", but at this commit nothing in the tree calls…
- [Low] fdma_pci_atu_init() is the designated initializer of struct
fdma_pci_atu (it even programs hardware, disabling all six outbound…
- [Low] fdma_pci_atu_translate_addr() reads region->base_addr and
region->target_addr with no lock, no READ_ONCE() and no in_use…
--
net: microchip: fdma: add PCIe ATU support
This adds a new fdma_pci.c/fdma_pci.h library that carves the PCIe endpoint
outbound address space (0x10000000-0x1fffffff) into six equally sized ATU
regions, and adds fdma_alloc_coherent_and_map()/fdma_free_coherent_and_unmap()
helpers that pair a coherent DMA allocation with an ATU region mapping.
A few questions below.
Add the required functions and helpers that combine the DMA allocation
with the ATU region mapping, effectively adding support for PCIe FDMA.
[Severity: Low]
Is "effectively adding support for PCIe FDMA" the right wording for this
patch on its own?
At this commit nothing in the tree calls fdma_pci_atu_init(),
fdma_pci_atu_region_map(), fdma_pci_atu_translate_addr(),
fdma_alloc_coherent_and_map() or fdma_free_coherent_and_unmap(), and
FDMA_PCI_DB_ALIGN/FDMA_PCI_DB_SIZE() have no user either.
The wiring appears later in the series, in "net: lan966x: add PCIe FDMA
support" and its follow-ups (lan966x_fdma_pci.c calls fdma_pci_atu_init(),
fdma_alloc_coherent_and_map(), fdma_free_coherent_and_unmap(),
fdma_pci_atu_translate_addr() and FDMA_PCI_DB_SIZE()).
Would "add the infrastructure for PCIe FDMA" describe this patch more
accurately?
@@ -127,6 +127,50 @@ void fdma_free_phys(struct fdma *fdma)}EXPORT_SYMBOL_GPL(fdma_free_phys);+#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+/* Allocate coherent DMA memory and map it in the ATU. */+intfdma_alloc_coherent_and_map(structdevice*dev,structfdma*fdma,+structfdma_pci_atu*atu)+{+structfdma_pci_atu_region*region;+interr;++if(WARN_ON(fdma->atu_region))+return-EBUSY;++/* The ATU cannot express a limit finer than the region granularity, so+*thehardwarewidenstheprogrammedlimittothatboundary.Padthe+*allocationtomatch,ortheoutboundwindowwouldextendpastthe+*memoryweown.+*/+fdma->size=ALIGN(fdma->size,FDMA_PCI_ATU_REGION_ALIGN);++err=fdma_alloc_coherent(dev,fdma);+if(err)+returnerr;++region=fdma_pci_atu_region_map(atu,fdma->dma,fdma->size);+if(IS_ERR(region)){+fdma_free_coherent(dev,fdma);+returnPTR_ERR(region);+}
[Severity: Medium]
Does the enable bit take effect by the time this writel() retires?
The register block looks like the DesignWare unrolled iATU: the 0x300000
base matches DEFAULT_DBI_ATU_OFFSET, the 0x200 stride matches
PCIE_ATU_UNROLL_BASE(dir, index) = index << 9, and 0x0/0x4/0x8/0xc/0x10/
0x14/0x18 match PCIE_ATU_UNR_REGION_CTRL1/CTRL2/LOWER_BASE/UPPER_BASE/
LOWER_LIMIT/LOWER_TARGET/UPPER_TARGET with PCIE_ATU_ENABLE = BIT(31) in
CTRL2.
For that IP, dw_pcie_prog_outbound_atu() in
drivers/pci/controller/dwc/pcie-designware.c polls until the enable bit
reads back:
for (retries = 0; retries < LINK_WAIT_MAX_IATU_RETRIES; retries++) {
val = dw_pcie_readl_atu(pci, dir, index, PCIE_ATU_REGION_CTRL2);
if (val & PCIE_ATU_ENABLE)
return 0;
mdelay(LINK_WAIT_IATU);
}
dev_err(pci->dev, "Outbound iATU is not being enabled\n");
return -ETIMEDOUT;
with the comment "Make sure ATU enable takes effect before any subsequent
config and I/O accesses".
Here fdma_pci_atu_region_map() returns the region as usable right after
this write, and the caller programs FDMA descriptors with
region->base_addr. Should the enable be read back (and a failure to arm
reported to the caller) instead of being fire-and-forget?
quoted hunk
+/* Configure the address translation in the ATU. */+static void+fdma_pci_atu_configure_translation(struct fdma_pci_atu_region *region)+{+ struct fdma_pci_atu *atu = region->atu;+ int idx = region->idx;++ writel(lower_32_bits(region->base_addr),+ fdma_pci_atu_addr_get(atu->addr,+ FDMA_PCI_ATU_LWR_BASE_ADDR, idx));++ writel(upper_32_bits(region->base_addr),+ fdma_pci_atu_addr_get(atu->addr,+ FDMA_PCI_ATU_UPP_BASE_ADDR, idx));++ /* Upper limit register only needed with REGION_SIZE > 4GB. */+ writel(region->limit_addr,+ fdma_pci_atu_addr_get(atu->addr, FDMA_PCI_ATU_LIMIT_ADDR, idx));
[Severity: Medium]
Should region control 1 (offset 0x0) be programmed here too?
Everything except CTRL1 is written: base, limit, target and the enable bit
in CTRL2. On the DWC unrolled iATU, CTRL1 holds the outbound TLP TYPE (MEM
= 0), TD, PCIE_ATU_FUNC_NUM(pf) and PCIE_ATU_INCREASE_REGION_SIZE
(BIT(13)). dw_pcie_prog_outbound_atu() always writes CTRL1 before enabling
CTRL2:
val = type | PCIE_ATU_FUNC_NUM(func_no);
...
dw_pcie_writel_atu(pci, dir, index, PCIE_ATU_REGION_CTRL1, val);
dw_pcie_writel_atu(pci, dir, index, PCIE_ATU_REGION_CTRL2,
PCIE_ATU_ENABLE);
There is no define for offset 0x0 in this file, and fdma_pci_atu_init()
only clears the enable bit, so each window is armed with whatever CTRL1
value reset, the endpoint bootloader, or a previous OS instance left in
place. With a residual TYPE the FDMA writes go out as the wrong TLP type,
with a stale FUNC_NUM they carry the wrong requester, and with a stale
INCREASE_REGION_SIZE the window end is taken from a register this code
never writes.
Related, is the comment "Upper limit register only needed with REGION_SIZE
4GB" accurate here? That mode is selected by CTRL1 BIT(13), which is
neither set nor cleared, and the upper limit register is never written.
[ ... ]
quoted hunk
+/* Find an unused ATU region. */+static struct fdma_pci_atu_region *+fdma_pci_atu_region_get_free(struct fdma_pci_atu *atu)+{+ struct fdma_pci_atu_region *regions = atu->regions;++ for (int i = 0; i < FDMA_PCI_ATU_REGION_MAX; i++) {+ if (regions[i].in_use)+ continue;++ return ®ions[i];+ }++ return ERR_PTR(-ENOSPC);+}
[ ... ]
quoted hunk
+/* Map a host DMA address into a free outbound region. */+struct fdma_pci_atu_region *+fdma_pci_atu_region_map(struct fdma_pci_atu *atu, u64 target_addr, int size)+{+ struct fdma_pci_atu_region *region;++ if (!atu)+ return ERR_PTR(-EINVAL);++ if (size <= 0)+ return ERR_PTR(-EINVAL);++ if (size > fdma_pci_atu_region_size())+ return ERR_PTR(-ERANGE);++ /* The ATU region base is only ever aligned to FDMA_PCI_ATU_REGION_ALIGN;+ * require the same alignment of the host target address, since the ATU+ * translates addr - target_addr + base_addr and any misalignment here+ * would shift every translated address by the same amount.+ */+ if (!IS_ALIGNED(target_addr, FDMA_PCI_ATU_REGION_ALIGN))+ return ERR_PTR(-EINVAL);
[Severity: Medium]
Should this validation block also reject a size that is not a multiple of
FDMA_PCI_ATU_REGION_ALIGN?
fdma_pci.h states the rule for both halves:
/* Outbound regions are 64KB granular (datasheet section 3.24.7.4.1), so both
* the region base and the mapped size must be aligned to this.
*/
#define FDMA_PCI_ATU_REGION_ALIGN BIT(16)
and the commit message names the consequence of not honouring it: "the
outbound window would extend past the memory the host allocated for DMA".
The size given here becomes the programmed limit:
region->limit_addr = region->base_addr + size - 1;
so with, say, a PAGE_SIZE-aligned 0x21000 from fdma_get_size() the hardware
widens the limit to the next 64KB boundary and the enabled outbound window
covers up to 64KB-1 of host memory beyond the buffer, which the FDMA engine
can then write into.
The 64KB padding is only done in the convenience wrapper
fdma_alloc_coherent_and_map(), not in this exported primitive that
programs the register, and the commit message advertises the primitive for
reuse by lan969x. Would an IS_ALIGNED(size, FDMA_PCI_ATU_REGION_ALIGN)
check (or rounding here) be better placed alongside the target_addr check?
Also, the header comment for fdma_pci_atu_region_map() is just "Map a host
DMA address into a free ATU region", with no mention that a target_addr
that is not 64KB aligned is rejected with -EINVAL. Could that requirement
be documented at the prototype?
quoted hunk
++ mutex_lock(&atu->lock);++ region = fdma_pci_atu_region_get_free(atu);+ if (IS_ERR(region)) {+ mutex_unlock(&atu->lock);+ return region;+ }++ region->target_addr = target_addr;+ region->limit_addr = region->base_addr + size - 1;+ region->in_use = true;++ fdma_pci_atu_configure_translation(region);+ fdma_pci_atu_region_enable(region);++ mutex_unlock(&atu->lock);++ return region;+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_region_map);++/* Translate a host DMA address to the corresponding OB address. */+u64 fdma_pci_atu_translate_addr(struct fdma_pci_atu_region *region, u64 addr)+{+ return region->base_addr + (addr - region->target_addr);+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_translate_addr);
[Severity: Low]
What serializes these reads against the writers?
fdma_pci_atu_region_map() and fdma_pci_atu_region_unmap() mutate
target_addr/in_use and the hardware window under the sleeping atu->lock,
while this helper reads base_addr/target_addr with no lock, no READ_ONCE()
and no in_use check. Its intended callers are descriptor callbacks
(lan966x_fdma_pci_dataptr_cb()/nextptr_cb(), reached from NAPI poll and
from lan966x_fdma_pci_xmit()), which cannot take a mutex.
Looking at the end of the series, the writers are
lan966x_fdma_pci_reload() and lan966x_fdma_pci_deinit(), both of which
first do napi_disable() and lan966x_fdma_tx_disable_netdev() (netif_tx_disable()
drains in-flight ndo_start_xmit callers), and both are under rtnl, so there
does not seem to be a reachable race today.
Given that, would it help to document at the prototype that the caller must
quiesce DMA and the descriptor paths before unmapping a region, since this
is an exported helper?
quoted hunk
++/* Initialize ATU, dividing the OB space into equally sized regions. */+void fdma_pci_atu_init(struct fdma_pci_atu *atu, void __iomem *addr)+{+ struct fdma_pci_atu_region *regions = atu->regions;+ u32 region_size = fdma_pci_atu_region_size();++ atu->addr = addr;+ mutex_init(&atu->lock);++ for (int i = 0; i < FDMA_PCI_ATU_REGION_MAX; i++) {+ regions[i].base_addr =+ FDMA_PCI_ATU_OB_START + (i * region_size);+ regions[i].idx = i;+ regions[i].atu = atu;++ fdma_pci_atu_region_disable(®ions[i]);+ }+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_init);
[Severity: Low]
Should this also initialize in_use, target_addr and limit_addr?
This is the designated initializer for struct fdma_pci_atu and it even
touches hardware (disabling all six windows), but it only sets base_addr,
idx and atu. The allocation state that fdma_pci_atu_region_get_free()
consumes is in_use, which is never assigned here, so the helper silently
depends on the embedding structure having been zeroed. If in_use held
garbage, every region would look busy and mapping would fail with -ENOSPC.
The only in-tree caller later in the series passes &lan966x->atu, and
lan966x comes from devm_kzalloc() in lan966x_probe(), so it works today.
Would it be worth clearing the per-region software state here (or noting
the "caller must zero the struct" precondition at the prototype)?
There is also no counterpart to this function: nothing resets the software
state, unmaps still-mapped regions, or calls mutex_destroy(). Is a
fdma_pci_atu_deinit() wanted for symmetry?
+/* Outbound regions are 64KB granular (datasheet section 3.24.7.4.1), so both+ * the region base and the mapped size must be aligned to this.+ */+#define FDMA_PCI_ATU_REGION_ALIGN BIT(16)
[ ... ]
quoted hunk
+/* Initialize ATU, dividing OB space into regions. */+void fdma_pci_atu_init(struct fdma_pci_atu *atu, void __iomem *addr);++/* Unmap an ATU region, clearing its translation and disabling it. */+void fdma_pci_atu_region_unmap(struct fdma_pci_atu_region *region);++/* Map a host DMA address into a free ATU region. */+struct fdma_pci_atu_region *fdma_pci_atu_region_map(struct fdma_pci_atu *atu,+ u64 target_addr,+ int size);
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 1
- [Low] Commit-message/comment vs. reachability at this revision: the
added FDMA sticky clear in lan966x_reset_switch()…
--
net: lan966x: clear FDMA interrupt stickies after switch reset
The patch acks FDMA_ERRORS, FDMA_INTR_ERR and FDMA_INTR_DB right after
reset_control_reset() in lan966x_reset_switch(), so that spurious sticky
bits latched by the GCB soft reset in PCI mode do not make the FDMA IRQ
handler fire as soon as the IRQ is requested. The clear is unconditional and
is described as a no-op on the platform path.
@@ -1067,6 +1067,15 @@ static int lan966x_reset_switch(struct lan966x *lan966x)reset_control_reset(switch_reset);+/* When in PCI mode, the GCB soft reset issued by the reset+*controllercanlatchspuriousbitsintheFDMAerrorstickies.+*Clearthembeforerequest_irqhooksuptheFDMAIRQline,+*otherwisethehandlerfiresimmediatelyonprobe.+*/+lan_wr(lan_rd(lan966x,FDMA_ERRORS),lan966x,FDMA_ERRORS);+lan_wr(lan_rd(lan966x,FDMA_INTR_ERR),lan966x,FDMA_INTR_ERR);+lan_wr(lan_rd(lan966x,FDMA_INTR_DB),lan966x,FDMA_INTR_DB);
[Severity: Low]
This isn't a bug in the register writes themselves, but is the failure
described in the changelog reachable at this point in the series?
The commit message says the latched bits "sit in FDMA_INTR_ERR until the FDMA
IRQ is requested later in probe, at which point the handler fires immediately
and WARNs". In lan966x_probe() the FDMA handler is only installed when a named
"fdma" interrupt exists:
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
err = devm_request_irq(&pdev->dev, lan966x->fdma_irq,
lan966x_fdma_irq_handler, 0,
"fdma irq", lan966x);
...
lan966x->fdma = true;
}
At this commit the PCI overlay in drivers/misc/lan966x_pci.dtso does not
describe that interrupt:
interrupts = <12 IRQ_TYPE_LEVEL_HIGH>,
<9 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "xtr", "ana";
so platform_get_irq_byname(pdev, "fdma") fails and lan966x_fdma_irq_handler()
is never requested on the PCI path.
The overlay also disagrees with the positional list in
Documentation/devicetree/bindings/net/microchip,lan966x-switch.yaml, where the
second entry is reserved for "fdma":
interrupt-names:
minItems: 1
items:
- const: xtr
- const: fdma
- const: ana
Both points are resolved later in the same series by "misc: lan966x-pci: dts:
add fdma interrupt to overlay", which switches the overlay to
interrupts <12>,<14>,<9> and interrupt-names = "xtr", "fdma", "ana", so this is
only an observation about the ordering of the changelog against the code state
here rather than an actionable defect.
Would it help to mention in the commit message that the WARN only becomes
reachable once the overlay gains the "fdma" interrupt?
/* Don't reinitialize the switch core, if it is already initialized. In
* case it is initialized twice, some pointers inside the queue system
* in HW will get corrupted and then after a while the queue system gets
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 4 · Low: 1
- [Medium] lan966x_shutdown() leaves the shared NAPI permanently disabled
(napi_disable() with no napi_enable() and no state marker) while the…
- [Medium] lan966x_shutdown() quiesces TX only with napi_disable() +
netif_tx_disable() and then manipulates FDMA TX state…
- [Medium] lan966x_shutdown() masks the analyzer source with a plain
lan_wr(0, ANA_ANAINTR) while the ANA interrupt is still requested and…
- [Medium] The new shutdown path relies on
lan966x_fdma_rx_disable()/lan966x_fdma_tx_disable() to prove the…
- [Low] Claim: the `if (!lan966x->fdma) return;` guard in
lan966x_shutdown() skips the ANA_ANAINTR mask, which lan966x_init()…
--
net: lan966x: add shutdown callback to stop FDMA on reboot
Adds a platform shutdown() callback that disables NAPI, stops the netdev
TX queues, disables the RX and TX FDMA channels and masks FDMA_INTR_ENA,
FDMA_INTR_DB_ENA and ANA_ANAINTR so a card that survives a host reboot
does not assert the shared PCIe INTx before re-probe. FDMA_INTR_ENA is
also re-armed in lan966x_fdma_rx_start().
A few questions about the new shutdown path below.
@@ -325,6 +329,21 @@ static void lan966x_fdma_stop_netdev(struct lan966x *lan966x) } }+/* Drain in-flight xmit callers and stop all TX queues on every port. */+void lan966x_fdma_tx_disable_netdev(struct lan966x *lan966x)+{+ struct lan966x_port *port;+ int i;++ for (i = 0; i < lan966x->num_phys_ports; ++i) {+ port = lan966x->ports[i];+ if (!port)+ continue;++ netif_tx_disable(port->dev);+ }+}
[Severity: Medium]
Does netif_tx_disable() really drain every in-flight xmit caller here?
lan966x also registers .ndo_xdp_xmit = lan966x_xdp_xmit in
lan966x_port_netdev_ops, and that entry point is dispatched from a foreign
device's NAPI/softirq (bq_xmit_all()) without the netdev txq locks and
without regard for the txq stopped state or lan966x's NAPI state:
lan966x_xdp_xmit()
lan966x_fdma_xmit_xdpf()
spin_lock(&lan966x->tx_lock);
...
/* Start the transmission */
lan966x_fdma_tx_start(tx);
lan966x_fdma_tx_start() calls lan966x_fdma_tx_activate() when
tx->activated is false, and that re-sets BIT(channel_id) in
FDMA_INTR_DB_ENA and writes FDMA_CH_ACTIVATE. lan966x_fdma_tx_disable()
in the new shutdown path sets tx->activated = false, and lan966x_shutdown()
touches FDMA_CH_DISABLE, tx->activated and FDMA_INTR_DB_ENA without taking
lan966x->tx_lock.
If an XDP program on another interface redirects into a lan966x port in the
reboot window, can the TX channel end up re-activated (and the DB enable
bit re-set) after shutdown() returns? The shutdown path deliberately keeps
the DCB rings mapped, so a re-activated channel would keep fetching DCBs
from and writing DCB status into host memory that the next kernel reuses,
which is the state the commit message says it eliminates.
@@ -1324,9 +1324,35 @@ static void lan966x_remove(struct platform_device *pdev)debugfs_remove_recursive(lan966x->debugfs_root);}+staticvoidlan966x_shutdown(structplatform_device*pdev)+{+structlan966x*lan966x=platform_get_drvdata(pdev);++if(!lan966x->fdma)+return;++/* The reload paths disable this NAPI under rtnl; serialize with them. */+rtnl_lock();++if(lan966x->fdma_ndev)+napi_disable(&lan966x->napi);
[Severity: Medium]
The NAPI is disabled here and never re-enabled, and no state is recorded to
say the device has been shut down. After rtnl_unlock() the netdevs are
still registered and their ndo callbacks are still live.
Can a later MTU change on any port hang?
lan966x_port_change_mtu() /* .ndo_change_mtu */
lan966x->ops->fdma_resize()
lan966x_fdma_change_mtu()
__lan966x_fdma_reload()
lan966x_fdma_reload()
napi_synchronize(&lan966x->napi);
napi_disable(&lan966x->napi);
The second napi_disable() lands in napi_disable_locked(), which spins:
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
NAPIF_STATE_SCHED was set by the napi_disable() in lan966x_shutdown() and
is only cleared by napi_enable(), which shutdown() never calls.
Wouldn't the netlink task then loop with no timeout and no alternative wake
condition, while holding rtnl_mutex, blocking any later rtnl user in the
reboot sequence (including a second instance's lan966x_shutdown(), which
also takes rtnl_lock())? device_shutdown() runs from
kernel_restart_prepare() with userspace still runnable, and an XDP attach
reaches the same napi_disable() through lan966x_xdp_setup() ->
lan966x_fdma_reload_page_pool() -> __lan966x_fdma_reload().
[Severity: Medium]
The commit message says "fdma_deinit() disables both channels and waits for
them to go idle", and shutdown() relies on the same two helpers for that
guarantee. Both of them drop the poll result though:
lan966x_fdma_rx_disable() and lan966x_fdma_tx_disable() {
readx_poll_timeout_atomic(lan966x_fdma_channel_active, lan966x,
val, !(val & BIT(fdma->channel_id)),
READL_SLEEP_US, READL_TIMEOUT_US);
}
Is a timeout on FDMA_CH_ACTIVE distinguishable from a channel that really
went idle here? lan966x_fdma_tx_disable() sets tx->activated = false
regardless, and shutdown() then masks FDMA_INTR_ENA/FDMA_INTR_DB_ENA, which
hides the error sources that would report the still-active channel, and
returns.
Unlike the unbind path, shutdown() keeps the RX pages, the coherent DCB
rings and (on the PCIe backend) the outbound ATU window mapped, so a
channel that never reached the idle state keeps write access to memory the
next kernel will reuse. Would it make sense to check the poll return and
at least report it?
[Severity: Medium]
Can the ANA_ANAINTR mask be undone right after it is written? The ANA irq
is still requested and enabled at this point, and it is a threaded handler
(devm_request_threaded_irq() with lan966x_ana_irq_handler and IRQF_ONESHOT
in lan966x_probe()), whose tail does an unlocked read-modify-write of the
same register:
lan966x_mac_irq_handler() {
...
lan_rmw(ANA_ANAINTR_INTR_SET(0),
ANA_ANAINTR_INTR,
lan966x, ANA_ANAINTR);
}
lan_rmw() is readl() -> modify -> writel() and preserves ANA_ANAINTR_INTR_ENA
from the value it read. If the handler thread has already done its readl()
(with INTR_ENA still 1) when shutdown() writes 0, wouldn't its write-back
restore ANA_ANAINTR_INTR_ENA = 1?
shutdown() holds only rtnl, which the threaded handler never takes, and it
neither disable_irq()s nor frees the ANA irq, so the source stays mapped
and unmasked. The switch keeps learning and forwarding after the host
reboots, so MAC-table sync events keep arriving. Would a
disable_irq()/synchronize_irq() on lan966x->ana_irq before the mask, or a
shared lock with the handler, close that window?
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 0 (1) · Medium: 1 (1) · Low: 3
- [Medium] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
Pre-existing issues:
- [High] Use-after-free in the new PCIe receive path:
`lan966x_fdma_pci_rx_get_frame()`…
- [Medium] DISCARDED — pre-existing shared-library ordering weakness.
--
net: lan966x: add PCIe FDMA support
Adds a second FDMA backend for lan966x used when the switch is reached over
PCIe, using contiguous ATU-mapped DMA buffers and memcpy-based frame
transfer instead of per-page mappings. XDP and PTP are not advertised or
allowed on this path.
[Severity: High]
This isn't a bug introduced by this patch - lan966x_fdma_rx_get_frame() in
lan966x_fdma.c has the same caller pattern - but can lan966x_fdma_pci_rx_get_frame()
touch skb after lan966x_hw_offload() has already freed it?
lan966x_hw_offload() replaces the buffer and reports only a boolean:
drivers/net/ethernet/microchip/lan966x/lan966x_main.c:lan966x_hw_offload() {
...
if (eth_type_vlan(skb->protocol)) {
skb = skb_vlan_untag(skb);
if (unlikely(!skb))
return false;
}
...
}
and skb_vlan_untag() frees the caller's skb on failure:
net/core/skbuff.c:skb_vlan_untag() {
...
if (unlikely(!pskb_may_pull(skb, VLAN_HLEN + sizeof(unsigned short))))
goto err_free;
...
err_free:
kfree_skb(skb);
return NULL;
}
So on the false return the caller's pointer is dangling, and
lan966x_fdma_pci_rx_get_frame() then does:
skb->offload_fwd_mark = 0;
skb->dev->stats.rx_bytes += skb->len;
skb->dev->stats.rx_packets++;
return skb;
with the returned skb going to napi_gro_receive() in
lan966x_fdma_pci_napi_poll().
Is the new lower bound in lan966x_fdma_pci_rx_size_fits() wide enough to let
this happen? IFH_LEN_BYTES + ETH_HLEN + ETH_FCS_LEN is 46, so a BLOCKL of
46..51 is accepted, and after skb_pull(IFH_LEN_BYTES),
skb_trim(skb->len - ETH_FCS_LEN) and eth_type_trans() there are only 0..5
bytes left. For a frame with h_proto 0x8100 on a bridged port that has IGMP
or MLD redirection enabled, the pskb_may_pull(VLAN_HLEN + 2) inside
skb_vlan_untag() then fails and the buffer is freed.
Would it make sense to have lan966x_hw_offload() take a struct sk_buff **,
or to have the caller stop using skb once it returns false?
static int lan966x_fdma_pci_xmit(struct sk_buff *skb, __be32 *ifh,
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 2 · Medium: 1 · Low: 1
- [High] NULL pointer dereference in the new lan966x_fdma_pci_reset_mem()
restore path.
- [High] Unbalanced/unguarded napi_disable()/napi_enable() in the new MTU
reload path.
- [Medium] Netdev MTU contract mismatch on the PCIe FDMA path.
- [Low] Inverted restart ordering in lan966x_fdma_pci_reload(): both the
success path (lines 458-460) and the restore path (lines 476-478) call…
--
net: lan966x: add PCIe FDMA MTU change support
This adds MTU change support to the PCIe FDMA path by reallocating the
contiguous ATU-mapped RX and TX buffers, falling back to resetting and
reusing the existing buffers if the reallocation fails. It also caps the
PCIe DCB ring at 256 entries so the contiguous allocation stays below
MAX_PAGE_ORDER at jumbo MTU.
A few questions about the new reload and resize paths below.
@@ -1,8 +1,15 @@// SPDX-License-Identifier: GPL-2.0++#include<linux/mmzone.h>+#include"fdma_api.h"#include"lan966x_main.h"+/* Ring must fit in one MAX_PAGE_ORDER DMA block; 512 DCBs overflows+*atjumboMTU.+*/+#define FDMA_PCI_DCB_MAX 256+
[Severity: High]
Can tx->fdma.atu_region be NULL here?
lan966x_fdma_pci_reset_mem() is only reached from the restore label of
lan966x_fdma_pci_reload(), and that label is taken when the ring
allocation failed. Probe registers the netdevs from
lan966x_probe_port() -> register_netdev() inside the ports loop, while
lan966x->fdma is already true, and ops->fdma_init() (which sets
n_dcbs/db_size/size and maps the ATU regions) only runs after that loop.
An MTU change in that window passes the !lan966x->fdma guard in
lan966x_port_change_mtu() and reaches lan966x_fdma_pci_resize() with a
zeroed fdma:
rx.max_mtu == 0, so the "max_mtu == lan966x->rx.max_mtu" early return
does not fire
n_dcbs == 0, so fdma_get_size_contiguous() returns ALIGN(0, PAGE_SIZE)
== 0 and both -ERANGE guards pass
lan966x_fdma_pci_reload() then recomputes size, which stays 0, and:
lan966x_fdma_pci_rx_alloc()
fdma_alloc_coherent_and_map()
fdma_pci_atu_region_map()
if (size <= 0)
return ERR_PTR(-EINVAL);
so control reaches restore, memcpy's the still-zeroed fdma structs back
(atu_region == NULL, dcbs == NULL, size == 0), and calls reset_mem().
memset(NULL, 0, 0) and fdma_dcbs_init() with n_dcbs == 0 are both
no-ops, so nothing stops execution before tx->fdma.atu_region->base_addr
is evaluated. Would a NULL check on atu_region (or an early bail in
resize() when the FDMA is not initialized yet) be appropriate here?
quoted hunk
+ lan966x_fdma_llp_configure(lan966x,+ rx->fdma.atu_region->base_addr,+ rx->fdma.channel_id);+}++/* Wake all TX queues on every port (undoes lan966x_fdma_tx_disable_netdev). */+static void lan966x_fdma_pci_wakeup_netdev(struct lan966x *lan966x)+{+ for (int i = 0; i < lan966x->num_phys_ports; ++i) {+ struct lan966x_port *port = lan966x->ports[i];++ if (port)+ netif_tx_wake_all_queues(port->dev);+ }+}++static int lan966x_fdma_pci_reload(struct lan966x *lan966x, int new_mtu)+{+ struct fdma tx_fdma_old = lan966x->tx.fdma;+ struct fdma rx_fdma_old = lan966x->rx.fdma;+ u32 old_mtu = lan966x->rx.max_mtu;+ int err;++ napi_disable(&lan966x->napi);
[Severity: High]
Should this napi_disable() be guarded the way the other users of
lan966x->napi in this driver are?
lan966x_fdma_pci_deinit() does:
if (lan966x->fdma_ndev)
napi_disable(&lan966x->napi);
and lan966x_shutdown() has the same guard, with a comment noting that the
reload paths disable this NAPI under rtnl. Two states look problematic
for the unguarded call:
The NAPI may not have been added yet. netif_napi_add() only runs from
lan966x_fdma_netdev_init(), called by lan966x_port_init(), which happens
after lan966x_probe_port() already did register_netdev(). An MTU change
in that window reaches napi_disable() with n->dev == NULL (lan966x is
devm_kzalloc'ed), and napi_disable() does netdev_lock(n->dev).
The NAPI may already be disabled. lan966x_remove() calls
ops->fdma_deinit() (which disables the NAPI and frees/unmaps both rings)
before lan966x_cleanup_ports() unregisters the netdevs, and
lan966x_shutdown() disables the NAPI without clearing fdma_ndev. A
concurrent MTU change then calls napi_disable() a second time and
napi_disable_locked() spins:
net/core/dev.c:napi_disable_locked() {
...
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
...
}
There is no timeout and no other wake condition, and ndo_change_mtu
holds rtnl throughout, which also blocks the unregister_netdev() that
would end the window. The matching napi_enable() calls on both exit
paths below have the same issue.
quoted hunk
+ lan966x_fdma_tx_disable_netdev(lan966x);+ lan966x_fdma_rx_disable(&lan966x->rx);+ lan966x_fdma_tx_disable(&lan966x->tx);++ lan966x->rx.max_mtu = new_mtu;++ /* Must be NULL'ed in order to realloc them. */+ lan966x->rx.fdma.atu_region = NULL;+ lan966x->tx.fdma.atu_region = NULL;++ lan966x->tx.fdma.db_size = FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+ lan966x->tx.fdma.size = fdma_get_size_contiguous(&lan966x->tx.fdma);+ lan966x->rx.fdma.db_size = FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+ lan966x->rx.fdma.size = fdma_get_size_contiguous(&lan966x->rx.fdma);++ err = lan966x_fdma_pci_rx_alloc(&lan966x->rx);+ if (err)+ goto restore;++ err = lan966x_fdma_pci_tx_alloc(&lan966x->tx);+ if (err) {+ fdma_free_coherent_and_unmap(lan966x->dma_dev,+ &lan966x->rx.fdma);+ goto restore;+ }++ /* Free and unmap old memory. */+ fdma_free_coherent_and_unmap(lan966x->dma_dev, &rx_fdma_old);+ fdma_free_coherent_and_unmap(lan966x->dma_dev, &tx_fdma_old);++ napi_enable(&lan966x->napi);+ lan966x_fdma_rx_start(&lan966x->rx);+ lan966x_fdma_pci_wakeup_netdev(lan966x);
[Severity: Low]
This isn't a bug as such, but the restart order here is the reverse of
the pre-existing non-PCI path. lan966x_fdma_reload() does:
lan966x_fdma_rx_start(&lan966x->rx);
...
lan966x_fdma_wakeup_netdev(lan966x);
napi_enable(&lan966x->napi);
Once napi_enable() clears NAPI_STATE_DISABLE, a pending FDMA DB
interrupt reaching napi_schedule() from lan966x_fdma_irq_handler() can
run lan966x_fdma_pci_napi_poll() on another CPU concurrently with
lan966x_fdma_rx_start(). Both then do unsynchronized read-modify-write
on FDMA_INTR_DB_ENA:
lan966x_fdma_pci_napi_poll():
if (counter < weight && napi_complete_done(napi, counter))
lan_wr(0xff, lan966x, FDMA_INTR_DB_ENA);
lan966x_fdma_rx_start() instead does lan_rd() followed by lan_rmw() on
the same register, and the poll can also issue FDMA_CH_RELOAD for a
channel rx_start() has not activated yet. Would matching the ordering
used by lan966x_fdma_reload() be preferable?
quoted hunk
++ return err;+restore:++ /* No new buffers are allocated at this point. Use the old buffers,+ * but reset them before starting the FDMA again.+ */++ memcpy(&lan966x->tx.fdma, &tx_fdma_old, sizeof(struct fdma));+ memcpy(&lan966x->rx.fdma, &rx_fdma_old, sizeof(struct fdma));++ lan966x->rx.max_mtu = old_mtu;++ lan966x_fdma_pci_reset_mem(lan966x);++ napi_enable(&lan966x->napi);+ lan966x_fdma_rx_start(&lan966x->rx);+ lan966x_fdma_pci_wakeup_netdev(lan966x);++ return err;+}+
[ ... ]
quoted hunk
static int lan966x_fdma_pci_resize(struct lan966x *lan966x) {- return -EOPNOTSUPP;+ struct fdma rx_fdma;+ int max_mtu;++ max_mtu = lan966x_fdma_get_max_frame(lan966x);+ if (max_mtu == lan966x->rx.max_mtu)+ return 0;++ /* rx and tx have n_dbs == 1, so both rings need the same contiguous+ * dma_alloc_coherent() block, which can't exceed MAX_PAGE_ORDER. The+ * allocation is padded to the ATU region granularity, so test the+ * padded size.+ */+ rx_fdma = lan966x->rx.fdma;+ rx_fdma.db_size = FDMA_PCI_DB_SIZE(max_mtu);+ if (ALIGN(fdma_get_size_contiguous(&rx_fdma),+ FDMA_PCI_ATU_REGION_ALIGN) > (PAGE_SIZE << MAX_PAGE_ORDER))+ return -ERANGE;
[Severity: Medium]
Does the MTU range the netdev advertises still match what this path can
actually do?
lan966x_probe_port() sets dev->max_mtu = ETH_MAX_MTU for the PCIe
instantiation too, but with n_dcbs == FDMA_PCI_DCB_MAX (256),
n_dbs == FDMA_RX_DCB_MAX_DBS (1) and db_size == ALIGN(max_frame, 128),
the padded contiguous allocation crosses PAGE_SIZE << MAX_PAGE_ORDER at
roughly 15.7 KiB of MTU on a 4 KiB-page build, so larger requests get a
bare -ERANGE that lan966x_port_change_mtu() passes straight to
userspace, and the cut-off moves with PAGE_SIZE and MAX_PAGE_ORDER.
There is a second effect for MTUs in [65518, 65535], which the
advertised max_mtu still permits. lan966x_port_change_mtu() programs
the register before calling the resize callback:
lan_wr(DEV_MAC_MAXLEN_CFG_MAX_LEN_SET(LAN966X_HW_MTU(new_mtu)),
lan966x, DEV_MAC_MAXLEN_CFG(port->chip_port));
DEV_MAC_MAXLEN_CFG_MAX_LEN is GENMASK(15, 0) and LAN966X_HW_MTU(mtu) is
mtu + 18, so FIELD_PREP() masks the runtime value, e.g. mtu 65535
programs MAX_LEN = 65553 & 0xffff = 17. lan966x_fdma_pci_resize() then
derives max_mtu from that already-truncated register via
lan966x_fdma_get_max_frame() -> lan966x_fdma_get_max_mtu() ->
lan_rd(DEV_MAC_MAXLEN_CFG), so it either returns 0 (multi-port case,
where the max over ports is unchanged) or reloads with a tiny db_size,
and the MTU change is reported as successful while the MAC is configured
for a 17-byte frame.
Before this patch the PCIe callback returned -EOPNOTSUPP for every
resize, so lan966x_port_change_mtu() always rolled the register back;
this patch is what makes the path reachable. Would capping dev->max_mtu
for the PCIe path (to both the ring limit and the 16-bit MAC field) be
the better fix, so the resize-time rejection becomes unreachable?
quoted hunk
++ /* db_size is also handed to the FDMA in the 16-bit DCB DATAL field,+ * where a larger value would be silently truncated.+ */+ if (rx_fdma.db_size > GENMASK(15, 0))+ return -ERANGE;++ return __lan966x_fdma_pci_reload(lan966x, max_mtu); }
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 1 · Low: 0
- [Medium] Inconsistent XDP frame presentation between the two RX hooks
of the same driver: after this patch lan966x has two in-tree XDP RX…
--
net: lan966x: add PCIe FDMA XDP support
This adds XDP support to the lan966x PCIe FDMA path, using contiguous
ATU-mapped buffers and a memcpy-based XDP_TX instead of the page_pool
scheme used by the platform path. The IFH and FCS are stripped before
the BPF program runs, and lan966x_xdp_setup() now skips the page_pool
reload for the PCIe case. Only XDP_ACT_BASIC is advertised for PCIe.
@@ -158,38 +275,33 @@ static int lan966x_fdma_pci_rx_check_frame(struct lan966x_rx *rx, u64 *src_port) if (!lan966x_fdma_pci_rx_size_fits(fdma, blockl)) return FDMA_ERROR;- return FDMA_PASS;+ /* Present the Ethernet frame (no IFH, no FCS). HW re-inserts the+ * FCS on TX; see lan966x_fdma_pci_xmit_xdpf(). May be overridden+ * by XDP. The FCS strip is unconditional because NETIF_F_RXFCS+ * is not advertised in hw_features.+ */+ *data = virt_addr + IFH_LEN_BYTES;+ *data_len = blockl - IFH_LEN_BYTES - ETH_FCS_LEN;++ return lan966x_xdp_pci_run(port, virt_addr, *data_len, data, data_len); }
[Severity: Medium]
Should the two lan966x XDP receive hooks agree on whether the FCS is part
of the window handed to the BPF program?
On the PCIe path, lan966x_fdma_pci_rx_check_frame() subtracts the FCS
before calling into the program:
*data_len = blockl - IFH_LEN_BYTES - ETH_FCS_LEN;
return lan966x_xdp_pci_run(port, virt_addr, *data_len, data, data_len);
On the platform path, lan966x_fdma_rx_check_frame() passes the raw BLOCKL
value and lan966x_xdp_run() only removes the IFH:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_rx_check_frame() {
...
return lan966x_xdp_run(port, page, FDMA_DCB_STATUS_BLOCKL(db->status));
}
drivers/net/ethernet/microchip/lan966x/lan966x_xdp.c:lan966x_xdp_run() {
...
xdp_prepare_buff(&xdp, page_address(page),
IFH_LEN_BYTES + XDP_PACKET_HEADROOM,
data_len - IFH_LEN_BYTES, false);
}
That BLOCKL value still contains the FCS, as shown by the trim done later
on the same path:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_rx_get_frame() {
...
skb_put(skb, FDMA_DCB_STATUS_BLOCKL(db->status));
...
skb_pull(skb, IFH_LEN_BYTES);
if (likely(!(skb->dev->features & NETIF_F_RXFCS)))
skb_trim(skb, skb->len - ETH_FCS_LEN);
}
So xdp.data_end points four bytes past the end of the Ethernet payload on
the platform path, and exactly at the end of the payload on the new PCIe
path. Both windows stay inside the DMA buffer, so this looks like a
behavioural difference rather than an out-of-bounds access.
A program that parses backwards from data_end, compares data_end - data
against an L3 length field, or trims via bpf_xdp_adjust_tail() would get
different results depending on whether the switch is platform- or
PCIe-attached. The PCIe framing matches the usual XDP convention of no
FCS, which would make the platform hook the one that differs from
convention.
@@ -874,11 +874,13 @@ static int lan966x_probe_port(struct lan966x *lan966x, u32 p,port->phylink=phylink;-/* XDP is not supported on the PCIe FDMA path. */-if(lan966x->fdma&&!lan966x_is_pci(lan966x))-dev->xdp_features=NETDEV_XDP_ACT_BASIC|-NETDEV_XDP_ACT_REDIRECT|-NETDEV_XDP_ACT_NDO_XMIT;+if(lan966x->fdma){+dev->xdp_features=NETDEV_XDP_ACT_BASIC;++if(!lan966x_is_pci(lan966x))+dev->xdp_features|=NETDEV_XDP_ACT_REDIRECT|+NETDEV_XDP_ACT_NDO_XMIT;+}
[Severity: Medium]
Both paths now report the same NETDEV_XDP_ACT_BASIC bit from
lan966x_probe_port(), so is there anything in the uAPI that lets a
program distinguish the FCS-included window of lan966x_xdp_run() from
the FCS-stripped window of lan966x_xdp_pci_run()?
Would it make sense to make one of the two hooks match the other so that
the frame presentation is the same for identically-advertised netdevs?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260909-lan966x-pci-fdma-v6-0-6f48dab9d671%40microchip.com
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 1
- [Low] Claimed patch-ordering / bisectability bug: the "cpu" reg window
is enlarged from 0x100000 to 0x800000 only in this commit, while the…
--
misc: lan966x-pci: dts: extend cpu reg to cover PCIE DBI space
This grows the "cpu" register window of the switch node in the lan966x PCI
device tree overlay from 0x100000 to 0x800000 so that the PCIE DBI and iATU
registers at offset 0x400000+ are covered. Those registers are programmed
when the FDMA engine sets up the ATU outbound windows.
No Link: tags in the commit header.
[Severity: Low]
This isn't a bug, just a question about the ordering of the series.
The earlier commits "net: lan966x: add PCIe FDMA support", "net: lan966x: add
PCIe FDMA MTU change support" and "net: lan966x: add PCIe FDMA XDP support"
already derive regs[TARGET_PCIE_DBI] from the "cpu" base plus 0x400000, and
fdma_pci_atu_init() writes at DBI + 0x300000 + idx * 0x200:
drivers/net/ethernet/microchip/fdma/fdma_pci.c
static void __iomem *fdma_pci_atu_addr_get(void __iomem *addr, int offset, int idx)
{
return addr + FDMA_PCI_ATU_ADDR + FDMA_PCI_ATU_IDX_SIZE * idx + offset;
}
That lands roughly 0x700000 from the "cpu" base, i.e. outside the 0x100000
mapping that exists until this commit. Would it be clearer to move this
overlay change before the driver commits that compute the DBI offset, so
that the mapping is never smaller than the offsets the driver derives from
it?
For what it is worth, the intermediate commits do look safe in practice,
since the ATU access in lan966x_fdma_pci_init() is gated:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma_pci.c
if (!lan966x->fdma)
return 0;
...
fdma_pci_atu_init(&lan966x->atu, lan966x->regs[TARGET_PCIE_DBI]);
and lan966x->fdma is only set when the node names an "fdma" interrupt in
lan966x_probe():
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
...
lan966x->fdma = true;
}
The overlay here still carries only interrupt-names = "xtr", "ana", and the
"fdma" interrupt is added by the following commit, so the enlarged window is
in place before the FDMA path can be enabled. Is that ordering intentional?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260909-lan966x-pci-fdma-v6-0-6f48dab9d671%40microchip.com
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 1 · Low: 0
- [Medium] The FDMA interrupt is hooked up (and thereby unmasked in the
OIC) long before any FDMA state exists.
--
misc: lan966x-pci: dts: add fdma interrupt to overlay
This adds OIC interrupt 14 as the "fdma" named interrupt to the lan966x
PCI device tree overlay, so FDMA-based injection/extraction can be used
when the switch is reached over PCIe.
[Severity: Medium]
With source 14 now named "fdma", platform_get_irq_byname(pdev, "fdma")
starts succeeding on the PCIe path, and lan966x_probe() requests the
handler right there:
drivers/net/ethernet/microchip/lan966x/lan966x_main.c:lan966x_probe() {
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
err = devm_request_irq(&pdev->dev, lan966x->fdma_irq,
lan966x_fdma_irq_handler, 0,
"fdma irq", lan966x);
...
lan966x->fdma = true;
}
}
Is this ordering safe? The OIC maps and unmasks source 14 at
devm_request_irq() time, but at that point no FDMA state exists yet:
- netif_napi_add()/napi_enable() only run later, from
lan966x_port_init() -> lan966x_fdma_netdev_init() in the per-port
probe loop, so lan966x->napi is still the zeroed devm_kzalloc()
memory and napi.poll is NULL.
- the DCB rings, the LLP/ATU windows and the FDMA_CTRL NRESET pulse are
all done even later, from lan966x->ops->fdma_init() ->
lan966x_fdma_pci_init().
If the OIC delivers an FDMA interrupt in that window, the handler
schedules NAPI unconditionally:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_irq_handler() {
db = lan_rd(lan966x, FDMA_INTR_DB);
err = lan_rd(lan966x, FDMA_INTR_ERR);
if (db) {
lan_wr(0, lan966x, FDMA_INTR_DB_ENA);
lan_wr(db, lan966x, FDMA_INTR_DB);
napi_schedule(&lan966x->napi);
}
}
Can this then oops in __napi_poll(), either calling n->poll() while it
is still NULL, or running lan966x_fdma_pci_napi_poll() against
rx->fdma.dcbs == NULL and n_dcbs == 0, where fdma_has_frames() ->
fdma_db_get() dereferences the dcbs array?
How reachable is that window in practice? The series itself describes
the card arriving with FDMA state from a previous boot. The commit
"net: lan966x: clear FDMA interrupt stickies after switch reset" says
the stickies "sit in FDMA_INTR_ERR until the FDMA IRQ is requested later
in probe, at which point the handler fires immediately and WARNs", and
"net: lan966x: add shutdown callback to stop FDMA on reboot" says that
on the endpoint "channels stay active and interrupt sources stay armed
across the reset" and that "FDMA_INTR_ENA persists on the card across a
warm reboot".
The sticky clear in lan966x_reset_switch() clears the latched bits at
one instant, but does not mask FDMA_INTR_ENA/FDMA_INTR_DB_ENA and does
not stop the channels. After a reboot that skips device_shutdown()
(panic reboot, kexec/kdump, watchdog warm reset with the card still
powered), could a still-active channel latch FDMA_INTR_DB again after
the clear and have it delivered as soon as source 14 is unmasked?
Would it be more robust to reset and mask the FDMA block (the
FDMA_CTRL NRESET pulse that already exists in lan966x_fdma_pci_init(),
plus clearing FDMA_INTR_ENA/FDMA_INTR_DB_ENA) before
devm_request_irq(), or to request the FDMA IRQ only after fdma_init()
has built the rings and added the NAPI instance?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260909-lan966x-pci-fdma-v6-0-6f48dab9d671%40microchip.com
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-14 13:57:15
Thank you for your contribution! Sashiko AI review found 6 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 3 · Low: 3
- [Medium] fdma_pci_atu_region_map() (EXPORT_SYMBOL_GPL) is the primitive
that programs the ATU limit register (region->limit_addr =…
- [Medium] fdma_pci_atu_region_enable() writes the region enable bit
(BIT(31) at offset 0x4) and returns immediately, and…
- [Medium] fdma_pci_atu_configure_translation() programs lower/upper base
(0x8/0xc), limit (0x10), lower/upper target (0x14/0x18) and the enable…
- [Low] The commit message says the patch "effectively add[s] support for
PCIe FDMA", but at this commit nothing in the tree calls…
- [Low] fdma_pci_atu_init() is the designated initializer of struct
fdma_pci_atu (it even programs hardware, disabling all six outbound…
- [Low] fdma_pci_atu_translate_addr() reads region->base_addr and
region->target_addr with no lock, no READ_ONCE() and no in_use…
--
net: microchip: fdma: add PCIe ATU support
This adds a new fdma_pci.c/fdma_pci.h library that carves the PCIe endpoint
outbound address space (0x10000000-0x1fffffff) into six equally sized ATU
regions, and adds fdma_alloc_coherent_and_map()/fdma_free_coherent_and_unmap()
helpers that pair a coherent DMA allocation with an ATU region mapping.
A few questions below.
quoted
Add the required functions and helpers that combine the DMA allocation
with the ATU region mapping, effectively adding support for PCIe FDMA.
[Severity: Low]
Is "effectively adding support for PCIe FDMA" the right wording for this
patch on its own?
At this commit nothing in the tree calls fdma_pci_atu_init(),
fdma_pci_atu_region_map(), fdma_pci_atu_translate_addr(),
fdma_alloc_coherent_and_map() or fdma_free_coherent_and_unmap(), and
FDMA_PCI_DB_ALIGN/FDMA_PCI_DB_SIZE() have no user either.
The wiring appears later in the series, in "net: lan966x: add PCIe FDMA
support" and its follow-ups (lan966x_fdma_pci.c calls fdma_pci_atu_init(),
fdma_alloc_coherent_and_map(), fdma_free_coherent_and_unmap(),
fdma_pci_atu_translate_addr() and FDMA_PCI_DB_SIZE()).
Would "add the infrastructure for PCIe FDMA" describe this patch more
accurately?
Wording. The subject is "add PCIe ATU support" and patch 10/14 is "net: lan966x:
add PCIe FDMA support"; side by side, I think its clear. Can reword if a respin
is required.
@@ -127,6 +127,50 @@ void fdma_free_phys(struct fdma *fdma)}EXPORT_SYMBOL_GPL(fdma_free_phys);+#if IS_ENABLED(CONFIG_MCHP_LAN966X_PCI)+/* Allocate coherent DMA memory and map it in the ATU. */+intfdma_alloc_coherent_and_map(structdevice*dev,structfdma*fdma,+structfdma_pci_atu*atu)+{+structfdma_pci_atu_region*region;+interr;++if(WARN_ON(fdma->atu_region))+return-EBUSY;++/* The ATU cannot express a limit finer than the region granularity, so+*thehardwarewidenstheprogrammedlimittothatboundary.Padthe+*allocationtomatch,ortheoutboundwindowwouldextendpastthe+*memoryweown.+*/+fdma->size=ALIGN(fdma->size,FDMA_PCI_ATU_REGION_ALIGN);++err=fdma_alloc_coherent(dev,fdma);+if(err)+returnerr;++region=fdma_pci_atu_region_map(atu,fdma->dma,fdma->size);+if(IS_ERR(region)){+fdma_free_coherent(dev,fdma);+returnPTR_ERR(region);+}
[Severity: Medium]
Does the enable bit take effect by the time this writel() retires?
The register block looks like the DesignWare unrolled iATU: the 0x300000
base matches DEFAULT_DBI_ATU_OFFSET, the 0x200 stride matches
PCIE_ATU_UNROLL_BASE(dir, index) = index << 9, and 0x0/0x4/0x8/0xc/0x10/
0x14/0x18 match PCIE_ATU_UNR_REGION_CTRL1/CTRL2/LOWER_BASE/UPPER_BASE/
LOWER_LIMIT/LOWER_TARGET/UPPER_TARGET with PCIE_ATU_ENABLE = BIT(31) in
CTRL2.
For that IP, dw_pcie_prog_outbound_atu() in
drivers/pci/controller/dwc/pcie-designware.c polls until the enable bit
reads back:
for (retries = 0; retries < LINK_WAIT_MAX_IATU_RETRIES; retries++) {
val = dw_pcie_readl_atu(pci, dir, index, PCIE_ATU_REGION_CTRL2);
if (val & PCIE_ATU_ENABLE)
return 0;
mdelay(LINK_WAIT_IATU);
}
dev_err(pci->dev, "Outbound iATU is not being enabled\n");
return -ETIMEDOUT;
with the comment "Make sure ATU enable takes effect before any subsequent
config and I/O accesses".
Here fdma_pci_atu_region_map() returns the region as usable right after
this write, and the caller programs FDMA descriptors with
region->base_addr. Should the enable be read back (and a failure to arm
reported to the caller) instead of being fire-and-forget?
dw_pcie_prog_outbound_atu() polls because it programs the *local* iATU and then
immediately transacts through the window it armed. We program the *endpoint's*
iATU and never transact through it: the only consumer is the FDMA engine, which
cannot start before FDMA_CH_ACTIVATE, with four readl()s of the same BAR in
between.
quoted
+/* Configure the address translation in the ATU. */+static void+fdma_pci_atu_configure_translation(struct fdma_pci_atu_region *region)+{+ struct fdma_pci_atu *atu = region->atu;+ int idx = region->idx;++ writel(lower_32_bits(region->base_addr),+ fdma_pci_atu_addr_get(atu->addr,+ FDMA_PCI_ATU_LWR_BASE_ADDR, idx));++ writel(upper_32_bits(region->base_addr),+ fdma_pci_atu_addr_get(atu->addr,+ FDMA_PCI_ATU_UPP_BASE_ADDR, idx));++ /* Upper limit register only needed with REGION_SIZE > 4GB. */+ writel(region->limit_addr,+ fdma_pci_atu_addr_get(atu->addr, FDMA_PCI_ATU_LIMIT_ADDR, idx));
[Severity: Medium]
Should region control 1 (offset 0x0) be programmed here too?
Everything except CTRL1 is written: base, limit, target and the enable bit
in CTRL2. On the DWC unrolled iATU, CTRL1 holds the outbound TLP TYPE (MEM
= 0), TD, PCIE_ATU_FUNC_NUM(pf) and PCIE_ATU_INCREASE_REGION_SIZE
(BIT(13)). dw_pcie_prog_outbound_atu() always writes CTRL1 before enabling
CTRL2:
val = type | PCIE_ATU_FUNC_NUM(func_no);
...
dw_pcie_writel_atu(pci, dir, index, PCIE_ATU_REGION_CTRL1, val);
dw_pcie_writel_atu(pci, dir, index, PCIE_ATU_REGION_CTRL2,
PCIE_ATU_ENABLE);
There is no define for offset 0x0 in this file, and fdma_pci_atu_init()
only clears the enable bit, so each window is armed with whatever CTRL1
value reset, the endpoint bootloader, or a previous OS instance left in
place. With a residual TYPE the FDMA writes go out as the wrong TLP type,
with a stale FUNC_NUM they carry the wrong requester, and with a stale
INCREASE_REGION_SIZE the window end is taken from a register this code
never writes.
The datasheet does not describe programming region CTRL1 as part of setting up an
outbound window, and no side effect from leaving it alone has ever been
observed.
Related, is the comment "Upper limit register only needed with REGION_SIZE
quoted
4GB" accurate here? That mode is selected by CTRL1 BIT(13), which is
neither set nor cleared, and the upper limit register is never written.
The comment (tries) to clarify why the upper half is not written, which is when
the region size is > 4GB. I'd say its fine. And yes, in the case of >4GB,
BIT(13) needs to be set.
[ ... ]
quoted
+/* Find an unused ATU region. */+static struct fdma_pci_atu_region *+fdma_pci_atu_region_get_free(struct fdma_pci_atu *atu)+{+ struct fdma_pci_atu_region *regions = atu->regions;++ for (int i = 0; i < FDMA_PCI_ATU_REGION_MAX; i++) {+ if (regions[i].in_use)+ continue;++ return ®ions[i];+ }++ return ERR_PTR(-ENOSPC);+}
[ ... ]
quoted
+/* Map a host DMA address into a free outbound region. */+struct fdma_pci_atu_region *+fdma_pci_atu_region_map(struct fdma_pci_atu *atu, u64 target_addr, int size)+{+ struct fdma_pci_atu_region *region;++ if (!atu)+ return ERR_PTR(-EINVAL);++ if (size <= 0)+ return ERR_PTR(-EINVAL);++ if (size > fdma_pci_atu_region_size())+ return ERR_PTR(-ERANGE);++ /* The ATU region base is only ever aligned to FDMA_PCI_ATU_REGION_ALIGN;+ * require the same alignment of the host target address, since the ATU+ * translates addr - target_addr + base_addr and any misalignment here+ * would shift every translated address by the same amount.+ */+ if (!IS_ALIGNED(target_addr, FDMA_PCI_ATU_REGION_ALIGN))+ return ERR_PTR(-EINVAL);
[Severity: Medium]
Should this validation block also reject a size that is not a multiple of
FDMA_PCI_ATU_REGION_ALIGN?
Hardening: it cannot be reached: the only in-tree caller is
fdma_alloc_coherent_and_map(), which does fdma->size = ALIGN(fdma->size,
FDMA_PCI_ATU_REGION_ALIGN.
fdma_pci.h states the rule for both halves:
/* Outbound regions are 64KB granular (datasheet section 3.24.7.4.1), so both
* the region base and the mapped size must be aligned to this.
*/
#define FDMA_PCI_ATU_REGION_ALIGN BIT(16)
and the commit message names the consequence of not honouring it: "the
outbound window would extend past the memory the host allocated for DMA".
The size given here becomes the programmed limit:
region->limit_addr = region->base_addr + size - 1;
so with, say, a PAGE_SIZE-aligned 0x21000 from fdma_get_size() the hardware
widens the limit to the next 64KB boundary and the enabled outbound window
covers up to 64KB-1 of host memory beyond the buffer, which the FDMA engine
can then write into.
The 64KB padding is only done in the convenience wrapper
fdma_alloc_coherent_and_map(), not in this exported primitive that
programs the register, and the commit message advertises the primitive for
reuse by lan969x. Would an IS_ALIGNED(size, FDMA_PCI_ATU_REGION_ALIGN)
check (or rounding here) be better placed alongside the target_addr check?
Also, the header comment for fdma_pci_atu_region_map() is just "Map a host
DMA address into a free ATU region", with no mention that a target_addr
that is not 64KB aligned is rejected with -EINVAL. Could that requirement
be documented at the prototype?
Yes, I will document that if a respin is required.
quoted
++ mutex_lock(&atu->lock);++ region = fdma_pci_atu_region_get_free(atu);+ if (IS_ERR(region)) {+ mutex_unlock(&atu->lock);+ return region;+ }++ region->target_addr = target_addr;+ region->limit_addr = region->base_addr + size - 1;+ region->in_use = true;++ fdma_pci_atu_configure_translation(region);+ fdma_pci_atu_region_enable(region);++ mutex_unlock(&atu->lock);++ return region;+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_region_map);++/* Translate a host DMA address to the corresponding OB address. */+u64 fdma_pci_atu_translate_addr(struct fdma_pci_atu_region *region, u64 addr)+{+ return region->base_addr + (addr - region->target_addr);+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_translate_addr);
[Severity: Low]
What serializes these reads against the writers?
fdma_pci_atu_region_map() and fdma_pci_atu_region_unmap() mutate
target_addr/in_use and the hardware window under the sleeping atu->lock,
while this helper reads base_addr/target_addr with no lock, no READ_ONCE()
and no in_use check. Its intended callers are descriptor callbacks
(lan966x_fdma_pci_dataptr_cb()/nextptr_cb(), reached from NAPI poll and
from lan966x_fdma_pci_xmit()), which cannot take a mutex.
Looking at the end of the series, the writers are
lan966x_fdma_pci_reload() and lan966x_fdma_pci_deinit(), both of which
first do napi_disable() and lan966x_fdma_tx_disable_netdev() (netif_tx_disable()
drains in-flight ndo_start_xmit callers), and both are under rtnl, so there
does not seem to be a reachable race today.
Given that, would it help to document at the prototype that the caller must
quiesce DMA and the descriptor paths before unmapping a region, since this
is an exported helper?
Yes, I will document that if a respin is required.
quoted
++/* Initialize ATU, dividing the OB space into equally sized regions. */+void fdma_pci_atu_init(struct fdma_pci_atu *atu, void __iomem *addr)+{+ struct fdma_pci_atu_region *regions = atu->regions;+ u32 region_size = fdma_pci_atu_region_size();++ atu->addr = addr;+ mutex_init(&atu->lock);++ for (int i = 0; i < FDMA_PCI_ATU_REGION_MAX; i++) {+ regions[i].base_addr =+ FDMA_PCI_ATU_OB_START + (i * region_size);+ regions[i].idx = i;+ regions[i].atu = atu;++ fdma_pci_atu_region_disable(®ions[i]);+ }+}+EXPORT_SYMBOL_GPL(fdma_pci_atu_init);
[Severity: Low]
Should this also initialize in_use, target_addr and limit_addr?
This is the designated initializer for struct fdma_pci_atu and it even
touches hardware (disabling all six windows), but it only sets base_addr,
idx and atu. The allocation state that fdma_pci_atu_region_get_free()
consumes is in_use, which is never assigned here, so the helper silently
depends on the embedding structure having been zeroed. If in_use held
garbage, every region would look busy and mapping would fail with -ENOSPC.
The only in-tree caller later in the series passes &lan966x->atu, and
lan966x comes from devm_kzalloc() in lan966x_probe(), so it works today.
Would it be worth clearing the per-region software state here (or noting
the "caller must zero the struct" precondition at the prototype)?
I think this is a false positive: The struct is zeroed by design rather
than by luck: struct fdma_pci_atu is embedded in struct lan966x, which is
devm_kzalloc()'d, so in_use is already false when fdma_pci_atu_init() runs. It
is also the only one of the three that is read before it is written - it has a
single reader, fdma_pci_atu_region_get_free(). target_addr and limit_addr are
assigned by fdma_pci_atu_region_map() under the lock, and a region pointer only
ever reaches a caller as that function's return value, so neither can be read
first.
There is also no counterpart to this function: nothing resets the software
state, unmaps still-mapped regions, or calls mutex_destroy(). Is a
fdma_pci_atu_deinit() wanted for symmetry?
Maybe. This version deliberately leaves the ATU reigions alone, and rely on the
fdma_pci_atu_init, to reinitialize the regions on probe. No issues have been
observed so far.
+/* Outbound regions are 64KB granular (datasheet section 3.24.7.4.1), so both+ * the region base and the mapped size must be aligned to this.+ */+#define FDMA_PCI_ATU_REGION_ALIGN BIT(16)
[ ... ]
quoted
+/* Initialize ATU, dividing OB space into regions. */+void fdma_pci_atu_init(struct fdma_pci_atu *atu, void __iomem *addr);++/* Unmap an ATU region, clearing its translation and disabling it. */+void fdma_pci_atu_region_unmap(struct fdma_pci_atu_region *region);++/* Map a host DMA address into a free ATU region. */+struct fdma_pci_atu_region *fdma_pci_atu_region_map(struct fdma_pci_atu *atu,+ u64 target_addr,+ int size);
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-14 14:09:47
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 1
- [Low] Commit-message/comment vs. reachability at this revision: the
added FDMA sticky clear in lan966x_reset_switch()…
--
net: lan966x: clear FDMA interrupt stickies after switch reset
The patch acks FDMA_ERRORS, FDMA_INTR_ERR and FDMA_INTR_DB right after
reset_control_reset() in lan966x_reset_switch(), so that spurious sticky
bits latched by the GCB soft reset in PCI mode do not make the FDMA IRQ
handler fire as soon as the IRQ is requested. The clear is unconditional and
is described as a no-op on the platform path.
@@ -1067,6 +1067,15 @@ static int lan966x_reset_switch(struct lan966x *lan966x)reset_control_reset(switch_reset);+/* When in PCI mode, the GCB soft reset issued by the reset+*controllercanlatchspuriousbitsintheFDMAerrorstickies.+*Clearthembeforerequest_irqhooksuptheFDMAIRQline,+*otherwisethehandlerfiresimmediatelyonprobe.+*/+lan_wr(lan_rd(lan966x,FDMA_ERRORS),lan966x,FDMA_ERRORS);+lan_wr(lan_rd(lan966x,FDMA_INTR_ERR),lan966x,FDMA_INTR_ERR);+lan_wr(lan_rd(lan966x,FDMA_INTR_DB),lan966x,FDMA_INTR_DB);
[Severity: Low]
This isn't a bug in the register writes themselves, but is the failure
described in the changelog reachable at this point in the series?
No, but this is a preparation patch that makes sure that once the FDMA support
lands, in a later patch, the failure is not introduced.
The commit message says the latched bits "sit in FDMA_INTR_ERR until the FDMA
IRQ is requested later in probe, at which point the handler fires immediately
and WARNs". In lan966x_probe() the FDMA handler is only installed when a named
"fdma" interrupt exists:
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
err = devm_request_irq(&pdev->dev, lan966x->fdma_irq,
lan966x_fdma_irq_handler, 0,
"fdma irq", lan966x);
...
lan966x->fdma = true;
}
At this commit the PCI overlay in drivers/misc/lan966x_pci.dtso does not
describe that interrupt:
interrupts = <12 IRQ_TYPE_LEVEL_HIGH>,
<9 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "xtr", "ana";
so platform_get_irq_byname(pdev, "fdma") fails and lan966x_fdma_irq_handler()
is never requested on the PCI path.
The overlay also disagrees with the positional list in
Documentation/devicetree/bindings/net/microchip,lan966x-switch.yaml, where the
second entry is reserved for "fdma":
interrupt-names:
minItems: 1
items:
- const: xtr
- const: fdma
- const: ana
Both points are resolved later in the same series by "misc: lan966x-pci: dts:
add fdma interrupt to overlay", which switches the overlay to
interrupts <12>,<14>,<9> and interrupt-names = "xtr", "fdma", "ana", so this is
only an observation about the ordering of the changelog against the code state
here rather than an actionable defect.
Would it help to mention in the commit message that the WARN only becomes
reachable once the overlay gains the "fdma" interrupt?
The changelog already scopes itself with "When in PCI mode".
quoted
/* Don't reinitialize the switch core, if it is already initialized. In
* case it is initialized twice, some pointers inside the queue system
* in HW will get corrupted and then after a while the queue system gets
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-14 15:06:43
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 4 · Low: 1
- [Medium] lan966x_shutdown() leaves the shared NAPI permanently disabled
(napi_disable() with no napi_enable() and no state marker) while the…
- [Medium] lan966x_shutdown() quiesces TX only with napi_disable() +
netif_tx_disable() and then manipulates FDMA TX state…
- [Medium] lan966x_shutdown() masks the analyzer source with a plain
lan_wr(0, ANA_ANAINTR) while the ANA interrupt is still requested and…
- [Medium] The new shutdown path relies on
lan966x_fdma_rx_disable()/lan966x_fdma_tx_disable() to prove the…
- [Low] Claim: the `if (!lan966x->fdma) return;` guard in
lan966x_shutdown() skips the ANA_ANAINTR mask, which lan966x_init()…
--
net: lan966x: add shutdown callback to stop FDMA on reboot
Adds a platform shutdown() callback that disables NAPI, stops the netdev
TX queues, disables the RX and TX FDMA channels and masks FDMA_INTR_ENA,
FDMA_INTR_DB_ENA and ANA_ANAINTR so a card that survives a host reboot
does not assert the shared PCIe INTx before re-probe. FDMA_INTR_ENA is
also re-armed in lan966x_fdma_rx_start().
A few questions about the new shutdown path below.
Seems like the shutdown() introduced in this patch is racy because netdevs are
left registered and FDMA/ATU is kept alive. Let me rework this in v7 to ensure
the netdevs are unregistered/detached, to avoid races with MTU/XDP etc and tear
down FDMA/ATU.
@@ -325,6 +329,21 @@ static void lan966x_fdma_stop_netdev(struct lan966x *lan966x) } }+/* Drain in-flight xmit callers and stop all TX queues on every port. */+void lan966x_fdma_tx_disable_netdev(struct lan966x *lan966x)+{+ struct lan966x_port *port;+ int i;++ for (i = 0; i < lan966x->num_phys_ports; ++i) {+ port = lan966x->ports[i];+ if (!port)+ continue;++ netif_tx_disable(port->dev);+ }+}
[Severity: Medium]
Does netif_tx_disable() really drain every in-flight xmit caller here?
lan966x also registers .ndo_xdp_xmit = lan966x_xdp_xmit in
lan966x_port_netdev_ops, and that entry point is dispatched from a foreign
device's NAPI/softirq (bq_xmit_all()) without the netdev txq locks and
without regard for the txq stopped state or lan966x's NAPI state:
lan966x_xdp_xmit()
lan966x_fdma_xmit_xdpf()
spin_lock(&lan966x->tx_lock);
...
/* Start the transmission */
lan966x_fdma_tx_start(tx);
lan966x_fdma_tx_start() calls lan966x_fdma_tx_activate() when
tx->activated is false, and that re-sets BIT(channel_id) in
FDMA_INTR_DB_ENA and writes FDMA_CH_ACTIVATE. lan966x_fdma_tx_disable()
in the new shutdown path sets tx->activated = false, and lan966x_shutdown()
touches FDMA_CH_DISABLE, tx->activated and FDMA_INTR_DB_ENA without taking
lan966x->tx_lock.
If an XDP program on another interface redirects into a lan966x port in the
reboot window, can the TX channel end up re-activated (and the DB enable
bit re-set) after shutdown() returns? The shutdown path deliberately keeps
the DCB rings mapped, so a re-activated channel would keep fetching DCBs
from and writing DCB status into host memory that the next kernel reuses,
which is the state the commit message says it eliminates.
@@ -1324,9 +1324,35 @@ static void lan966x_remove(struct platform_device *pdev)debugfs_remove_recursive(lan966x->debugfs_root);}+staticvoidlan966x_shutdown(structplatform_device*pdev)+{+structlan966x*lan966x=platform_get_drvdata(pdev);++if(!lan966x->fdma)+return;++/* The reload paths disable this NAPI under rtnl; serialize with them. */+rtnl_lock();++if(lan966x->fdma_ndev)+napi_disable(&lan966x->napi);
[Severity: Medium]
The NAPI is disabled here and never re-enabled, and no state is recorded to
say the device has been shut down. After rtnl_unlock() the netdevs are
still registered and their ndo callbacks are still live.
Can a later MTU change on any port hang?
lan966x_port_change_mtu() /* .ndo_change_mtu */
lan966x->ops->fdma_resize()
lan966x_fdma_change_mtu()
__lan966x_fdma_reload()
lan966x_fdma_reload()
napi_synchronize(&lan966x->napi);
napi_disable(&lan966x->napi);
The second napi_disable() lands in napi_disable_locked(), which spins:
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
NAPIF_STATE_SCHED was set by the napi_disable() in lan966x_shutdown() and
is only cleared by napi_enable(), which shutdown() never calls.
Wouldn't the netlink task then loop with no timeout and no alternative wake
condition, while holding rtnl_mutex, blocking any later rtnl user in the
reboot sequence (including a second instance's lan966x_shutdown(), which
also takes rtnl_lock())? device_shutdown() runs from
kernel_restart_prepare() with userspace still runnable, and an XDP attach
reaches the same napi_disable() through lan966x_xdp_setup() ->
lan966x_fdma_reload_page_pool() -> __lan966x_fdma_reload().
[Severity: Medium]
The commit message says "fdma_deinit() disables both channels and waits for
them to go idle", and shutdown() relies on the same two helpers for that
guarantee. Both of them drop the poll result though:
lan966x_fdma_rx_disable() and lan966x_fdma_tx_disable() {
readx_poll_timeout_atomic(lan966x_fdma_channel_active, lan966x,
val, !(val & BIT(fdma->channel_id)),
READL_SLEEP_US, READL_TIMEOUT_US);
}
Is a timeout on FDMA_CH_ACTIVE distinguishable from a channel that really
went idle here? lan966x_fdma_tx_disable() sets tx->activated = false
regardless, and shutdown() then masks FDMA_INTR_ENA/FDMA_INTR_DB_ENA, which
hides the error sources that would report the still-active channel, and
returns.
Unlike the unbind path, shutdown() keeps the RX pages, the coherent DCB
rings and (on the PCIe backend) the outbound ATU window mapped, so a
channel that never reached the idle state keeps write access to memory the
next kernel will reuse. Would it make sense to check the poll return and
at least report it?
[Severity: Medium]
Can the ANA_ANAINTR mask be undone right after it is written? The ANA irq
is still requested and enabled at this point, and it is a threaded handler
(devm_request_threaded_irq() with lan966x_ana_irq_handler and IRQF_ONESHOT
in lan966x_probe()), whose tail does an unlocked read-modify-write of the
same register:
lan966x_mac_irq_handler() {
...
lan_rmw(ANA_ANAINTR_INTR_SET(0),
ANA_ANAINTR_INTR,
lan966x, ANA_ANAINTR);
}
lan_rmw() is readl() -> modify -> writel() and preserves ANA_ANAINTR_INTR_ENA
from the value it read. If the handler thread has already done its readl()
(with INTR_ENA still 1) when shutdown() writes 0, wouldn't its write-back
restore ANA_ANAINTR_INTR_ENA = 1?
shutdown() holds only rtnl, which the threaded handler never takes, and it
neither disable_irq()s nor frees the ANA irq, so the source stays mapped
and unmasked. The switch keeps learning and forwarding after the host
reboots, so MAC-table sync events keep arriving. Would a
disable_irq()/synchronize_irq() on lan966x->ana_irq before the mask, or a
shared lock with the handler, close that window?
Seems legit. Sashiko complained that the ANAINTR could cause the same splats
that this patch tries to fix. Therefore ANAINTR was masked in v6. Seems correct
to also disable it, to make sure that the interrupt is not unmasked again.
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-15 08:54:51
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 0 (1) · Medium: 1 (1) · Low: 3
- [Medium] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
- [Low] DISCARDED — fixed later in the series.
Pre-existing issues:
- [High] Use-after-free in the new PCIe receive path:
`lan966x_fdma_pci_rx_get_frame()`…
- [Medium] DISCARDED — pre-existing shared-library ordering weakness.
--
net: lan966x: add PCIe FDMA support
Adds a second FDMA backend for lan966x used when the switch is reached over
PCIe, using contiguous ATU-mapped DMA buffers and memcpy-based frame
transfer instead of per-page mappings. XDP and PTP are not advertised or
allowed on this path.
[Severity: High]
This isn't a bug introduced by this patch - lan966x_fdma_rx_get_frame() in
lan966x_fdma.c has the same caller pattern - but can lan966x_fdma_pci_rx_get_frame()
touch skb after lan966x_hw_offload() has already freed it?
Yes, but pre-existing. I think this deserves a proper fix through net and
updates for all callers.
lan966x_hw_offload() replaces the buffer and reports only a boolean:
drivers/net/ethernet/microchip/lan966x/lan966x_main.c:lan966x_hw_offload() {
...
if (eth_type_vlan(skb->protocol)) {
skb = skb_vlan_untag(skb);
if (unlikely(!skb))
return false;
}
...
}
and skb_vlan_untag() frees the caller's skb on failure:
net/core/skbuff.c:skb_vlan_untag() {
...
if (unlikely(!pskb_may_pull(skb, VLAN_HLEN + sizeof(unsigned short))))
goto err_free;
...
err_free:
kfree_skb(skb);
return NULL;
}
So on the false return the caller's pointer is dangling, and
lan966x_fdma_pci_rx_get_frame() then does:
skb->offload_fwd_mark = 0;
skb->dev->stats.rx_bytes += skb->len;
skb->dev->stats.rx_packets++;
return skb;
with the returned skb going to napi_gro_receive() in
lan966x_fdma_pci_napi_poll().
Is the new lower bound in lan966x_fdma_pci_rx_size_fits() wide enough to let
this happen? IFH_LEN_BYTES + ETH_HLEN + ETH_FCS_LEN is 46, so a BLOCKL of
46..51 is accepted, and after skb_pull(IFH_LEN_BYTES),
skb_trim(skb->len - ETH_FCS_LEN) and eth_type_trans() there are only 0..5
bytes left. For a frame with h_proto 0x8100 on a bridged port that has IGMP
or MLD redirection enabled, the pskb_may_pull(VLAN_HLEN + 2) inside
skb_vlan_untag() then fails and the buffer is freed.
Would it make sense to have lan966x_hw_offload() take a struct sk_buff **,
or to have the caller stop using skb once it returns false?
quoted
static int lan966x_fdma_pci_xmit(struct sk_buff *skb, __be32 *ifh,
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-15 10:23:33
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 2 · Medium: 1 · Low: 1
- [High] NULL pointer dereference in the new lan966x_fdma_pci_reset_mem()
restore path.
- [High] Unbalanced/unguarded napi_disable()/napi_enable() in the new MTU
reload path.
- [Medium] Netdev MTU contract mismatch on the PCIe FDMA path.
- [Low] Inverted restart ordering in lan966x_fdma_pci_reload(): both the
success path (lines 458-460) and the restore path (lines 476-478) call…
--
net: lan966x: add PCIe FDMA MTU change support
This adds MTU change support to the PCIe FDMA path by reallocating the
contiguous ATU-mapped RX and TX buffers, falling back to resetting and
reusing the existing buffers if the reallocation fails. It also caps the
PCIe DCB ring at 256 entries so the contiguous allocation stays below
MAX_PAGE_ORDER at jumbo MTU.
A few questions about the new reload and resize paths below.
@@ -1,8 +1,15 @@// SPDX-License-Identifier: GPL-2.0++#include<linux/mmzone.h>+#include"fdma_api.h"#include"lan966x_main.h"+/* Ring must fit in one MAX_PAGE_ORDER DMA block; 512 DCBs overflows+*atjumboMTU.+*/+#define FDMA_PCI_DCB_MAX 256+
[Severity: High]
Can tx->fdma.atu_region be NULL here?
lan966x_fdma_pci_reset_mem() is only reached from the restore label of
lan966x_fdma_pci_reload(), and that label is taken when the ring
allocation failed. Probe registers the netdevs from
lan966x_probe_port() -> register_netdev() inside the ports loop, while
lan966x->fdma is already true, and ops->fdma_init() (which sets
n_dcbs/db_size/size and maps the ATU regions) only runs after that loop.
An MTU change in that window passes the !lan966x->fdma guard in
lan966x_port_change_mtu() and reaches lan966x_fdma_pci_resize() with a
zeroed fdma:
rx.max_mtu == 0, so the "max_mtu == lan966x->rx.max_mtu" early return
does not fire
n_dcbs == 0, so fdma_get_size_contiguous() returns ALIGN(0, PAGE_SIZE)
== 0 and both -ERANGE guards pass
lan966x_fdma_pci_reload() then recomputes size, which stays 0, and:
lan966x_fdma_pci_rx_alloc()
fdma_alloc_coherent_and_map()
fdma_pci_atu_region_map()
if (size <= 0)
return ERR_PTR(-EINVAL);
so control reaches restore, memcpy's the still-zeroed fdma structs back
(atu_region == NULL, dcbs == NULL, size == 0), and calls reset_mem().
memset(NULL, 0, 0) and fdma_dcbs_init() with n_dcbs == 0 are both
no-ops, so nothing stops execution before tx->fdma.atu_region->base_addr
is evaluated. Would a NULL check on atu_region (or an early bail in
resize() when the FDMA is not initialized yet) be appropriate here?
Real and the proper fix is to reorder initialization like we did on sparx5 in
b8909aad5b8d ("net: sparx5: move netdev and notifier block registration to
probe"), where the FDMA irq is requested and the FDMA initialized in
sparx5_frame_io_init() and netdev registration happens last.
For now, I can add a small check that makes sure that MTU change bails out in
case FDMA is not initialized yet.
quoted
+ lan966x_fdma_llp_configure(lan966x,+ rx->fdma.atu_region->base_addr,+ rx->fdma.channel_id);+}++/* Wake all TX queues on every port (undoes lan966x_fdma_tx_disable_netdev). */+static void lan966x_fdma_pci_wakeup_netdev(struct lan966x *lan966x)+{+ for (int i = 0; i < lan966x->num_phys_ports; ++i) {+ struct lan966x_port *port = lan966x->ports[i];++ if (port)+ netif_tx_wake_all_queues(port->dev);+ }+}++static int lan966x_fdma_pci_reload(struct lan966x *lan966x, int new_mtu)+{+ struct fdma tx_fdma_old = lan966x->tx.fdma;+ struct fdma rx_fdma_old = lan966x->rx.fdma;+ u32 old_mtu = lan966x->rx.max_mtu;+ int err;++ napi_disable(&lan966x->napi);
[Severity: High]
Should this napi_disable() be guarded the way the other users of
lan966x->napi in this driver are?
lan966x_fdma_pci_deinit() does:
if (lan966x->fdma_ndev)
napi_disable(&lan966x->napi);
and lan966x_shutdown() has the same guard, with a comment noting that the
reload paths disable this NAPI under rtnl. Two states look problematic
for the unguarded call:
The NAPI may not have been added yet. netif_napi_add() only runs from
lan966x_fdma_netdev_init(), called by lan966x_port_init(), which happens
after lan966x_probe_port() already did register_netdev(). An MTU change
in that window reaches napi_disable() with n->dev == NULL (lan966x is
devm_kzalloc'ed), and napi_disable() does netdev_lock(n->dev).
I believe the fix mentioned above also covers this.
The NAPI may already be disabled. lan966x_remove() calls
ops->fdma_deinit() (which disables the NAPI and frees/unmaps both rings)
before lan966x_cleanup_ports() unregisters the netdevs, and
lan966x_shutdown() disables the NAPI without clearing fdma_ndev. A
concurrent MTU change then calls napi_disable() a second time and
napi_disable_locked() spins:
net/core/dev.c:napi_disable_locked() {
...
while (val & (NAPIF_STATE_SCHED | NAPIF_STATE_NPSVC)) {
usleep_range(20, 200);
val = READ_ONCE(n->state);
}
...
}
There is no timeout and no other wake condition, and ndo_change_mtu
holds rtnl throughout, which also blocks the unregister_netdev() that
would end the window. The matching napi_enable() calls on both exit
paths below have the same issue.
I'd say pre-existing. In order to fix this race, we need a proper net fix to
reorder the probe init/deinit paths.
quoted
+ lan966x_fdma_tx_disable_netdev(lan966x);+ lan966x_fdma_rx_disable(&lan966x->rx);+ lan966x_fdma_tx_disable(&lan966x->tx);++ lan966x->rx.max_mtu = new_mtu;++ /* Must be NULL'ed in order to realloc them. */+ lan966x->rx.fdma.atu_region = NULL;+ lan966x->tx.fdma.atu_region = NULL;++ lan966x->tx.fdma.db_size = FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+ lan966x->tx.fdma.size = fdma_get_size_contiguous(&lan966x->tx.fdma);+ lan966x->rx.fdma.db_size = FDMA_PCI_DB_SIZE(lan966x->rx.max_mtu);+ lan966x->rx.fdma.size = fdma_get_size_contiguous(&lan966x->rx.fdma);++ err = lan966x_fdma_pci_rx_alloc(&lan966x->rx);+ if (err)+ goto restore;++ err = lan966x_fdma_pci_tx_alloc(&lan966x->tx);+ if (err) {+ fdma_free_coherent_and_unmap(lan966x->dma_dev,+ &lan966x->rx.fdma);+ goto restore;+ }++ /* Free and unmap old memory. */+ fdma_free_coherent_and_unmap(lan966x->dma_dev, &rx_fdma_old);+ fdma_free_coherent_and_unmap(lan966x->dma_dev, &tx_fdma_old);++ napi_enable(&lan966x->napi);+ lan966x_fdma_rx_start(&lan966x->rx);+ lan966x_fdma_pci_wakeup_netdev(lan966x);
[Severity: Low]
This isn't a bug as such, but the restart order here is the reverse of
the pre-existing non-PCI path. lan966x_fdma_reload() does:
lan966x_fdma_rx_start(&lan966x->rx);
...
lan966x_fdma_wakeup_netdev(lan966x);
napi_enable(&lan966x->napi);
Once napi_enable() clears NAPI_STATE_DISABLE, a pending FDMA DB
interrupt reaching napi_schedule() from lan966x_fdma_irq_handler() can
run lan966x_fdma_pci_napi_poll() on another CPU concurrently with
lan966x_fdma_rx_start(). Both then do unsynchronized read-modify-write
on FDMA_INTR_DB_ENA:
lan966x_fdma_pci_napi_poll():
if (counter < weight && napi_complete_done(napi, counter))
lan_wr(0xff, lan966x, FDMA_INTR_DB_ENA);
lan966x_fdma_rx_start() instead does lan_rd() followed by lan_rmw() on
the same register, and the poll can also issue FDMA_CH_RELOAD for a
channel rx_start() has not activated yet. Would matching the ordering
used by lan966x_fdma_reload() be preferable?
The order is deliberate - it was swapped in v6 after an earlier round of this
review flagged the platform order as the live bug: waking the queues before NAPI
is live lets a TX clobber the extraction bit, with no poll scheduled to restore
it. v7 will carry a comment saying so.
quoted
++ return err;+restore:++ /* No new buffers are allocated at this point. Use the old buffers,+ * but reset them before starting the FDMA again.+ */++ memcpy(&lan966x->tx.fdma, &tx_fdma_old, sizeof(struct fdma));+ memcpy(&lan966x->rx.fdma, &rx_fdma_old, sizeof(struct fdma));++ lan966x->rx.max_mtu = old_mtu;++ lan966x_fdma_pci_reset_mem(lan966x);++ napi_enable(&lan966x->napi);+ lan966x_fdma_rx_start(&lan966x->rx);+ lan966x_fdma_pci_wakeup_netdev(lan966x);++ return err;+}+
[ ... ]
quoted
static int lan966x_fdma_pci_resize(struct lan966x *lan966x) {- return -EOPNOTSUPP;+ struct fdma rx_fdma;+ int max_mtu;++ max_mtu = lan966x_fdma_get_max_frame(lan966x);+ if (max_mtu == lan966x->rx.max_mtu)+ return 0;++ /* rx and tx have n_dbs == 1, so both rings need the same contiguous+ * dma_alloc_coherent() block, which can't exceed MAX_PAGE_ORDER. The+ * allocation is padded to the ATU region granularity, so test the+ * padded size.+ */+ rx_fdma = lan966x->rx.fdma;+ rx_fdma.db_size = FDMA_PCI_DB_SIZE(max_mtu);+ if (ALIGN(fdma_get_size_contiguous(&rx_fdma),+ FDMA_PCI_ATU_REGION_ALIGN) > (PAGE_SIZE << MAX_PAGE_ORDER))+ return -ERANGE;
[Severity: Medium]
Does the MTU range the netdev advertises still match what this path can
actually do?
lan966x_probe_port() sets dev->max_mtu = ETH_MAX_MTU for the PCIe
instantiation too, but with n_dcbs == FDMA_PCI_DCB_MAX (256),
n_dbs == FDMA_RX_DCB_MAX_DBS (1) and db_size == ALIGN(max_frame, 128),
the padded contiguous allocation crosses PAGE_SIZE << MAX_PAGE_ORDER at
roughly 15.7 KiB of MTU on a 4 KiB-page build, so larger requests get a
bare -ERANGE that lan966x_port_change_mtu() passes straight to
userspace, and the cut-off moves with PAGE_SIZE and MAX_PAGE_ORDER.
The dev->max_mtu = ETH_MAX_MTU is intentional. Max MTU is rejected at runtime
and scales with PAGE_SIZE and MAX_PAGE_ORDER, depending on kernel configuration.
There is a second effect for MTUs in [65518, 65535], which the
advertised max_mtu still permits. lan966x_port_change_mtu() programs
the register before calling the resize callback:
lan_wr(DEV_MAC_MAXLEN_CFG_MAX_LEN_SET(LAN966X_HW_MTU(new_mtu)),
lan966x, DEV_MAC_MAXLEN_CFG(port->chip_port));
DEV_MAC_MAXLEN_CFG_MAX_LEN is GENMASK(15, 0) and LAN966X_HW_MTU(mtu) is
mtu + 18, so FIELD_PREP() masks the runtime value, e.g. mtu 65535
programs MAX_LEN = 65553 & 0xffff = 17. lan966x_fdma_pci_resize() then
derives max_mtu from that already-truncated register via
lan966x_fdma_get_max_frame() -> lan966x_fdma_get_max_mtu() ->
lan_rd(DEV_MAC_MAXLEN_CFG), so it either returns 0 (multi-port case,
where the max over ports is unchanged) or reloads with a tiny db_size,
and the MTU change is reported as successful while the MAC is configured
for a 17-byte frame.
True and pre-existing in the platform path. If I decide to cap at dev->max_mtu,
which requires a PCI special case (lan966x_is_pci()), I might as well fix this
one by ETH_MAX_MTU - (ETH_HLEN + ETH_FCS_LEN).
Before this patch the PCIe callback returned -EOPNOTSUPP for every
resize, so lan966x_port_change_mtu() always rolled the register back;
this patch is what makes the path reachable. Would capping dev->max_mtu
for the PCIe path (to both the ring limit and the 16-bit MAC field) be
the better fix, so the resize-time rejection becomes unreachable?
quoted
++ /* db_size is also handed to the FDMA in the 16-bit DCB DATAL field,+ * where a larger value would be silently truncated.+ */+ if (rx_fdma.db_size > GENMASK(15, 0))+ return -ERANGE;++ return __lan966x_fdma_pci_reload(lan966x, max_mtu); }
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-15 10:34:24
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 1 · Low: 0
- [Medium] Inconsistent XDP frame presentation between the two RX hooks
of the same driver: after this patch lan966x has two in-tree XDP RX…
--
net: lan966x: add PCIe FDMA XDP support
This adds XDP support to the lan966x PCIe FDMA path, using contiguous
ATU-mapped buffers and a memcpy-based XDP_TX instead of the page_pool
scheme used by the platform path. The IFH and FCS are stripped before
the BPF program runs, and lan966x_xdp_setup() now skips the page_pool
reload for the PCIe case. Only XDP_ACT_BASIC is advertised for PCIe.
@@ -158,38 +275,33 @@ static int lan966x_fdma_pci_rx_check_frame(struct lan966x_rx *rx, u64 *src_port) if (!lan966x_fdma_pci_rx_size_fits(fdma, blockl)) return FDMA_ERROR;- return FDMA_PASS;+ /* Present the Ethernet frame (no IFH, no FCS). HW re-inserts the+ * FCS on TX; see lan966x_fdma_pci_xmit_xdpf(). May be overridden+ * by XDP. The FCS strip is unconditional because NETIF_F_RXFCS+ * is not advertised in hw_features.+ */+ *data = virt_addr + IFH_LEN_BYTES;+ *data_len = blockl - IFH_LEN_BYTES - ETH_FCS_LEN;++ return lan966x_xdp_pci_run(port, virt_addr, *data_len, data, data_len); }
[Severity: Medium]
Should the two lan966x XDP receive hooks agree on whether the FCS is part
of the window handed to the BPF program?
On the PCIe path, lan966x_fdma_pci_rx_check_frame() subtracts the FCS
before calling into the program:
*data_len = blockl - IFH_LEN_BYTES - ETH_FCS_LEN;
return lan966x_xdp_pci_run(port, virt_addr, *data_len, data, data_len);
On the platform path, lan966x_fdma_rx_check_frame() passes the raw BLOCKL
value and lan966x_xdp_run() only removes the IFH:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_rx_check_frame() {
...
return lan966x_xdp_run(port, page, FDMA_DCB_STATUS_BLOCKL(db->status));
}
drivers/net/ethernet/microchip/lan966x/lan966x_xdp.c:lan966x_xdp_run() {
...
xdp_prepare_buff(&xdp, page_address(page),
IFH_LEN_BYTES + XDP_PACKET_HEADROOM,
data_len - IFH_LEN_BYTES, false);
}
That BLOCKL value still contains the FCS, as shown by the trim done later
on the same path:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_rx_get_frame() {
...
skb_put(skb, FDMA_DCB_STATUS_BLOCKL(db->status));
...
skb_pull(skb, IFH_LEN_BYTES);
if (likely(!(skb->dev->features & NETIF_F_RXFCS)))
skb_trim(skb, skb->len - ETH_FCS_LEN);
}
So xdp.data_end points four bytes past the end of the Ethernet payload on
the platform path, and exactly at the end of the payload on the new PCIe
path. Both windows stay inside the DMA buffer, so this looks like a
behavioural difference rather than an out-of-bounds access.
A program that parses backwards from data_end, compares data_end - data
against an L3 length field, or trims via bpf_xdp_adjust_tail() would get
different results depending on whether the switch is platform- or
PCIe-attached. The PCIe framing matches the usual XDP convention of no
FCS, which would make the platform hook the one that differs from
convention.
They should, and the PCIe framing is the one to keep. The strip was asked for by an
earlier round of this review, in v3, because the PCIe hook honours the post-program
window - lan966x_xdp_pci_run() writes back xdp.data_end - xdp.data and XDP_TX uses
it - so an FCS left in there would be delivered as payload whenever a program
touched the tail. The platform hook does not use them for XDP_PASS or XDP_TX at
all - it passes the pre-program length instead.
@@ -874,11 +874,13 @@ static int lan966x_probe_port(struct lan966x *lan966x, u32 p,port->phylink=phylink;-/* XDP is not supported on the PCIe FDMA path. */-if(lan966x->fdma&&!lan966x_is_pci(lan966x))-dev->xdp_features=NETDEV_XDP_ACT_BASIC|-NETDEV_XDP_ACT_REDIRECT|-NETDEV_XDP_ACT_NDO_XMIT;+if(lan966x->fdma){+dev->xdp_features=NETDEV_XDP_ACT_BASIC;++if(!lan966x_is_pci(lan966x))+dev->xdp_features|=NETDEV_XDP_ACT_REDIRECT|+NETDEV_XDP_ACT_NDO_XMIT;+}
[Severity: Medium]
Both paths now report the same NETDEV_XDP_ACT_BASIC bit from
lan966x_probe_port(), so is there anything in the uAPI that lets a
program distinguish the FCS-included window of lan966x_xdp_run() from
the FCS-stripped window of lan966x_xdp_pci_run()?
Would it make sense to make one of the two hooks match the other so that
the frame presentation is the same for identically-advertised netdevs?
Same as above - the correct handling is removing the FCS, so the platform hook
is the one to move. There is no uAPI signal to describe the difference with
either, so converging is the only option.
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-15 10:38:33
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 1
- [Low] Claimed patch-ordering / bisectability bug: the "cpu" reg window
is enlarged from 0x100000 to 0x800000 only in this commit, while the…
--
misc: lan966x-pci: dts: extend cpu reg to cover PCIE DBI space
This grows the "cpu" register window of the switch node in the lan966x PCI
device tree overlay from 0x100000 to 0x800000 so that the PCIE DBI and iATU
registers at offset 0x400000+ are covered. Those registers are programmed
when the FDMA engine sets up the ATU outbound windows.
No Link: tags in the commit header.
[Severity: Low]
This isn't a bug, just a question about the ordering of the series.
The earlier commits "net: lan966x: add PCIe FDMA support", "net: lan966x: add
PCIe FDMA MTU change support" and "net: lan966x: add PCIe FDMA XDP support"
already derive regs[TARGET_PCIE_DBI] from the "cpu" base plus 0x400000, and
fdma_pci_atu_init() writes at DBI + 0x300000 + idx * 0x200:
drivers/net/ethernet/microchip/fdma/fdma_pci.c
static void __iomem *fdma_pci_atu_addr_get(void __iomem *addr, int offset, int idx)
{
return addr + FDMA_PCI_ATU_ADDR + FDMA_PCI_ATU_IDX_SIZE * idx + offset;
}
That lands roughly 0x700000 from the "cpu" base, i.e. outside the 0x100000
mapping that exists until this commit. Would it be clearer to move this
overlay change before the driver commits that compute the DBI offset, so
that the mapping is never smaller than the offsets the driver derives from
it?
Yes, intentional: the window is widened here and only the next commit names the
"fdma" interrupt that lets anything use it.
For what it is worth, the intermediate commits do look safe in practice,
since the ATU access in lan966x_fdma_pci_init() is gated:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma_pci.c
if (!lan966x->fdma)
return 0;
...
fdma_pci_atu_init(&lan966x->atu, lan966x->regs[TARGET_PCIE_DBI]);
and lan966x->fdma is only set when the node names an "fdma" interrupt in
lan966x_probe():
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
...
lan966x->fdma = true;
}
The overlay here still carries only interrupt-names = "xtr", "ana", and the
"fdma" interrupt is added by the following commit, so the enlarged window is
in place before the FDMA path can be enabled. Is that ordering intentional?
From: Daniel Machon <daniel.machon@microchip.com> Date: 2026-09-15 10:52:28
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 1 · Low: 0
- [Medium] The FDMA interrupt is hooked up (and thereby unmasked in the
OIC) long before any FDMA state exists.
--
misc: lan966x-pci: dts: add fdma interrupt to overlay
This adds OIC interrupt 14 as the "fdma" named interrupt to the lan966x
PCI device tree overlay, so FDMA-based injection/extraction can be used
when the switch is reached over PCIe.
[Severity: Medium]
With source 14 now named "fdma", platform_get_irq_byname(pdev, "fdma")
starts succeeding on the PCIe path, and lan966x_probe() requests the
handler right there:
drivers/net/ethernet/microchip/lan966x/lan966x_main.c:lan966x_probe() {
lan966x->fdma_irq = platform_get_irq_byname(pdev, "fdma");
if (lan966x->fdma_irq > 0) {
err = devm_request_irq(&pdev->dev, lan966x->fdma_irq,
lan966x_fdma_irq_handler, 0,
"fdma irq", lan966x);
...
lan966x->fdma = true;
}
}
Is this ordering safe? The OIC maps and unmasks source 14 at
devm_request_irq() time, but at that point no FDMA state exists yet:
- netif_napi_add()/napi_enable() only run later, from
lan966x_port_init() -> lan966x_fdma_netdev_init() in the per-port
probe loop, so lan966x->napi is still the zeroed devm_kzalloc()
memory and napi.poll is NULL.
- the DCB rings, the LLP/ATU windows and the FDMA_CTRL NRESET pulse are
all done even later, from lan966x->ops->fdma_init() ->
lan966x_fdma_pci_init().
If the OIC delivers an FDMA interrupt in that window, the handler
schedules NAPI unconditionally:
drivers/net/ethernet/microchip/lan966x/lan966x_fdma.c:lan966x_fdma_irq_handler() {
db = lan_rd(lan966x, FDMA_INTR_DB);
err = lan_rd(lan966x, FDMA_INTR_ERR);
if (db) {
lan_wr(0, lan966x, FDMA_INTR_DB_ENA);
lan_wr(db, lan966x, FDMA_INTR_DB);
napi_schedule(&lan966x->napi);
}
}
Can this then oops in __napi_poll(), either calling n->poll() while it
is still NULL, or running lan966x_fdma_pci_napi_poll() against
rx->fdma.dcbs == NULL and n_dcbs == 0, where fdma_has_frames() ->
fdma_db_get() dereferences the dcbs array?
How reachable is that window in practice? The series itself describes
the card arriving with FDMA state from a previous boot. The commit
"net: lan966x: clear FDMA interrupt stickies after switch reset" says
the stickies "sit in FDMA_INTR_ERR until the FDMA IRQ is requested later
in probe, at which point the handler fires immediately and WARNs", and
"net: lan966x: add shutdown callback to stop FDMA on reboot" says that
on the endpoint "channels stay active and interrupt sources stay armed
across the reset" and that "FDMA_INTR_ENA persists on the card across a
warm reboot".
The sticky clear in lan966x_reset_switch() clears the latched bits at
one instant, but does not mask FDMA_INTR_ENA/FDMA_INTR_DB_ENA and does
not stop the channels. After a reboot that skips device_shutdown()
(panic reboot, kexec/kdump, watchdog warm reset with the card still
powered), could a still-active channel latch FDMA_INTR_DB again after
the clear and have it delivered as soon as source 14 is unmasked?
Would it be more robust to reset and mask the FDMA block (the
FDMA_CTRL NRESET pulse that already exists in lan966x_fdma_pci_init(),
plus clearing FDMA_INTR_ENA/FDMA_INTR_DB_ENA) before
devm_request_irq(), or to request the FDMA IRQ only after fdma_init()
has built the rings and added the NAPI instance?
I think this is pre-existing. It can oops, both variants, and yes - a channel the
previous kernel left active can re-latch FDMA_INTR_DB before devm_request_irq()
unmasks the source. Your second suggestion is the right fix; the reset half of the
first is what fdma_init()'s NRESET pulse already does.
None of this originates here: the request has sat before netif_napi_add() and
ops->fdma_init() since c8349639324a ("net: lan966x: Add FDMA functionality"), and
the SoC device tree has named "fdma" since 2952d32c0611 ("ARM: dts: lan966x: add
switch node") - this patch adds one entry to an interrupt-names list. Nothing to
change in this patch; it goes to net with the same reorder as the MTU-change problem
earlier in the series.