* [PATCH v9 net-next 0/8] Switch support
@ 2026-08-31 13:19 Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files Ratheesh Kannoth
` (7 more replies)
0 siblings, 8 replies; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Marvell OcteonTX2 switch hardware is capable of accelerating L2, L3, and
flow. When representors are enabled through devlink, a logical port is
created in switch hardware for each representor device.
This patch series implements communication from the host OS to the switch
hardware and vice versa.
control plane (FDB / FIB / flow over mailbox)
+---------------------------------------------------------------------+
| HOST OS |
| |
| +------------+ +---------------------+ +---------------+ |
| | bridge / | | PF + notifiers | | TC / flower | |
| | routing | | (FDB, FIB, flow) | | offload | |
| +------+-----+ +-----------+---------+ +-------+-------+ |
| ^ | | |
| | slow path | | |
| | (unmatched pkts) v v |
| +------+------+ +------+------+ +------+------+ |
| | rep-eth0 | | PF / VF | <----> | RVU AF | |
| +------+------+ +-------------+ +------+------+ |
| | | |
| +------+------+ | |
| | rep-eth1 | | |
| +-------------+ | |
| | slow path (miss / control to host stack) | |
+---------------------------------------------------------------------+
| | | |
| | mailbox / PCIe | |
| v v |
+---------------------------------------------------------------------+
| SWITCH HARDWARE |
| |
| +---------------------------------------------------+ |
| | L2 tables | L3 tables | flow TCAM (HW) | |
| +-------+-------------+---------------+-------------+ |
| | fast path (HW) | |
| v v |
| lport0 <========================> lport1 |
| HW forwarding between ports |
| |
| slow path (miss): packets raised to host via representors |
+---------------------------------------------------------------------+
When representors are created, corresponding logical ports are created in
switchdev. Matching traffic is accelerated in switch hardware using
installed L2, L3, and flow rules. Packets that do not match offloaded
state, or that require software handling, take the slow path through
representors and the host networking stack. Control updates from the host
(FDB, FIB, TC flower) are sent to the AF over mailbox and programmed into
switch hardware tables.
Notifier callbacks are registered to receive system events such as FDB
add/delete and FIB add/delete. Flow add/delete operations are handled
through the ingress flow-table offload path. These events are captured and
processed by the NIC driver and forwarded to the switch device through the
AF driver. All message exchanges use the mailbox interface.
Bridge acceleration:
FDB add/delete notifications are processed, and learned SMAC information is
sent to the switch hardware. The switch inserts a hardware rule to
accelerate packets destined to the MAC address. Switch-initiated FDB
refresh is propagated back to the host bridge through an AF to PF/VF
mailbox and switchdev notifier.
L3 acceleration:
IPv4 and IPv6 FIB updates observed through netdev and FIB notifiers are
queued on the PF and sent to the AF. The AF batches fib_entry structures
and forwards them to switchdev when firmware is ready.
Flow acceleration:
TC flower rules accepted by the ingress flow-table offload callback are
translated into fl_tuple state, ingress and egress pcifunc are resolved
through FIB lookup, and flow updates are sent to the switch hardware
through the mailbox interface. Per-cookie packet counters are kept in sync
using NPC MCAM multi-stats when the switch requests a flow refresh.
Ratheesh Kannoth (9):
octeontx2-af: switch: Add AF to switch mbox and skeleton files
Host to switch mailbox definitions for FDB, FIB, flow, and flow stats;
AF-side L2/L3/flow skeleton objects.
octeontx2-af: switch: Add switch dev to AF mboxes
Switch to AF and AF to switchdev mailbox messages; interface query
handler and NPC helpers for flow delete/stats/features.
octeontx2-pf: switch: Add pf files hierarchy
CONFIG_OCTEONTX_SWITCH and stub PF switch objects for FDB, FIB, flow, and
notifier plumbing.
octeontx2-af: switch: Representor for switch port
Copy devlink switch_id to the AF and map representor pcifunc to a switch
port id when eswitch mode is enabled.
octeontx2-af: PAN switch TL1 scheduling and NPC channel control
Allocate multiple TL1 scheduler queues in PAN mode and honor caller
supplied NPC channel masks for multi-link steering.
octeontx2-pf: register switch notifiers for eswitch offload
Register PF notifier blocks for FIB, neighbour, address, netdev, and
switchdev FDB events; split IPv4/IPv6 handling.
octeontx2: plumb bridge FDB updates through AF and switchdev
End-to-end L2 offload from switchdev FDB notifications to AF to
switchdev, including firmware gating and FDB refresh to host.
octeontx2: offload host FIB updates to switch via AF mailbox
Queue IPv4/IPv6 FIB updates from notifiers and batch fib_entry delivery
from AF to switchdev.
octeontx2: add TC flow offload path for switch flows
Ingress flow-table offload for TC flower rules, AF forwarding to
switchdev, and flow counter refresh from switch to host.
.../net/ethernet/marvell/octeontx2/Kconfig | 13 +
.../ethernet/marvell/octeontx2/af/Makefile | 3 +-
.../net/ethernet/marvell/octeontx2/af/mbox.h | 227 ++++++++
.../net/ethernet/marvell/octeontx2/af/rvu.c | 110 ++++
.../net/ethernet/marvell/octeontx2/af/rvu.h | 6 +
.../ethernet/marvell/octeontx2/af/rvu_nix.c | 53 +-
.../ethernet/marvell/octeontx2/af/rvu_npc.c | 76 +++
.../marvell/octeontx2/af/rvu_npc_fs.c | 11 +
.../ethernet/marvell/octeontx2/af/rvu_rep.c | 3 +-
.../marvell/octeontx2/af/switch/rvu_sw.c | 48 ++
.../marvell/octeontx2/af/switch/rvu_sw.h | 14 +
.../marvell/octeontx2/af/switch/rvu_sw_fl.c | 294 ++++++++++
.../marvell/octeontx2/af/switch/rvu_sw_fl.h | 12 +
.../marvell/octeontx2/af/switch/rvu_sw_l2.c | 283 +++++++++
.../marvell/octeontx2/af/switch/rvu_sw_l2.h | 13 +
.../marvell/octeontx2/af/switch/rvu_sw_l3.c | 215 +++++++
.../marvell/octeontx2/af/switch/rvu_sw_l3.h | 11 +
.../ethernet/marvell/octeontx2/nic/Makefile | 13 +-
.../marvell/octeontx2/nic/otx2_txrx.h | 2 +
.../ethernet/marvell/octeontx2/nic/otx2_vf.c | 17 +
.../net/ethernet/marvell/octeontx2/nic/rep.c | 11 +
.../marvell/octeontx2/nic/switch/sw_fdb.c | 144 +++++
.../marvell/octeontx2/nic/switch/sw_fdb.h | 14 +
.../marvell/octeontx2/nic/switch/sw_fib.c | 132 +++++
.../marvell/octeontx2/nic/switch/sw_fib.h | 16 +
.../marvell/octeontx2/nic/switch/sw_fl.c | 546 ++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fl.h | 15 +
.../marvell/octeontx2/nic/switch/sw_nb.c | 422 ++++++++++++++
.../marvell/octeontx2/nic/switch/sw_nb.h | 35 ++
.../marvell/octeontx2/nic/switch/sw_nb_v4.c | 335 +++++++++++
.../marvell/octeontx2/nic/switch/sw_nb_v4.h | 21 +
.../marvell/octeontx2/nic/switch/sw_nb_v6.c | 236 ++++++++
.../marvell/octeontx2/nic/switch/sw_nb_v6.h | 21 +
33 files changed, 3361 insertions(+), 11 deletions(-)
---
v8 -> v9: Addressed Sashiko comments
https://sashiko.dev/#/patchset/20260811085406.3227312-1-rkannoth%40marvell.com
v7 -> v8: Addressed Sashiko comments
https://sashiko.dev/#/patchset/20260810034738.1786029-1-rkannoth%40marvell.com
v6 -> v7: Addressed Sashiko comments
https://lore.kernel.org/netdev/20260729045100.2177958-1-rkannoth@marvell.com/
v5 -> v6: Addressed Jakub/Sashiko comments
https://lore.kernel.org/netdev/20260724094018.3213907-1-rkannoth@marvell.com/
v4 -> v5: Addressed Sashiko comments
https://lore.kernel.org/netdev/20260721081824.1430607-1-rkannoth@marvell.com/
v3 -> v4: Addressed Sashiko comments
https://lore.kernel.org/netdev/20260714015331.1801922-1-rkannoth@marvell.com/
v2 -> v3: Addressed Pabolo comments.
https://lore.kernel.org/netdev/20260702045026.2914748-1-rkannoth@marvell.com/
v1 -> v2: Addressed Jakub comments
https://lore.kernel.org/netdev/20260630024715.4124281-1-rkannoth@marvell.com/
--
2.43.0
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,1/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 2/8] octeontx2-af: switch: Add switch dev to AF mboxes Ratheesh Kannoth
` (6 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
The Marvell switch hardware runs on a Linux OS. This OS receives
various messages, which are parsed to create flow rules that can be
installed on HW. The switch is capable of accelerating both L2 and
L3 flows.
This commit adds mailbox messages used by the Linux OS (on arm64) to
send events to the switch hardware, along with skeleton handler
functions:
fdb messages: Linux bridge FDB messages
fib messages: Linux routing table messages
fl messages: Flow acceleration tuple and actions (FL_NOTIFY)
fl stats: Host-initiated flow counter polling (FL_GET_STATS)
fl_tuple defines the flow acceleration match tuple exchanged over the
mailbox. It currently carries IPv4 five-tuple and L2 match fields only.
IPv6 flow acceleration is not supported in this patch and will be added
in a follow-up change extending the mailbox ABI.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../ethernet/marvell/octeontx2/af/Makefile | 3 +-
.../net/ethernet/marvell/octeontx2/af/mbox.h | 118 ++++++++++++++++++
.../marvell/octeontx2/af/switch/rvu_sw_fl.c | 21 ++++
.../marvell/octeontx2/af/switch/rvu_sw_fl.h | 11 ++
.../marvell/octeontx2/af/switch/rvu_sw_l2.c | 14 +++
.../marvell/octeontx2/af/switch/rvu_sw_l2.h | 11 ++
.../marvell/octeontx2/af/switch/rvu_sw_l3.c | 14 +++
.../marvell/octeontx2/af/switch/rvu_sw_l3.h | 11 ++
8 files changed, 202 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/Makefile b/drivers/net/ethernet/marvell/octeontx2/af/Makefile
index 91b7d6e96a61..82dd387308c9 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/Makefile
+++ b/drivers/net/ethernet/marvell/octeontx2/af/Makefile
@@ -3,7 +3,7 @@
# Makefile for Marvell's RVU Admin Function driver
#
-ccflags-y += -I$(src)
+ccflags-y += -I$(src) -I$(src)/switch/
obj-$(CONFIG_OCTEONTX2_MBOX) += rvu_mbox.o
obj-$(CONFIG_OCTEONTX2_AF) += rvu_af.o
@@ -12,5 +12,6 @@ rvu_af-y := cgx.o rvu.o rvu_cgx.o rvu_npa.o rvu_nix.o \
rvu_reg.o rvu_npc.o rvu_debugfs.o ptp.o rvu_npc_fs.o \
rvu_cpt.o rvu_devlink.o rpm.o rvu_cn10k.o rvu_switch.o \
rvu_sdp.o rvu_npc_hash.o mcs.o mcs_rvu_if.o mcs_cnf10kb.o \
+ switch/rvu_sw_l2.o switch/rvu_sw_l3.o switch/rvu_sw_fl.o\
rvu_rep.o cn20k/mbox_init.o cn20k/nix.o cn20k/debugfs.o \
cn20k/npa.o cn20k/npc.o
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index cece197d1074..854696d2a35f 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -164,6 +164,14 @@ M(PTP_GET_CAP, 0x00c, ptp_get_cap, msg_req, ptp_get_cap_rsp) \
M(GET_REP_CNT, 0x00d, get_rep_cnt, msg_req, get_rep_cnt_rsp) \
M(ESW_CFG, 0x00e, esw_cfg, esw_cfg_req, msg_rsp) \
M(REP_EVENT_NOTIFY, 0x00f, rep_event_notify, rep_event, msg_rsp) \
+M(FDB_NOTIFY, 0x010, fdb_notify, \
+ fdb_notify_req, msg_rsp) \
+M(FIB_NOTIFY, 0x011, fib_notify, \
+ fib_notify_req, msg_rsp) \
+M(FL_NOTIFY, 0x012, fl_notify, \
+ fl_notify_req, msg_rsp) \
+M(FL_GET_STATS, 0x013, fl_get_stats, \
+ fl_get_stats_req, fl_get_stats_rsp) \
/* CGX mbox IDs (range 0x200 - 0x3FF) */ \
M(CGX_START_RXTX, 0x200, cgx_start_rxtx, msg_req, msg_rsp) \
M(CGX_STOP_RXTX, 0x201, cgx_stop_rxtx, msg_req, msg_rsp) \
@@ -1812,6 +1820,116 @@ struct rep_event {
struct rep_evt_data evt_data;
};
+#define OTX2_FDB_ADD BIT_ULL(0)
+#define OTX2_FDB_DEL BIT_ULL(1)
+#define OTX2_FIB_CMD BIT_ULL(2)
+#define OTX2_FL_ADD BIT_ULL(3)
+#define OTX2_FL_DEL BIT_ULL(4)
+#define OTX2_DP_ADD BIT_ULL(5)
+
+struct fdb_notify_req {
+ struct mbox_msghdr hdr;
+ u64 flags;
+ u8 mac[ETH_ALEN];
+ u8 rsvd[2]; /* explicit tail padding */
+};
+
+/* Zero-initialize before populating fields shared with switch OS. */
+struct fib_entry {
+ u64 cmd;
+ u64 gw_valid : 1;
+ u64 mac_valid : 1;
+ u64 vlan_valid: 1;
+ u64 host : 1;
+ u64 bridge : 1;
+ u64 ipv6 : 1;
+ u64 rsvd : 58;
+ __be16 vlan_tag;
+ u16 rsvd1;
+ u32 dst_len;
+ u8 dst6_plen;
+ u8 gw6_plen;
+ u8 rsvd2[2]; /* explicit padding before address unions */
+ union {
+ __be32 dst;
+ __be32 dst6[4];
+ };
+ union {
+ __be32 gw;
+ __be32 gw6[4];
+ };
+ u16 port_id;
+ u8 nud_state;
+ u8 rsvd3;
+ u8 mac[ETH_ALEN];
+ u16 rsvd4; /* explicit tail padding */
+};
+
+struct fib_notify_req {
+ struct mbox_msghdr hdr;
+ u16 cnt;
+ u16 rsvd[3]; /* explicit padding for entry[] 8-byte alignment */
+ struct fib_entry entry[16];
+};
+
+struct fl_tuple {
+ __be32 ip4src;
+ __be32 m_ip4src;
+ __be32 ip4dst;
+ __be32 m_ip4dst;
+ __be16 sport;
+ __be16 m_sport;
+ __be16 dport;
+ __be16 m_dport;
+ __be16 eth_type;
+ __be16 m_eth_type;
+ u8 proto;
+ u8 rsvd_l3[3]; /* explicit padding before MAC addresses */
+ u8 smac[6];
+ u8 m_smac[6];
+ u8 dmac[6];
+ u8 m_dmac[6];
+ u64 is_xdev_br : 1;
+ u64 is_indev_br : 1;
+ u64 uni_di : 1;
+ u64 rsvd_br : 61;
+ u16 in_pf;
+ u16 xmit_pf;
+ u16 rsvd;
+ u16 rsvd_align; /* explicit padding before u64 features */
+ u64 features;
+ struct { /* FLOW_ACTION_MANGLE */
+ u8 offset;
+ u8 type;
+ u16 rsvd;
+ u32 mask;
+ u32 val;
+#define MANGLE_ARR_SZ 9
+ } mangle[MANGLE_ARR_SZ]; /* 2 for ETH, 1 for VLAN, 4 for IPv6, 2 for L4. */
+#define MANGLE_LAYER_CNT 4
+ u16 mangle_map[MANGLE_LAYER_CNT]; /* 1 for ETH, 1 for VLAN, 1 for L3, 1 for L4 */
+ u8 mangle_cnt;
+ u8 rsvd_tail[3]; /* explicit tail padding */
+};
+
+struct fl_notify_req {
+ struct mbox_msghdr hdr;
+ u64 cookie;
+ u64 flags;
+ u64 features;
+ struct fl_tuple tuple;
+};
+
+struct fl_get_stats_req {
+ struct mbox_msghdr hdr;
+ u64 cookie;
+};
+
+struct fl_get_stats_rsp {
+ struct mbox_msghdr hdr;
+ u64 pkts_diff;
+};
+
struct flow_msg {
unsigned char dmac[6];
unsigned char smac[6];
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c
new file mode 100644
index 000000000000..1f8b82a84a5d
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "rvu.h"
+
+int rvu_mbox_handler_fl_get_stats(struct rvu *rvu,
+ struct fl_get_stats_req *req,
+ struct fl_get_stats_rsp *rsp)
+{
+ return 0;
+}
+
+int rvu_mbox_handler_fl_notify(struct rvu *rvu,
+ struct fl_notify_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.h
new file mode 100644
index 000000000000..cf3e5b884f77
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+
+#ifndef RVU_SW_FL_H
+#define RVU_SW_FL_H
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
new file mode 100644
index 000000000000..5f805bfa81ed
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "rvu.h"
+
+int rvu_mbox_handler_fdb_notify(struct rvu *rvu,
+ struct fdb_notify_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
new file mode 100644
index 000000000000..ff28612150c9
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+
+#ifndef RVU_SW_L2_H
+#define RVU_SW_L2_H
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
new file mode 100644
index 000000000000..2b798d5f0644
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "rvu.h"
+
+int rvu_mbox_handler_fib_notify(struct rvu *rvu,
+ struct fib_notify_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
new file mode 100644
index 000000000000..ac8c4f9ba5ac
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+
+#ifndef RVU_SW_L3_H
+#define RVU_SW_L3_H
+
+#endif
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 2/8] octeontx2-af: switch: Add switch dev to AF mboxes
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,2/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 3/8] octeontx2-pf: switch: Add pf files hierarchy Ratheesh Kannoth
` (5 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
The Marvell switch hardware runs on a Linux OS. Switch
needs various information from AF driver. These mboxes are defined
to query those from AF driver.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../ethernet/marvell/octeontx2/af/Makefile | 2 +-
.../net/ethernet/marvell/octeontx2/af/mbox.h | 130 +++++++++++++++++
.../net/ethernet/marvell/octeontx2/af/rvu.c | 134 +++++++++++++++++
.../net/ethernet/marvell/octeontx2/af/rvu.h | 1 +
.../ethernet/marvell/octeontx2/af/rvu_nix.c | 137 +++++++++++++-----
.../ethernet/marvell/octeontx2/af/rvu_npc.c | 115 +++++++++++++++
.../marvell/octeontx2/af/rvu_npc_fs.c | 11 ++
.../marvell/octeontx2/af/switch/rvu_sw.c | 15 ++
.../marvell/octeontx2/af/switch/rvu_sw.h | 11 ++
9 files changed, 518 insertions(+), 38 deletions(-)
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/Makefile b/drivers/net/ethernet/marvell/octeontx2/af/Makefile
index 82dd387308c9..73f20a44f1a0 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/Makefile
+++ b/drivers/net/ethernet/marvell/octeontx2/af/Makefile
@@ -12,6 +12,6 @@ rvu_af-y := cgx.o rvu.o rvu_cgx.o rvu_npa.o rvu_nix.o \
rvu_reg.o rvu_npc.o rvu_debugfs.o ptp.o rvu_npc_fs.o \
rvu_cpt.o rvu_devlink.o rpm.o rvu_cn10k.o rvu_switch.o \
rvu_sdp.o rvu_npc_hash.o mcs.o mcs_rvu_if.o mcs_cnf10kb.o \
- switch/rvu_sw_l2.o switch/rvu_sw_l3.o switch/rvu_sw_fl.o\
+ switch/rvu_sw.o switch/rvu_sw_l2.o switch/rvu_sw_l3.o switch/rvu_sw_fl.o \
rvu_rep.o cn20k/mbox_init.o cn20k/nix.o cn20k/debugfs.o \
cn20k/npa.o cn20k/npc.o
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index 854696d2a35f..e45e6e93ed08 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -172,6 +172,10 @@ M(FL_NOTIFY, 0x012, fl_notify, \
fl_notify_req, msg_rsp) \
M(FL_GET_STATS, 0x013, fl_get_stats, \
fl_get_stats_req, fl_get_stats_rsp) \
+M(IFACE_GET_INFO, 0x014, iface_get_info, msg_req, \
+ iface_get_info_rsp) \
+M(SWDEV2AF_NOTIFY, 0x015, swdev2af_notify, \
+ swdev2af_notify_req, msg_rsp) \
/* CGX mbox IDs (range 0x200 - 0x3FF) */ \
M(CGX_START_RXTX, 0x200, cgx_start_rxtx, msg_req, msg_rsp) \
M(CGX_STOP_RXTX, 0x201, cgx_stop_rxtx, msg_req, msg_rsp) \
@@ -317,8 +321,16 @@ M(NPC_MCAM_GET_DFT_RL_IDXS, 0x601e, npc_get_dft_rl_idxs, \
M(NPC_MCAM_GET_NPC_PFL_INFO, 0x601f, npc_get_pfl_info, \
msg_req, \
npc_get_pfl_info_rsp) \
+M(NPC_MCAM_FLOW_DEL_N_FREE, 0x6020, npc_flow_del_n_free, \
+ npc_flow_del_n_free_req, msg_rsp) \
M(NPC_MCAM_READ_DEFAULT_RULE, 0x6021, npc_read_default_rule, msg_req, \
npc_mcam_read_base_rule_rsp) \
+M(NPC_MCAM_GET_MUL_STATS, 0x6022, npc_mcam_mul_stats, \
+ npc_mcam_get_mul_stats_req, \
+ npc_mcam_get_mul_stats_rsp) \
+M(NPC_MCAM_GET_FEATURES, 0x6023, npc_mcam_get_features, \
+ msg_req, \
+ npc_mcam_get_features_rsp) \
/* NIX mbox IDs (range 0x8000 - 0xFFFF) */ \
M(NIX_LF_ALLOC, 0x8000, nix_lf_alloc, \
nix_lf_alloc_req, nix_lf_alloc_rsp) \
@@ -449,6 +461,12 @@ M(MCS_INTR_NOTIFY, 0xE00, mcs_intr_notify, mcs_intr_info, msg_rsp)
#define MBOX_UP_REP_MESSAGES \
M(REP_EVENT_UP_NOTIFY, 0xEF0, rep_event_up_notify, rep_event, msg_rsp) \
+#define MBOX_UP_AF2SWDEV_MESSAGES \
+M(AF2SWDEV, 0xEF1, af2swdev_notify, af2swdev_notify_req, msg_rsp)
+
+#define MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES \
+M(AF2PF_FDB_REFRESH, 0xEF2, af2pf_fdb_refresh, af2pf_fdb_refresh_req, msg_rsp)
+
enum {
#define M(_name, _id, _1, _2, _3) MBOX_MSG_ ## _name = _id,
MBOX_MESSAGES
@@ -456,6 +474,8 @@ MBOX_UP_CGX_MESSAGES
MBOX_UP_CPT_MESSAGES
MBOX_UP_MCS_MESSAGES
MBOX_UP_REP_MESSAGES
+MBOX_UP_AF2SWDEV_MESSAGES
+MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES
#undef M
};
@@ -1594,6 +1614,31 @@ struct npc_mcam_alloc_entry_rsp {
u16 entry_list[NPC_MAX_NONCONTIG_ENTRIES];
};
+struct npc_flow_del_n_free_req {
+ struct mbox_msghdr hdr;
+ u16 cnt;
+ u16 entry[256]; /* Entry index to be freed */
+};
+
+struct npc_mcam_get_features_rsp {
+ struct mbox_msghdr hdr;
+ u64 rx_features;
+ u64 tx_features;
+};
+
+struct npc_mcam_get_mul_stats_req {
+ struct mbox_msghdr hdr;
+ u16 cnt;
+ u16 entry[256]; /* mcam entry */
+};
+
+struct npc_mcam_get_mul_stats_rsp {
+ struct mbox_msghdr hdr;
+ u16 cnt;
+ u16 rsvd[3]; /* explicit padding for stat[] 8-byte alignment */
+ u64 stat[256]; /* counter stats */
+};
+
struct npc_mcam_free_entry_req {
struct mbox_msghdr hdr;
u16 entry; /* Entry index to be freed */
@@ -1930,6 +1975,91 @@ struct fl_get_stats_rsp {
u64 pkts_diff;
};
+struct af2swdev_notify_req {
+ struct mbox_msghdr hdr;
+ u64 flags;
+ u32 port_id;
+ u32 switch_id;
+ union {
+ struct {
+ u8 mac[6];
+ u8 rsvd_mac[2]; /* explicit padding to 8 bytes */
+ };
+ struct {
+ u8 cnt;
+ u8 rsvd[7]; /* explicit padding before fib_entry[] */
+ struct fib_entry entry[12];
+ };
+
+ struct {
+ u64 cookie;
+ u64 features;
+ struct fl_tuple tuple;
+ };
+ };
+};
+
+struct af2pf_fdb_refresh_req {
+ struct mbox_msghdr hdr;
+ u16 pcifunc;
+ u8 mac[6];
+};
+
+struct iface_info {
+ u8 is_vf : 1;
+ u8 is_sdp : 1;
+ u8 rsvd : 6;
+ u16 pcifunc;
+ u16 rx_chan_base;
+ u16 tx_chan_base;
+ u16 sq_cnt;
+ u16 cq_cnt;
+ u16 rq_cnt;
+ u8 rx_chan_cnt;
+ u8 tx_chan_cnt;
+ u8 tx_link;
+ u8 nix;
+};
+
+/* Max supported */
+#define IFACE_MAX (256 + 32) /* 32 PFs + 256 VFs */
+
+struct iface_get_info_rsp {
+ struct mbox_msghdr hdr;
+ u16 cnt;
+ u8 truncated;
+ u8 rsvd[5];
+ struct iface_info info[IFACE_MAX];
+};
+
+struct fl_info {
+ u64 cookie;
+ u16 mcam_idx[2];
+ u8 dis : 1;
+ u8 uni_di : 1;
+};
+
+struct swdev2af_notify_req {
+ struct mbox_msghdr hdr;
+ u64 msg_type;
+#define SWDEV2AF_MSG_TYPE_FW_STATUS BIT_ULL(0)
+#define SWDEV2AF_MSG_TYPE_REFRESH_FDB BIT_ULL(1)
+#define SWDEV2AF_MSG_TYPE_REFRESH_FL BIT_ULL(2)
+ u16 pcifunc;
+ u16 rsvd1[3]; /* explicit padding before union for 8-byte alignment */
+ union {
+ bool fw_up; // FW_STATUS message
+
+ u8 mac[ETH_ALEN]; // fdb refresh message
+
+ struct { // fl refresh message
+ u8 cnt;
+ u8 rsvd2[7];
+ struct fl_info fl[64];
+ };
+ };
+};
+
struct flow_msg {
unsigned char dmac[6];
unsigned char smac[6];
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
index 74c041ab5280..1402beccf661 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
@@ -1990,6 +1990,140 @@ int rvu_mbox_handler_msix_offset(struct rvu *rvu, struct msg_req *req,
return 0;
}
+static void rvu_iface_get_qcnts(struct rvu *rvu, struct rvu_pfvf *pfvf,
+ struct iface_info *info)
+{
+ struct admin_queue *aq;
+ unsigned long flags;
+
+ info->sq_cnt = 0;
+ info->cq_cnt = 0;
+ info->rq_cnt = 0;
+
+ aq = rvu->hw->block[pfvf->nix_blkaddr].aq;
+ if (!aq)
+ return;
+
+ spin_lock_irqsave(&aq->lock, flags);
+
+ /* Use each LF queue context size; bitmaps are sized to qsize longs. */
+ if (pfvf->sq_ctx && pfvf->sq_bmap)
+ info->sq_cnt = bitmap_weight(pfvf->sq_bmap, pfvf->sq_ctx->qsize);
+ if (pfvf->cq_ctx && pfvf->cq_bmap)
+ info->cq_cnt = bitmap_weight(pfvf->cq_bmap, pfvf->cq_ctx->qsize);
+ if (pfvf->rq_ctx && pfvf->rq_bmap)
+ info->rq_cnt = bitmap_weight(pfvf->rq_bmap, pfvf->rq_ctx->qsize);
+
+ spin_unlock_irqrestore(&aq->lock, flags);
+}
+
+int rvu_mbox_handler_iface_get_info(struct rvu *rvu, struct msg_req *req,
+ struct iface_get_info_rsp *rsp)
+{
+ struct iface_info *info;
+ bool truncated = false;
+ struct rvu_pfvf *pfvf;
+ int pf, vf, numvfs;
+ int tot = 0;
+ u16 pcifunc;
+ u64 cfg;
+
+ /* Read-only topology snapshot for switch software; any PF/VF may
+ * request it. Only channel and queue counts already visible to the
+ * requester through AF are reported.
+ */
+ rsp->cnt = 0;
+ rsp->truncated = 0;
+ memset(rsp->rsvd, 0, sizeof(rsp->rsvd));
+ /* Preserve mbox_msghdr fields pre-filled by the mbox framework. */
+ memset(rsp->info, 0, sizeof(rsp->info));
+ info = rsp->info;
+ for (pf = 0; pf < rvu->hw->total_pfs; pf++) {
+ if (tot >= IFACE_MAX) {
+ truncated = true;
+ goto done;
+ }
+
+ cfg = rvu_read64(rvu, BLKADDR_RVUM, RVU_PRIV_PFX_CFG(pf));
+ numvfs = (cfg >> 12) & 0xFF;
+
+ /* Skip not enabled PFs */
+ if (!(cfg & BIT_ULL(20)))
+ goto chk_vfs;
+
+ /* If Admin function, check on VFs */
+ if (cfg & BIT_ULL(21))
+ goto chk_vfs;
+
+ pcifunc = rvu_make_pcifunc(rvu->pdev, pf, 0);
+ pfvf = rvu_get_pfvf(rvu, pcifunc);
+
+ /* Populate iff at least one Tx channel */
+ if (!pfvf->tx_chan_cnt)
+ goto chk_vfs;
+
+ info->is_vf = 0;
+ info->pcifunc = pcifunc;
+ info->rx_chan_base = pfvf->rx_chan_base;
+ info->rx_chan_cnt = pfvf->rx_chan_cnt;
+ info->tx_chan_base = pfvf->tx_chan_base;
+ info->tx_chan_cnt = pfvf->tx_chan_cnt;
+ info->tx_link = nix_get_tx_link(rvu, pcifunc);
+ if (is_sdp_pfvf(rvu, pcifunc))
+ info->is_sdp = 1;
+
+ rvu_iface_get_qcnts(rvu, pfvf, info);
+
+ if (pfvf->nix_blkaddr == BLKADDR_NIX0)
+ info->nix = 0;
+ else
+ info->nix = 1;
+
+ info++;
+ tot++;
+
+chk_vfs:
+ for (vf = 0; vf < numvfs; vf++) {
+ if (tot >= IFACE_MAX) {
+ truncated = true;
+ goto done;
+ }
+
+ pcifunc = rvu_make_pcifunc(rvu->pdev, pf, vf + 1);
+ pfvf = rvu_get_pfvf(rvu, pcifunc);
+
+ if (!pfvf->tx_chan_cnt)
+ continue;
+
+ info->is_vf = 1;
+ info->pcifunc = pcifunc;
+ info->rx_chan_base = pfvf->rx_chan_base;
+ info->rx_chan_cnt = pfvf->rx_chan_cnt;
+ info->tx_chan_base = pfvf->tx_chan_base;
+ info->tx_chan_cnt = pfvf->tx_chan_cnt;
+ info->tx_link = nix_get_tx_link(rvu, pcifunc);
+ if (is_sdp_pfvf(rvu, pcifunc))
+ info->is_sdp = 1;
+
+ rvu_iface_get_qcnts(rvu, pfvf, info);
+
+ if (pfvf->nix_blkaddr == BLKADDR_NIX0)
+ info->nix = 0;
+ else
+ info->nix = 1;
+
+ info++;
+
+ tot++;
+ }
+ }
+done:
+ rsp->cnt = tot;
+ rsp->truncated = truncated;
+
+ return 0;
+}
+
int rvu_mbox_handler_free_rsrc_cnt(struct rvu *rvu, struct msg_req *req,
struct free_rsrcs_rsp *rsp)
{
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
index 094227404ef9..2876c76ae61b 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
@@ -1160,6 +1160,7 @@ void rvu_program_channels(struct rvu *rvu);
/* CN10K NIX */
void rvu_nix_block_cn10k_init(struct rvu *rvu, struct nix_hw *nix_hw);
+int nix_get_tx_link(struct rvu *rvu, u16 pcifunc);
/* CN10K RVU - LMT*/
void rvu_reset_lmt_map_tbl(struct rvu *rvu, u16 pcifunc);
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
index 153eb57bad06..b8f4ad160afc 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
@@ -32,7 +32,6 @@ static int nix_free_all_bandprof(struct rvu *rvu, u16 pcifunc);
static void nix_clear_ratelimit_aggr(struct rvu *rvu, struct nix_hw *nix_hw,
u32 leaf_prof);
static const char *nix_get_ctx_name(int ctype);
-static int nix_get_tx_link(struct rvu *rvu, u16 pcifunc);
enum mc_tbl_sz {
MC_TBL_SZ_256,
@@ -912,33 +911,78 @@ static void nix_setup_lso(struct rvu *rvu, struct nix_hw *nix_hw, int blkaddr)
nix_hw->lso.in_use++;
}
+static void nix_qctx_assign(struct rvu *rvu, int blkaddr, struct qmem **ctx,
+ unsigned long **bmap, struct qmem *new_ctx,
+ unsigned long *new_bmap)
+{
+ struct admin_queue *aq = rvu->hw->block[blkaddr].aq;
+ unsigned long flags;
+
+ if (!aq)
+ return;
+
+ spin_lock_irqsave(&aq->lock, flags);
+ *ctx = new_ctx;
+ *bmap = new_bmap;
+ spin_unlock_irqrestore(&aq->lock, flags);
+}
+
+static void nix_ctx_assign(struct rvu *rvu, struct qmem **ctx,
+ struct qmem *new_ctx)
+{
+ mutex_lock(&rvu->rsrc_lock);
+ *ctx = new_ctx;
+ mutex_unlock(&rvu->rsrc_lock);
+}
+
static void nix_ctx_free(struct rvu *rvu, struct rvu_pfvf *pfvf)
{
- kfree(pfvf->rq_bmap);
- kfree(pfvf->sq_bmap);
- kfree(pfvf->cq_bmap);
- if (pfvf->rq_ctx)
- qmem_free(rvu->dev, pfvf->rq_ctx);
- if (pfvf->sq_ctx)
- qmem_free(rvu->dev, pfvf->sq_ctx);
- if (pfvf->cq_ctx)
- qmem_free(rvu->dev, pfvf->cq_ctx);
- if (pfvf->rss_ctx)
- qmem_free(rvu->dev, pfvf->rss_ctx);
- if (pfvf->nix_qints_ctx)
- qmem_free(rvu->dev, pfvf->nix_qints_ctx);
- if (pfvf->cq_ints_ctx)
- qmem_free(rvu->dev, pfvf->cq_ints_ctx);
+ struct admin_queue *aq = rvu->hw->block[pfvf->nix_blkaddr].aq;
+ unsigned long *rq_bmap, *sq_bmap, *cq_bmap;
+ struct qmem *rq_ctx, *sq_ctx, *cq_ctx;
+ struct qmem *rss_ctx, *nix_qints_ctx, *cq_ints_ctx;
+ unsigned long flags;
+
+ if (!aq)
+ return;
+
+ spin_lock_irqsave(&aq->lock, flags);
+ rq_bmap = pfvf->rq_bmap;
+ sq_bmap = pfvf->sq_bmap;
+ cq_bmap = pfvf->cq_bmap;
+ rq_ctx = pfvf->rq_ctx;
+ sq_ctx = pfvf->sq_ctx;
+ cq_ctx = pfvf->cq_ctx;
+ rss_ctx = pfvf->rss_ctx;
+ nix_qints_ctx = pfvf->nix_qints_ctx;
+ cq_ints_ctx = pfvf->cq_ints_ctx;
pfvf->rq_bmap = NULL;
- pfvf->cq_bmap = NULL;
pfvf->sq_bmap = NULL;
+ pfvf->cq_bmap = NULL;
pfvf->rq_ctx = NULL;
pfvf->sq_ctx = NULL;
pfvf->cq_ctx = NULL;
pfvf->rss_ctx = NULL;
pfvf->nix_qints_ctx = NULL;
pfvf->cq_ints_ctx = NULL;
+ spin_unlock_irqrestore(&aq->lock, flags);
+
+ kfree(rq_bmap);
+ kfree(sq_bmap);
+ kfree(cq_bmap);
+ if (rq_ctx)
+ qmem_free(rvu->dev, rq_ctx);
+ if (sq_ctx)
+ qmem_free(rvu->dev, sq_ctx);
+ if (cq_ctx)
+ qmem_free(rvu->dev, cq_ctx);
+ if (rss_ctx)
+ qmem_free(rvu->dev, rss_ctx);
+ if (nix_qints_ctx)
+ qmem_free(rvu->dev, nix_qints_ctx);
+ if (cq_ints_ctx)
+ qmem_free(rvu->dev, cq_ints_ctx);
}
static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
@@ -946,6 +990,7 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
int rss_sz, int rss_grps, int hwctx_size,
u64 way_mask, bool tag_lsb_as_adder)
{
+ struct qmem *rss_ctx;
int err, grp, num_indices;
u64 val;
@@ -955,12 +1000,12 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
num_indices = rss_sz * rss_grps;
/* Alloc NIX RSS HW context memory and config the base */
- err = qmem_alloc(rvu->dev, &pfvf->rss_ctx, num_indices, hwctx_size);
+ err = qmem_alloc(rvu->dev, &rss_ctx, num_indices, hwctx_size);
if (err)
return err;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_RSS_BASE(nixlf),
- (u64)pfvf->rss_ctx->iova);
+ (u64)rss_ctx->iova);
/* Config full RSS table size, enable RSS and caching */
val = BIT_ULL(36) | BIT_ULL(4) | way_mask << 20 |
@@ -974,6 +1019,8 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
for (grp = 0; grp < rss_grps; grp++)
rvu_write64(rvu, blkaddr, NIX_AF_LFX_RSS_GRPX(nixlf, grp),
((ilog2(rss_sz) - 1) << 16) | (rss_sz * grp));
+
+ nix_ctx_assign(rvu, &pfvf->rss_ctx, rss_ctx);
return 0;
}
@@ -1514,6 +1561,9 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
struct nix_lf_alloc_rsp *rsp)
{
int nixlf, qints, hwctx_size, intf, rc = 0, pf;
+ unsigned long *rq_bmap, *sq_bmap, *cq_bmap;
+ struct qmem *cq_ints_ctx, *nix_qints_ctx;
+ struct qmem *rq_ctx, *sq_ctx, *cq_ctx;
u16 bcast, mcast, promisc, ucast;
struct rvu_hwinfo *hw = rvu->hw;
u16 pcifunc = req->hdr.pcifunc;
@@ -1584,59 +1634,68 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
/* Alloc NIX RQ HW context memory and config the base */
hwctx_size = 1UL << ((ctx_cfg >> 4) & 0xF);
- rc = qmem_alloc(rvu->dev, &pfvf->rq_ctx, req->rq_cnt, hwctx_size);
+ rc = qmem_alloc(rvu->dev, &rq_ctx, req->rq_cnt, hwctx_size);
if (rc)
goto free_mem;
- pfvf->rq_bmap = kcalloc(req->rq_cnt, sizeof(long), GFP_KERNEL);
- if (!pfvf->rq_bmap) {
+ rq_bmap = kcalloc(req->rq_cnt, sizeof(long), GFP_KERNEL);
+ if (!rq_bmap) {
+ qmem_free(rvu->dev, rq_ctx);
rc = -ENOMEM;
goto free_mem;
}
rvu_write64(rvu, blkaddr, NIX_AF_LFX_RQS_BASE(nixlf),
- (u64)pfvf->rq_ctx->iova);
+ (u64)rq_ctx->iova);
/* Set caching and queue count in HW */
cfg = BIT_ULL(36) | (req->rq_cnt - 1) | req->way_mask << 20;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_RQS_CFG(nixlf), cfg);
+ nix_qctx_assign(rvu, blkaddr, &pfvf->rq_ctx, &pfvf->rq_bmap, rq_ctx, rq_bmap);
+
/* Alloc NIX SQ HW context memory and config the base */
hwctx_size = 1UL << (ctx_cfg & 0xF);
- rc = qmem_alloc(rvu->dev, &pfvf->sq_ctx, req->sq_cnt, hwctx_size);
+ rc = qmem_alloc(rvu->dev, &sq_ctx, req->sq_cnt, hwctx_size);
if (rc)
goto free_mem;
- pfvf->sq_bmap = kcalloc(req->sq_cnt, sizeof(long), GFP_KERNEL);
- if (!pfvf->sq_bmap) {
+ sq_bmap = kcalloc(req->sq_cnt, sizeof(long), GFP_KERNEL);
+ if (!sq_bmap) {
+ qmem_free(rvu->dev, sq_ctx);
rc = -ENOMEM;
goto free_mem;
}
rvu_write64(rvu, blkaddr, NIX_AF_LFX_SQS_BASE(nixlf),
- (u64)pfvf->sq_ctx->iova);
+ (u64)sq_ctx->iova);
cfg = BIT_ULL(36) | (req->sq_cnt - 1) | req->way_mask << 20;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_SQS_CFG(nixlf), cfg);
+ nix_qctx_assign(rvu, blkaddr, &pfvf->sq_ctx, &pfvf->sq_bmap, sq_ctx, sq_bmap);
+
/* Alloc NIX CQ HW context memory and config the base */
hwctx_size = 1UL << ((ctx_cfg >> 8) & 0xF);
- rc = qmem_alloc(rvu->dev, &pfvf->cq_ctx, req->cq_cnt, hwctx_size);
+ rc = qmem_alloc(rvu->dev, &cq_ctx, req->cq_cnt, hwctx_size);
if (rc)
goto free_mem;
- pfvf->cq_bmap = kcalloc(req->cq_cnt, sizeof(long), GFP_KERNEL);
- if (!pfvf->cq_bmap) {
+ cq_bmap = kcalloc(req->cq_cnt, sizeof(long), GFP_KERNEL);
+ if (!cq_bmap) {
+ qmem_free(rvu->dev, cq_ctx);
rc = -ENOMEM;
goto free_mem;
}
rvu_write64(rvu, blkaddr, NIX_AF_LFX_CQS_BASE(nixlf),
- (u64)pfvf->cq_ctx->iova);
+ (u64)cq_ctx->iova);
cfg = BIT_ULL(36) | (req->cq_cnt - 1) | req->way_mask << 20;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_CQS_CFG(nixlf), cfg);
+ nix_qctx_assign(rvu, blkaddr, &pfvf->cq_ctx, &pfvf->cq_bmap, cq_ctx, cq_bmap);
+
/* Initialize receive side scaling (RSS) */
hwctx_size = 1UL << ((ctx_cfg >> 12) & 0xF);
rc = nixlf_rss_ctx_init(rvu, blkaddr, pfvf, nixlf, req->rss_sz,
@@ -1649,29 +1708,33 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
cfg = rvu_read64(rvu, blkaddr, NIX_AF_CONST2);
qints = (cfg >> 24) & 0xFFF;
hwctx_size = 1UL << ((ctx_cfg >> 24) & 0xF);
- rc = qmem_alloc(rvu->dev, &pfvf->cq_ints_ctx, qints, hwctx_size);
+ rc = qmem_alloc(rvu->dev, &cq_ints_ctx, qints, hwctx_size);
if (rc)
goto free_mem;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_CINTS_BASE(nixlf),
- (u64)pfvf->cq_ints_ctx->iova);
+ (u64)cq_ints_ctx->iova);
rvu_write64(rvu, blkaddr, NIX_AF_LFX_CINTS_CFG(nixlf),
BIT_ULL(36) | req->way_mask << 20);
+ nix_ctx_assign(rvu, &pfvf->cq_ints_ctx, cq_ints_ctx);
+
/* Alloc memory for QINT's HW contexts */
cfg = rvu_read64(rvu, blkaddr, NIX_AF_CONST2);
qints = (cfg >> 12) & 0xFFF;
hwctx_size = 1UL << ((ctx_cfg >> 20) & 0xF);
- rc = qmem_alloc(rvu->dev, &pfvf->nix_qints_ctx, qints, hwctx_size);
+ rc = qmem_alloc(rvu->dev, &nix_qints_ctx, qints, hwctx_size);
if (rc)
goto free_mem;
rvu_write64(rvu, blkaddr, NIX_AF_LFX_QINTS_BASE(nixlf),
- (u64)pfvf->nix_qints_ctx->iova);
+ (u64)nix_qints_ctx->iova);
rvu_write64(rvu, blkaddr, NIX_AF_LFX_QINTS_CFG(nixlf),
BIT_ULL(36) | req->way_mask << 20);
+ nix_ctx_assign(rvu, &pfvf->nix_qints_ctx, nix_qints_ctx);
+
/* Setup VLANX TPID's.
* Use VLAN1 for 802.1Q
* and VLAN0 for 802.1AD.
@@ -2109,10 +2172,10 @@ static void nix_clear_tx_xoff(struct rvu *rvu, int blkaddr,
rvu_write64(rvu, blkaddr, reg, 0x0);
}
-static int nix_get_tx_link(struct rvu *rvu, u16 pcifunc)
+int nix_get_tx_link(struct rvu *rvu, u16 pcifunc)
{
- struct rvu_hwinfo *hw = rvu->hw;
int pf = rvu_get_pf(rvu->pdev, pcifunc);
+ struct rvu_hwinfo *hw = rvu->hw;
u8 cgx_id = 0, lmac_id = 0;
if (is_lbk_vf(rvu, pcifunc)) {/* LBK links */
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
index 60922944675b..c115601b1212 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
@@ -3545,6 +3545,46 @@ int rvu_mbox_handler_npc_mcam_free_entry(struct rvu *rvu,
return rc;
}
+int rvu_mbox_handler_npc_flow_del_n_free(struct rvu *rvu,
+ struct npc_flow_del_n_free_req *mreq,
+ struct msg_rsp *rsp)
+{
+ struct npc_mcam_free_entry_req sreq = { 0 };
+ struct npc_delete_flow_req dreq = { 0 };
+ struct npc_delete_flow_rsp drsp = { 0 };
+ u16 entry[256];
+ int ret = 0, i;
+ bool err = false;
+ u16 cnt;
+
+ sreq.hdr.pcifunc = mreq->hdr.pcifunc;
+ dreq.hdr.pcifunc = mreq->hdr.pcifunc;
+
+ cnt = mreq->cnt;
+ if (!cnt || cnt > 256) {
+ dev_err_ratelimited(rvu->dev, "Invalid cnt=%u\n", cnt);
+ return -EINVAL;
+ }
+
+ /* Snapshot shared mailbox memory before processing the request. */
+ memcpy(entry, mreq->entry, cnt * sizeof(entry[0]));
+
+ for (i = 0; i < cnt; i++) {
+ dreq.entry = entry[i];
+ rvu_mbox_handler_npc_delete_flow(rvu, &dreq, &drsp);
+
+ sreq.entry = entry[i];
+ ret = rvu_mbox_handler_npc_mcam_free_entry(rvu, &sreq, rsp);
+ if (ret) {
+ dev_err(rvu->dev, "free entry error for i=%d entry=%d\n",
+ i, entry[i]);
+ err = true;
+ }
+ }
+
+ return err ? -EINVAL : 0;
+}
+
int rvu_mbox_handler_npc_mcam_read_entry(struct rvu *rvu,
struct npc_mcam_read_entry_req *req,
struct npc_mcam_read_entry_rsp *rsp)
@@ -4444,6 +4484,81 @@ int rvu_mbox_handler_npc_mcam_entry_stats(struct rvu *rvu,
return 0;
}
+int rvu_mbox_handler_npc_mcam_mul_stats(struct rvu *rvu,
+ struct npc_mcam_get_mul_stats_req *req,
+ struct npc_mcam_get_mul_stats_rsp *rsp)
+{
+ struct npc_mcam *mcam = &rvu->hw->mcam;
+ u16 req_cnt, index, cntr, mcam_entry;
+ u16 pcifunc = req->hdr.pcifunc;
+ int blkaddr, cnt = 0, i;
+ u16 entry[256];
+ u64 regval;
+ u32 bank;
+
+ rsp->cnt = 0;
+ memset(rsp->rsvd, 0, sizeof(rsp->rsvd));
+ memset(rsp->stat, 0, sizeof(rsp->stat));
+
+ req_cnt = req->cnt;
+ if (!req_cnt || req_cnt > 256) {
+ dev_err_ratelimited(rvu->dev, "%s invalid request cnt=%u\n",
+ __func__, req_cnt);
+ return -EINVAL;
+ }
+
+ /* Snapshot shared mailbox memory before processing the request. */
+ memcpy(entry, req->entry, req_cnt * sizeof(entry[0]));
+
+ blkaddr = rvu_get_blkaddr(rvu, BLKTYPE_NPC, 0);
+ if (blkaddr < 0)
+ return NPC_MCAM_INVALID_REQ;
+
+ mutex_lock(&mcam->lock);
+
+ for (i = 0; i < req_cnt; i++) {
+ mcam_entry = npc_cn20k_vidx2idx(entry[i]);
+
+ if (npc_mcam_verify_entry(mcam, pcifunc, mcam_entry)) {
+ mutex_unlock(&mcam->lock);
+ dev_err(rvu->dev, "%s invalid mcam index=%d\n",
+ __func__, entry[i]);
+ return -EINVAL;
+ }
+
+ index = mcam_entry & (mcam->banksize - 1);
+ bank = npc_get_bank(mcam, mcam_entry);
+
+ if (is_cn20k(rvu->pdev)) {
+ regval = rvu_read64(rvu, blkaddr,
+ NPC_AF_CN20K_MCAMEX_BANKX_STAT_EXT(index,
+ bank));
+ rsp->stat[cnt] = regval;
+ cnt++;
+ continue;
+ }
+
+ /* read MCAM entry STAT_ACT register */
+ regval = rvu_read64(rvu, blkaddr, NPC_AF_MCAMEX_BANKX_STAT_ACT(index, bank));
+
+ if (!(regval & rvu->hw->npc_stat_ena)) {
+ rsp->stat[cnt] = 0;
+ cnt++;
+ continue;
+ }
+
+ cntr = regval & 0x1FF;
+
+ rsp->stat[cnt] = rvu_read64(rvu, blkaddr, NPC_AF_MATCH_STATX(cntr));
+ rsp->stat[cnt] &= BIT_ULL(48) - 1;
+ cnt++;
+ }
+
+ rsp->cnt = cnt;
+ mutex_unlock(&mcam->lock);
+ return 0;
+}
+
void rvu_npc_clear_ucast_entry(struct rvu *rvu, int pcifunc, int nixlf)
{
struct npc_mcam *mcam = &rvu->hw->mcam;
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
index d422bdd5e8f8..e36c68ee5d84 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
@@ -1931,6 +1931,17 @@ static int npc_delete_flow(struct rvu *rvu, struct rvu_npc_mcam_rule *rule,
return rvu_mbox_handler_npc_mcam_dis_entry(rvu, &dis_req, &dis_rsp);
}
+int rvu_mbox_handler_npc_mcam_get_features(struct rvu *rvu,
+ struct msg_req *req,
+ struct npc_mcam_get_features_rsp *rsp)
+{
+ struct npc_mcam *mcam = &rvu->hw->mcam;
+
+ rsp->rx_features = mcam->rx_features;
+ rsp->tx_features = mcam->tx_features;
+ return 0;
+}
+
int rvu_mbox_handler_npc_delete_flow(struct rvu *rvu,
struct npc_delete_flow_req *req,
struct npc_delete_flow_rsp *rsp)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
new file mode 100644
index 000000000000..fe143ad3f944
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+
+#include "rvu.h"
+
+int rvu_mbox_handler_swdev2af_notify(struct rvu *rvu,
+ struct swdev2af_notify_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
new file mode 100644
index 000000000000..f28dba556d80
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
@@ -0,0 +1,11 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell RVU Admin Function driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+
+#ifndef RVU_SWITCH_H
+#define RVU_SWITCH_H
+
+#endif
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 3/8] octeontx2-pf: switch: Add pf files hierarchy
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 2/8] octeontx2-af: switch: Add switch dev to AF mboxes Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,3/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 4/8] octeontx2-af: switch: Representor for switch port Ratheesh Kannoth
` (4 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Adds CONFIG_OCTEONTX_SWITCH, links stub switch objects into the PF
module, and introduces empty sw_* init/deinit and notifier hooks for
later patches.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../net/ethernet/marvell/octeontx2/Kconfig | 10 +++++++++
.../ethernet/marvell/octeontx2/nic/Makefile | 5 ++++-
.../marvell/octeontx2/nic/switch/sw_fdb.c | 19 +++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fdb.h | 20 ++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fib.c | 20 ++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fib.h | 20 ++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fl.c | 18 ++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fl.h | 20 ++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_nb.c | 21 +++++++++++++++++++
.../marvell/octeontx2/nic/switch/sw_nb.h | 20 ++++++++++++++++++
10 files changed, 172 insertions(+), 1 deletion(-)
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
diff --git a/drivers/net/ethernet/marvell/octeontx2/Kconfig b/drivers/net/ethernet/marvell/octeontx2/Kconfig
index 47e549c581f0..e2fb6dd71078 100644
--- a/drivers/net/ethernet/marvell/octeontx2/Kconfig
+++ b/drivers/net/ethernet/marvell/octeontx2/Kconfig
@@ -28,6 +28,16 @@ config NDC_DIS_DYNAMIC_CACHING
, NPA stack pages etc in NDC. Also locks down NIX SQ/CQ/RQ/RSS and
NPA Aura/Pool contexts.
+config OCTEONTX_SWITCH
+ bool "Marvell OcteonTX2 switch driver"
+ depends on (64BIT && COMPILE_TEST) || ARM64
+ depends on OCTEONTX2_PF
+ default n
+ help
+ This driver supports Marvell's OcteonTX2 switch.
+ Marvell SWITCH HW can offload L2, L3 flow. ARM core interacts
+ with Marvell SW HW thru mbox.
+
config OCTEONTX2_PF
tristate "Marvell OcteonTX2 NIC Physical Function driver"
select OCTEONTX2_MBOX
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/Makefile b/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
index 883e9f4d601c..27590b94133b 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
@@ -9,7 +9,10 @@ obj-$(CONFIG_RVU_ESWITCH) += rvu_rep.o
rvu_nicpf-y := otx2_pf.o otx2_common.o otx2_txrx.o otx2_ethtool.o \
otx2_flows.o otx2_tc.o cn10k.o cn20k.o otx2_dmac_flt.o \
- otx2_devlink.o qos_sq.o qos.o otx2_xsk.o
+ otx2_devlink.o qos_sq.o qos.o otx2_xsk.o switch/sw_fdb.o \
+ switch/sw_fl.o
+rvu_nicpf-$(CONFIG_OCTEONTX_SWITCH) += switch/sw_nb.o switch/sw_fib.o
+
rvu_nicvf-y := otx2_vf.o
rvu_rep-y := rep.o
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
new file mode 100644
index 000000000000..500451e85b50
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "sw_fdb.h"
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+int sw_fdb_init(void)
+{
+ return 0;
+}
+
+void sw_fdb_deinit(void)
+{
+}
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
new file mode 100644
index 000000000000..dc427e8ab7c6
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_FDB_H_
+#define SW_FDB_H_
+
+#include <linux/kconfig.h>
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+void sw_fdb_deinit(void);
+int sw_fdb_init(void);
+#else
+static inline void sw_fdb_deinit(void) {}
+static inline int sw_fdb_init(void) { return 0; }
+#endif
+
+#endif /* SW_FDB_H_ */
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
new file mode 100644
index 000000000000..f4c47111d763
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "sw_fib.h"
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+
+int otx2_sw_fib_init(void)
+{
+ return 0;
+}
+
+void otx2_sw_fib_deinit(void)
+{
+}
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
new file mode 100644
index 000000000000..448d5612133e
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_FIB_H_
+#define SW_FIB_H_
+
+#include <linux/kconfig.h>
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+void otx2_sw_fib_deinit(void);
+int otx2_sw_fib_init(void);
+#else
+static inline void otx2_sw_fib_deinit(void) {}
+static inline int otx2_sw_fib_init(void) { return 0; }
+#endif
+
+#endif /* SW_FIB_H_ */
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.c
new file mode 100644
index 000000000000..f2811d69f815
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.c
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "sw_fl.h"
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+int sw_fl_init(void)
+{
+ return 0;
+}
+
+void sw_fl_deinit(void)
+{
+}
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.h
new file mode 100644
index 000000000000..7648df59e215
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fl.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_FL_H_
+#define SW_FL_H_
+
+#include <linux/kconfig.h>
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+void sw_fl_deinit(void);
+int sw_fl_init(void);
+#else
+static inline void sw_fl_deinit(void) {}
+static inline int sw_fl_init(void) { return 0; }
+#endif
+
+#endif /* SW_FL_H_ */
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
new file mode 100644
index 000000000000..426a42011930
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include "sw_nb.h"
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+
+int otx2_sw_nb_unregister(void)
+{
+ return 0;
+}
+
+int otx2_sw_nb_register(void)
+{
+ return 0;
+}
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
new file mode 100644
index 000000000000..0ba29f76fd41
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_NB_H_
+#define SW_NB_H_
+
+#include <linux/kconfig.h>
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+int otx2_sw_nb_register(void);
+int otx2_sw_nb_unregister(void);
+#else
+static inline int otx2_sw_nb_register(void) { return 0; }
+static inline int otx2_sw_nb_unregister(void) { return 0; }
+#endif
+
+#endif /* SW_NB_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 4/8] octeontx2-af: switch: Representor for switch port
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
` (2 preceding siblings ...)
2026-08-31 13:19 ` [PATCH v9 net-next 3/8] octeontx2-pf: switch: Add pf files hierarchy Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,4/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control Ratheesh Kannoth
` (3 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Extends esw_cfg with a devlink-derived switch id, copies it into
rvu->rswitch on the AF, adds rvu_sw_port_id(), exports
rvu_rep_get_vlan_id().
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../net/ethernet/marvell/octeontx2/af/mbox.h | 1 +
.../net/ethernet/marvell/octeontx2/af/rvu.h | 5 +++
.../ethernet/marvell/octeontx2/af/rvu_rep.c | 33 ++++++++++++++++++-
.../marvell/octeontx2/af/switch/rvu_sw.c | 26 +++++++++++++++
.../marvell/octeontx2/af/switch/rvu_sw.h | 5 +++
.../net/ethernet/marvell/octeontx2/nic/rep.c | 4 +++
6 files changed, 73 insertions(+), 1 deletion(-)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index e45e6e93ed08..8e3850f33751 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -1841,6 +1841,7 @@ struct esw_cfg_req {
struct mbox_msghdr hdr;
u8 ena;
u64 rsvd;
+ unsigned char switch_id[MAX_PHYS_ITEM_ID_LEN];
};
struct rep_evt_data {
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
index 2876c76ae61b..9174b879850a 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
@@ -576,6 +576,10 @@ struct rvu_switch {
u16 *entry2pcifunc;
u16 mode;
u16 start_entry;
+ unsigned char switch_id[MAX_PHYS_ITEM_ID_LEN];
+#define RVU_SWITCH_FLAG_FW_READY BIT_ULL(0)
+ u64 flags;
+ u16 pcifunc;
};
struct rep_evtq_ent {
@@ -1199,4 +1203,5 @@ int rvu_rep_install_mcam_rules(struct rvu *rvu);
void rvu_rep_update_rules(struct rvu *rvu, u16 pcifunc, bool ena);
int rvu_rep_notify_pfvf_state(struct rvu *rvu, u16 pcifunc, bool enable);
int npc_mcam_verify_entry(struct npc_mcam *mcam, u16 pcifunc, int entry);
+u16 rvu_rep_get_vlan_id(struct rvu *rvu, u16 pcifunc);
#endif /* RVU_H */
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
index a2781e0f504e..672d54847c7b 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
@@ -6,6 +6,7 @@
*/
#include <linux/bitfield.h>
+#include <linux/stddef.h>
#include <linux/types.h>
#include <linux/device.h>
#include <linux/module.h>
@@ -189,7 +190,7 @@ int rvu_mbox_handler_nix_lf_stats(struct rvu *rvu,
return 0;
}
-static u16 rvu_rep_get_vlan_id(struct rvu *rvu, u16 pcifunc)
+u16 rvu_rep_get_vlan_id(struct rvu *rvu, u16 pcifunc)
{
int id;
@@ -429,6 +430,30 @@ int rvu_rep_pf_init(struct rvu *rvu)
return 0;
}
+/* ESW_CFG is always the sole message in a mailbox transaction.
+ *
+ * The otx2 mailbox API does not batch multiple messages per sync: the
+ * representor driver allocates only ESW_CFG before calling
+ * otx2_sync_mbox_msg() (see rvu_eswitch_config()), and the AF processes
+ * one message per dispatch. next_msgoff is therefore the end offset of this
+ * message, not a cumulative offset across batched messages, so the length
+ * check below is safe. Batching is not supported; do not flag this path.
+ */
+static bool esw_cfg_req_has_switch_id(const struct esw_cfg_req *req)
+{
+ u16 hdr_len = ALIGN(sizeof(struct mbox_hdr), MBOX_MSG_ALIGN);
+ u16 next_off = req->hdr.next_msgoff;
+ u16 msg_len;
+
+ if (next_off < hdr_len)
+ return false;
+
+ msg_len = next_off - hdr_len;
+
+ return msg_len >= offsetof(struct esw_cfg_req, switch_id) +
+ MAX_PHYS_ITEM_ID_LEN;
+}
+
int rvu_mbox_handler_esw_cfg(struct rvu *rvu, struct esw_cfg_req *req,
struct msg_rsp *rsp)
{
@@ -436,6 +461,9 @@ int rvu_mbox_handler_esw_cfg(struct rvu *rvu, struct esw_cfg_req *req,
return 0;
rvu->rep_mode = req->ena;
+ if (esw_cfg_req_has_switch_id(req))
+ memcpy(rvu->rswitch.switch_id, req->switch_id,
+ MAX_PHYS_ITEM_ID_LEN);
if (!rvu->rep_mode)
rvu_npc_free_mcam_entries(rvu, req->hdr.pcifunc, -1);
@@ -449,6 +477,9 @@ int rvu_mbox_handler_get_rep_cnt(struct rvu *rvu, struct msg_req *req,
int pf, vf, numvfs, hwvf, rep = 0;
u16 pcifunc;
+ /* Called once from representor driver probe during devlink eswitch
+ * SWITCHDEV bring-up; not re-run during switch device operation.
+ */
rvu->rep_pcifunc = req->hdr.pcifunc;
rsp->rep_cnt = rvu->cgx_mapped_pfs + rvu->cgx_mapped_vfs;
rvu->rep_cnt = rsp->rep_cnt;
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
index fe143ad3f944..2451eb57ec4c 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
@@ -5,7 +5,33 @@
*
*/
+#include <linux/bitfield.h>
+
#include "rvu.h"
+#include "rvu_sw.h"
+
+/*
+ * rep_cnt and rep2pfvf_map are populated once when the representor driver
+ * probes via GET_REP_CNT (see rvu_get_rep_cnt() in rep.c), as part of
+ * devlink eswitch SWITCHDEV bring-up. They are not updated during switch
+ * device mailbox handling, so this lockless lookup cannot race with a
+ * concurrent rep2pfvf_map resize.
+ */
+u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc)
+{
+ u16 rep_id;
+
+ if (!rvu->rep2pfvf_map || !rvu->rep_cnt)
+ return RVU_SW_INVALID_PORT_ID;
+
+ rep_id = rvu_rep_get_vlan_id(rvu, pcifunc);
+ if (rep_id >= rvu->rep_cnt ||
+ rvu->rep2pfvf_map[rep_id] != pcifunc)
+ return RVU_SW_INVALID_PORT_ID;
+
+ return FIELD_PREP(GENMASK_ULL(31, 16), rep_id) |
+ FIELD_PREP(GENMASK_ULL(15, 0), pcifunc);
+}
int rvu_mbox_handler_swdev2af_notify(struct rvu *rvu,
struct swdev2af_notify_req *req,
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
index f28dba556d80..e9ad32c84576 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
@@ -8,4 +8,9 @@
#ifndef RVU_SWITCH_H
#define RVU_SWITCH_H
+/* RVU Switch */
+#define RVU_SW_INVALID_PORT_ID ((u32)~0U)
+
+u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc);
+
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
index 0f5d5642d3f7..257a2ae6a53e 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
@@ -399,8 +399,11 @@ static void rvu_rep_get_stats64(struct net_device *dev,
static int rvu_eswitch_config(struct otx2_nic *priv, u8 ena)
{
+ struct devlink_port_attrs attrs = {};
struct esw_cfg_req *req;
+ rvu_rep_devlink_set_switch_id(priv, &attrs.switch_id);
+
mutex_lock(&priv->mbox.lock);
req = otx2_mbox_alloc_msg_esw_cfg(&priv->mbox);
if (!req) {
@@ -408,6 +411,7 @@ static int rvu_eswitch_config(struct otx2_nic *priv, u8 ena)
return -ENOMEM;
}
req->ena = ena;
+ memcpy(req->switch_id, attrs.switch_id.id, attrs.switch_id.id_len);
otx2_sync_mbox_msg(&priv->mbox);
mutex_unlock(&priv->mbox.lock);
return 0;
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
` (3 preceding siblings ...)
2026-08-31 13:19 ` [PATCH v9 net-next 4/8] octeontx2-af: switch: Representor for switch port Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,5/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 6/8] octeontx2-pf: switch: Register notifiers for switch offload Ratheesh Kannoth
` (2 subsequent siblings)
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Switch (PAN) mode needs more than one TL1 scheduler queue index so the
hardware can steer traffic to different links according to NPC flow
rules, not only the PF/VF default Tx link.
Add NIX_TXSCH_ALLOC_FLAG_PAN to nix_txsch_alloc requests: use the PAN
link index for scheduler range calculation, allow multiple TL1 queues
when the aggregate level spans start..end, and allocate indices in
that range. Add TXSCHQ_FREE_PAN_TL1 so TL1 entries in that path can be
freed via nix_txsch_free where they were previously skipped.
For NPC install flow, add set_chanmask so callers can keep a non-default
chan_mask when the requester is not the AF; without it, chan_mask was
always forced to 0xFFF for non-AF functions.
Allocate the NIX LF SQ bitmap with the same span used by
bitmap_weight(..., BITS_PER_LONG * 16) in rvu_get_hwinfo().
Extend struct sg_list with cq_idx and len for transmit-side metadata.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../net/ethernet/marvell/octeontx2/af/mbox.h | 15 ++
.../net/ethernet/marvell/octeontx2/af/rvu.c | 24 ++-
.../net/ethernet/marvell/octeontx2/af/rvu.h | 6 +
.../ethernet/marvell/octeontx2/af/rvu_nix.c | 180 ++++++++++++++++--
.../marvell/octeontx2/af/rvu_npc_fs.c | 20 +-
.../marvell/octeontx2/nic/otx2_txrx.h | 2 +
6 files changed, 216 insertions(+), 31 deletions(-)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index 8e3850f33751..2aa1aa6599a5 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -1162,6 +1162,13 @@ struct nix_txsch_alloc_req {
/* Scheduler queue count request at each level */
u16 schq_contig[NIX_TXSCH_LVL_CNT]; /* No of contiguous queues */
u16 schq[NIX_TXSCH_LVL_CNT]; /* No of non-contiguous queues */
+ /* Set only by the single switchdev PF (rvu->rswitch.pcifunc). This is
+ * not the eswitch representor (rvu->rep_pcifunc). That PF requests two
+ * aggregate-level TL2 queues on the PAN link, one for CGX and one for
+ * SDP steering. No other PF or VF sets this flag.
+ */
+#define NIX_TXSCH_ALLOC_FLAG_PAN BIT(0)
+ u32 flags;
};
struct nix_txsch_alloc_rsp {
@@ -1180,6 +1187,10 @@ struct nix_txsch_alloc_rsp {
struct nix_txsch_free_req {
struct mbox_msghdr hdr;
#define TXSCHQ_FREE_ALL BIT_ULL(0)
+ /* Frees PAN TL2 queues allocated with NIX_TXSCH_ALLOC_FLAG_PAN. Used
+ * only by the switchdev PF (rvu->rswitch.pcifunc), not by other PFs/VFs.
+ */
+#define TXSCHQ_FREE_PAN_TL1 BIT_ULL(1)
u16 flags;
/* Scheduler queue level to be freed */
u16 schq_lvl;
@@ -2135,6 +2146,10 @@ struct npc_install_flow_req {
u8 hw_prio;
u8 req_kw_type; /* Key type to be written */
u8 alloc_entry; /* only for cn20k */
+ /* When set, keep caller chan_mask instead of the CPT default. Only
+ * honored for the switchdev PF; see rvu_mbox_handler_npc_install_flow().
+ */
+ u8 set_chanmask;
/* For now use any priority, once AF driver is changed to
* allocate least priority entry instead of mid zone then make
* NPC_MCAM_LEAST_PRIO as 3
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
index 1402beccf661..e4d13adc2896 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
@@ -1990,11 +1990,12 @@ int rvu_mbox_handler_msix_offset(struct rvu *rvu, struct msg_req *req,
return 0;
}
-static void rvu_iface_get_qcnts(struct rvu *rvu, struct rvu_pfvf *pfvf,
- struct iface_info *info)
+static void rvu_iface_get_qcnts(struct rvu *rvu, u16 pcifunc,
+ struct rvu_pfvf *pfvf, struct iface_info *info)
{
struct admin_queue *aq;
unsigned long flags;
+ int sq_bmap_bits;
info->sq_cnt = 0;
info->cq_cnt = 0;
@@ -2006,9 +2007,18 @@ static void rvu_iface_get_qcnts(struct rvu *rvu, struct rvu_pfvf *pfvf,
spin_lock_irqsave(&aq->lock, flags);
- /* Use each LF queue context size; bitmaps are sized to qsize longs. */
- if (pfvf->sq_ctx && pfvf->sq_bmap)
- info->sq_cnt = bitmap_weight(pfvf->sq_bmap, pfvf->sq_ctx->qsize);
+ if (pfvf->sq_bmap) {
+ /* Match switchdev sq_bmap allocation size in nix_lf_alloc(). */
+ if (rvu_is_switch_pcifunc(rvu, pcifunc))
+ sq_bmap_bits = NIX_SQ_BMAP_BITS;
+ else if (pfvf->sq_ctx)
+ sq_bmap_bits = pfvf->sq_ctx->qsize;
+ else
+ sq_bmap_bits = 0;
+
+ if (sq_bmap_bits)
+ info->sq_cnt = bitmap_weight(pfvf->sq_bmap, sq_bmap_bits);
+ }
if (pfvf->cq_ctx && pfvf->cq_bmap)
info->cq_cnt = bitmap_weight(pfvf->cq_bmap, pfvf->cq_ctx->qsize);
if (pfvf->rq_ctx && pfvf->rq_bmap)
@@ -2072,7 +2082,7 @@ int rvu_mbox_handler_iface_get_info(struct rvu *rvu, struct msg_req *req,
if (is_sdp_pfvf(rvu, pcifunc))
info->is_sdp = 1;
- rvu_iface_get_qcnts(rvu, pfvf, info);
+ rvu_iface_get_qcnts(rvu, pcifunc, pfvf, info);
if (pfvf->nix_blkaddr == BLKADDR_NIX0)
info->nix = 0;
@@ -2105,7 +2115,7 @@ int rvu_mbox_handler_iface_get_info(struct rvu *rvu, struct msg_req *req,
if (is_sdp_pfvf(rvu, pcifunc))
info->is_sdp = 1;
- rvu_iface_get_qcnts(rvu, pfvf, info);
+ rvu_iface_get_qcnts(rvu, pcifunc, pfvf, info);
if (pfvf->nix_blkaddr == BLKADDR_NIX0)
info->nix = 0;
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
index 9174b879850a..0b0ba1350922 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
@@ -335,6 +335,7 @@ struct nix_txsch {
u8 lvl;
#define NIX_TXSCHQ_FREE BIT_ULL(1)
#define NIX_TXSCHQ_CFG_DONE BIT_ULL(0)
+#define NIX_SQ_BMAP_BITS (BITS_PER_LONG * 16)
#define TXSCH_MAP_FUNC(__pfvf_map) ((__pfvf_map) & 0xFFFF)
#define TXSCH_MAP_FLAGS(__pfvf_map) ((__pfvf_map) >> 16)
#define TXSCH_MAP(__func, __flags) (((__func) & 0xFFFF) | ((__flags) << 16))
@@ -904,6 +905,11 @@ static inline bool is_pffunc_af(u16 pcifunc)
return !pcifunc;
}
+static inline bool rvu_is_switch_pcifunc(struct rvu *rvu, u16 pcifunc)
+{
+ return rvu->rswitch.pcifunc && pcifunc == rvu->rswitch.pcifunc;
+}
+
static inline bool is_rvu_fwdata_valid(struct rvu *rvu)
{
return (rvu->fwdata->header_magic == RVU_FWDATA_HEADER_MAGIC) &&
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
index b8f4ad160afc..9ee6531afbf9 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
@@ -1104,6 +1104,7 @@ static int rvu_nix_blk_aq_enq_inst(struct rvu *rvu, struct nix_hw *nix_hw,
u16 pcifunc = req->hdr.pcifunc;
int nixlf, blkaddr, rc = 0;
struct nix_aq_inst_s inst;
+ u64 sq_bmap_bits, max_q;
struct rvu_block *block;
struct admin_queue *aq;
struct rvu_pfvf *pfvf;
@@ -1138,10 +1139,25 @@ static int rvu_nix_blk_aq_enq_inst(struct rvu *rvu, struct nix_hw *nix_hw,
if (!pfvf->rq_ctx || req->qidx >= pfvf->rq_ctx->qsize)
rc = NIX_AF_ERR_AQ_ENQUEUE;
break;
- case NIX_AQ_CTYPE_SQ:
- if (!pfvf->sq_ctx || req->qidx >= pfvf->sq_ctx->qsize)
+ case NIX_AQ_CTYPE_SQ: {
+ if (!pfvf->sq_ctx) {
+ rc = NIX_AF_ERR_AQ_ENQUEUE;
+ break;
+ }
+
+ /* Switchdev PF uses a fixed sq_bmap (NIX_SQ_BMAP_BITS); cap qidx
+ * to that span so __set_bit() cannot run past the allocation.
+ * nix_lf_alloc() also rejects sq_cnt above NIX_SQ_BMAP_BITS.
+ */
+ sq_bmap_bits = rvu_is_switch_pcifunc(rvu, pcifunc) ?
+ NIX_SQ_BMAP_BITS :
+ (u64)pfvf->sq_ctx->qsize * BITS_PER_LONG;
+ max_q = min_t(u64, pfvf->sq_ctx->qsize, sq_bmap_bits);
+
+ if ((u64)req->qidx >= max_q)
rc = NIX_AF_ERR_AQ_ENQUEUE;
break;
+ }
case NIX_AQ_CTYPE_CQ:
if (!pfvf->cq_ctx || req->qidx >= pfvf->cq_ctx->qsize)
rc = NIX_AF_ERR_AQ_ENQUEUE;
@@ -1566,18 +1582,28 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
struct qmem *rq_ctx, *sq_ctx, *cq_ctx;
u16 bcast, mcast, promisc, ucast;
struct rvu_hwinfo *hw = rvu->hw;
+ u64 cfg, ctx_cfg, sq_bmap_bits;
u16 pcifunc = req->hdr.pcifunc;
u8 cgx_id = 0, lmac_id = 0;
bool rules_created = false;
struct rvu_block *block;
struct rvu_pfvf *pfvf;
struct cgx *cgxd;
- u64 cfg, ctx_cfg;
int blkaddr;
if (!req->rq_cnt || !req->sq_cnt || !req->cq_cnt)
return NIX_AF_ERR_PARAM;
+ /* Switchdev PF sq_bmap is fixed at NIX_SQ_BMAP_BITS; reject larger
+ * sq_cnt before allocating context memory or the bitmap.
+ */
+ sq_bmap_bits = rvu_is_switch_pcifunc(rvu, pcifunc) ?
+ NIX_SQ_BMAP_BITS :
+ (u64)req->sq_cnt * BITS_PER_LONG;
+
+ if ((u64)req->sq_cnt > sq_bmap_bits)
+ return NIX_AF_ERR_PARAM;
+
if (req->way_mask)
req->way_mask &= 0xFFFF;
@@ -1660,7 +1686,12 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
if (rc)
goto free_mem;
- sq_bmap = kcalloc(req->sq_cnt, sizeof(long), GFP_KERNEL);
+ if (rvu_is_switch_pcifunc(rvu, pcifunc))
+ /* Fixed-size bitmap; sq_cnt capped to NIX_SQ_BMAP_BITS above. */
+ sq_bmap = kcalloc(BITS_TO_LONGS(NIX_SQ_BMAP_BITS),
+ sizeof(long), GFP_KERNEL);
+ else
+ sq_bmap = kcalloc(req->sq_cnt, sizeof(long), GFP_KERNEL);
if (!sq_bmap) {
qmem_free(rvu->dev, sq_ctx);
rc = -ENOMEM;
@@ -2209,6 +2240,25 @@ static void nix_get_txschq_range(struct rvu *rvu, u16 pcifunc,
}
}
+static int nix_get_pan_tx_link(struct rvu *rvu)
+{
+ struct rvu_hwinfo *hw = rvu->hw;
+
+ return hw->cgx_links + hw->lbk_links + 1;
+}
+
+static bool nix_txsch_is_pan_schq(struct rvu *rvu, int schq)
+{
+ int pan_link = nix_get_pan_tx_link(rvu);
+
+ return schq >= pan_link && schq <= pan_link + 1;
+}
+
+static bool nix_txsch_pan_allowed(struct rvu *rvu, u16 pcifunc)
+{
+ return rvu_is_switch_pcifunc(rvu, pcifunc);
+}
+
static int nix_check_txschq_alloc_req(struct rvu *rvu, int lvl, u16 pcifunc,
struct nix_hw *nix_hw,
struct nix_txsch_alloc_req *req)
@@ -2224,12 +2274,27 @@ static int nix_check_txschq_alloc_req(struct rvu *rvu, int lvl, u16 pcifunc,
if (!req_schq)
return 0;
- link = nix_get_tx_link(rvu, pcifunc);
+ if (req->flags & NIX_TXSCH_ALLOC_FLAG_PAN) {
+ if (!nix_txsch_pan_allowed(rvu, pcifunc))
+ return NIX_AF_ERR_TLX_ALLOC_FAIL;
+ link = nix_get_pan_tx_link(rvu);
+ } else {
+ link = nix_get_tx_link(rvu, pcifunc);
+ }
/* For traffic aggregating scheduler level, one queue is enough */
if (lvl >= hw->cap.nix_tx_aggr_lvl) {
- if (req_schq != 1)
+ if (req_schq != 1 && !(req->flags & NIX_TXSCH_ALLOC_FLAG_PAN))
return NIX_AF_ERR_TLX_ALLOC_FAIL;
+ if (req->schq[lvl] > MAX_TXSCHQ_PER_FUNC ||
+ req->schq_contig[lvl] > MAX_TXSCHQ_PER_FUNC)
+ return NIX_AF_ERR_TLX_ALLOC_FAIL;
+ if (req->flags & NIX_TXSCH_ALLOC_FLAG_PAN) {
+ if (link >= txsch->schq.max || link + 1 >= txsch->schq.max)
+ return NIX_AF_ERR_TLX_ALLOC_FAIL;
+ if (req_schq > 2)
+ return NIX_AF_ERR_TLX_ALLOC_FAIL;
+ }
return 0;
}
@@ -2258,9 +2323,9 @@ static int nix_check_txschq_alloc_req(struct rvu *rvu, int lvl, u16 pcifunc,
return 0;
}
-static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
- struct nix_txsch_alloc_rsp *rsp,
- int lvl, int start, int end)
+static int nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
+ struct nix_txsch_alloc_rsp *rsp,
+ int lvl, int start, int end)
{
struct rvu_hwinfo *hw = rvu->hw;
u16 pcifunc = rsp->hdr.pcifunc;
@@ -2270,6 +2335,46 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
* on transmit link to which PF_FUNC is mapped to.
*/
if (lvl >= hw->cap.nix_tx_aggr_lvl) {
+ if (start != end) {
+ int want_contig = rsp->schq_contig[lvl];
+ int got_contig = 0, got = 0;
+ int want = rsp->schq[lvl];
+
+ for (schq = start; schq <= end; schq++) {
+ if (test_bit(schq, txsch->schq.bmap))
+ continue;
+
+ if (got_contig < want_contig) {
+ set_bit(schq, txsch->schq.bmap);
+ rsp->schq_contig_list[lvl][got_contig++] = schq;
+ continue;
+ }
+
+ if (got < want) {
+ set_bit(schq, txsch->schq.bmap);
+ rsp->schq_list[lvl][got++] = schq;
+ }
+ }
+
+ rsp->schq_contig[lvl] = got_contig;
+ rsp->schq[lvl] = got;
+
+ if (got_contig < want_contig || got < want) {
+ for (idx = 0; idx < got_contig; idx++)
+ clear_bit(rsp->schq_contig_list[lvl][idx],
+ txsch->schq.bmap);
+ for (idx = 0; idx < got; idx++)
+ clear_bit(rsp->schq_list[lvl][idx],
+ txsch->schq.bmap);
+ rsp->schq_contig[lvl] = 0;
+ rsp->schq[lvl] = 0;
+ dev_err(rvu->dev,
+ "Could not allocate schq at lvl=%u start=%u end=%u\n",
+ lvl, start, end);
+ return -ENOMEM;
+ }
+ return 0;
+ }
/* A single TL queue is allocated */
if (rsp->schq_contig[lvl]) {
rsp->schq_contig[lvl] = 1;
@@ -2284,7 +2389,7 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
rsp->schq[lvl] = 1;
rsp->schq_list[lvl][0] = start;
}
- return;
+ return 0;
}
/* Adjust the queue request count if HW supports
@@ -2296,7 +2401,7 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
if (idx >= (end - start) || test_bit(schq, txsch->schq.bmap)) {
rsp->schq_contig[lvl] = 0;
rsp->schq[lvl] = 0;
- return;
+ return 0;
}
if (rsp->schq_contig[lvl]) {
@@ -2309,7 +2414,7 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
set_bit(schq, txsch->schq.bmap);
rsp->schq_list[lvl][0] = schq;
}
- return;
+ return 0;
}
/* Allocate contiguous queue indices requesty first */
@@ -2340,6 +2445,8 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
/* Update how many were allocated */
rsp->schq[lvl] = idx;
}
+
+ return 0;
}
int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
@@ -2364,6 +2471,10 @@ int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
if (!nix_hw)
return NIX_AF_ERR_INVALID_NIXBLK;
+ if ((req->flags & NIX_TXSCH_ALLOC_FLAG_PAN) &&
+ !nix_txsch_pan_allowed(rvu, pcifunc))
+ return NIX_AF_ERR_TLX_ALLOC_FAIL;
+
mutex_lock(&rvu->rsrc_lock);
/* Check if request is valid as per HW capabilities
@@ -2386,11 +2497,14 @@ int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
rsp->schq[lvl] = req->schq[lvl];
rsp->schq_contig[lvl] = req->schq_contig[lvl];
- link = nix_get_tx_link(rvu, pcifunc);
+ if (req->flags & NIX_TXSCH_ALLOC_FLAG_PAN)
+ link = nix_get_pan_tx_link(rvu);
+ else
+ link = nix_get_tx_link(rvu, pcifunc);
if (lvl >= hw->cap.nix_tx_aggr_lvl) {
start = link;
- end = link;
+ end = link + !!(req->flags & NIX_TXSCH_ALLOC_FLAG_PAN);
} else if (hw->cap.nix_fixed_txschq_mapping) {
nix_get_txschq_range(rvu, pcifunc, link, &start, &end);
} else {
@@ -2398,10 +2512,11 @@ int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
end = txsch->schq.max;
}
- nix_txsch_alloc(rvu, txsch, rsp, lvl, start, end);
+ if (nix_txsch_alloc(rvu, txsch, rsp, lvl, start, end))
+ goto err;
/* Reset queue config */
- for (idx = 0; idx < req->schq_contig[lvl]; idx++) {
+ for (idx = 0; idx < rsp->schq_contig[lvl]; idx++) {
schq = rsp->schq_contig_list[lvl][idx];
if (!(TXSCH_MAP_FLAGS(pfvf_map[schq]) &
NIX_TXSCHQ_CFG_DONE))
@@ -2411,7 +2526,7 @@ int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
nix_reset_tx_schedule(rvu, blkaddr, lvl, schq);
}
- for (idx = 0; idx < req->schq[lvl]; idx++) {
+ for (idx = 0; idx < rsp->schq[lvl]; idx++) {
schq = rsp->schq_list[lvl][idx];
if (!(TXSCH_MAP_FLAGS(pfvf_map[schq]) &
NIX_TXSCHQ_CFG_DONE))
@@ -2679,6 +2794,20 @@ static int nix_txschq_free(struct rvu *rvu, u16 pcifunc)
}
nix_clear_tx_xoff(rvu, blkaddr, NIX_TXSCH_LVL_TL1,
nix_get_tx_link(rvu, pcifunc));
+ /* TL1 is at nix_tx_aggr_lvl so the loop above skips it; also clear
+ * PAN TL1 XOFF on switch-owned links before flushing SMQs.
+ */
+ if (nix_txsch_pan_allowed(rvu, pcifunc)) {
+ txsch = &nix_hw->txsch[NIX_TXSCH_LVL_TL1];
+
+ for (schq = nix_get_pan_tx_link(rvu);
+ schq < txsch->schq.max &&
+ nix_txsch_is_pan_schq(rvu, schq); schq++) {
+ if (TXSCH_MAP_FUNC(txsch->pfvf_map[schq]) != pcifunc)
+ continue;
+ nix_clear_tx_xoff(rvu, blkaddr, NIX_TXSCH_LVL_TL1, schq);
+ }
+ }
/* On PF cleanup, clear cfg done flag as
* PF would have changed default config.
@@ -2706,11 +2835,11 @@ static int nix_txschq_free(struct rvu *rvu, u16 pcifunc)
/* TLs above aggregation level are shared across all PF
* and it's VFs, hence skip freeing them.
*/
- if (lvl >= hw->cap.nix_tx_aggr_lvl)
- continue;
-
txsch = &nix_hw->txsch[lvl];
for (schq = 0; schq < txsch->schq.max; schq++) {
+ if (lvl >= hw->cap.nix_tx_aggr_lvl &&
+ !nix_txsch_is_pan_schq(rvu, schq))
+ continue;
if (TXSCH_MAP_FUNC(txsch->pfvf_map[schq]) != pcifunc)
continue;
nix_reset_tx_schedule(rvu, blkaddr, lvl, schq);
@@ -2754,7 +2883,16 @@ static int nix_txschq_free_one(struct rvu *rvu,
schq = req->schq;
txsch = &nix_hw->txsch[lvl];
- if (lvl >= hw->cap.nix_tx_aggr_lvl || schq >= txsch->schq.max)
+ if (req->flags & TXSCHQ_FREE_PAN_TL1) {
+ if (!nix_txsch_pan_allowed(rvu, pcifunc))
+ return NIX_AF_ERR_TLX_INVALID;
+ if (!nix_txsch_is_pan_schq(rvu, schq))
+ return NIX_AF_ERR_TLX_INVALID;
+ } else if (lvl >= hw->cap.nix_tx_aggr_lvl) {
+ return 0;
+ }
+
+ if (schq >= txsch->schq.max)
return 0;
pfvf_map = txsch->pfvf_map;
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
index e36c68ee5d84..40d49a323814 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
@@ -1833,9 +1833,23 @@ int rvu_mbox_handler_npc_install_flow(struct rvu *rvu,
target = req->hdr.pcifunc;
}
- /* ignore chan_mask in case pf func is not AF, revisit later */
- if (!is_pffunc_af(req->hdr.pcifunc))
- req->chan_mask = rvu_get_cpt_chan_mask(rvu);
+ /* Non-AF callers get the CPT default chan_mask unless the authorized
+ * switchdev PF sets set_chanmask to preserve a caller-supplied mask.
+ * VFs and other PFs must not use set_chanmask; that would bypass
+ * channel isolation.
+ */
+ if (!is_pffunc_af(req->hdr.pcifunc)) {
+ if (req->set_chanmask &&
+ !rvu_is_switch_pcifunc(rvu, req->hdr.pcifunc)) {
+ rvu_npc_free_entry_for_flow_install(rvu,
+ req->hdr.pcifunc,
+ allocated,
+ req->entry);
+ return NPC_FLOW_VF_PERM_DENIED;
+ }
+ if (!req->set_chanmask)
+ req->chan_mask = rvu_get_cpt_chan_mask(rvu);
+ }
err = npc_check_unsupported_flows(rvu, req->features, req->intf);
if (err) {
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_txrx.h b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_txrx.h
index acf259d72008..73a98b94426b 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_txrx.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_txrx.h
@@ -78,6 +78,8 @@ struct otx2_rcv_queue {
struct sg_list {
u16 num_segs;
u16 flags;
+ u16 cq_idx;
+ u16 len;
u64 skb;
u64 size[OTX2_MAX_FRAGS_IN_SQE];
u64 dma_addr[OTX2_MAX_FRAGS_IN_SQE];
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 6/8] octeontx2-pf: switch: Register notifiers for switch offload
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
` (4 preceding siblings ...)
2026-08-31 13:19 ` [PATCH v9 net-next 5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,6/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox Ratheesh Kannoth
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
The representor enables switch mode via devlink; register and unregister
the switch notifier blocks when that mode is turned on or off so the PF
can observe FIB routes, neighbour updates, IPv4/IPv6 address changes,
netdev state, and switchdev FDB notifications.
Add sw_nb_v4.c and sw_nb_v6.c for IPv4 and IPv6-specific handling, build
sw_nb_v6.o only when CONFIG_IPV6 is set, and extend sw_nb.c with device
filtering for Cavium ports behind bridges and VLANs.
Initialize and tear down the existing sw_fdb, sw_fib, and sw_fl helpers
together with notifier registration.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../ethernet/marvell/octeontx2/nic/Makefile | 13 +-
.../net/ethernet/marvell/octeontx2/nic/rep.c | 45 +-
.../marvell/octeontx2/nic/switch/sw_nb.c | 541 +++++++++++++++++-
.../marvell/octeontx2/nic/switch/sw_nb.h | 37 +-
.../marvell/octeontx2/nic/switch/sw_nb_v4.c | 360 ++++++++++++
.../marvell/octeontx2/nic/switch/sw_nb_v4.h | 21 +
.../marvell/octeontx2/nic/switch/sw_nb_v6.c | 301 ++++++++++
.../marvell/octeontx2/nic/switch/sw_nb_v6.h | 21 +
8 files changed, 1330 insertions(+), 9 deletions(-)
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.h
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
create mode 100644 drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/Makefile b/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
index 27590b94133b..6050228be067 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/Makefile
@@ -11,7 +11,18 @@ rvu_nicpf-y := otx2_pf.o otx2_common.o otx2_txrx.o otx2_ethtool.o \
otx2_flows.o otx2_tc.o cn10k.o cn20k.o otx2_dmac_flt.o \
otx2_devlink.o qos_sq.o qos.o otx2_xsk.o switch/sw_fdb.o \
switch/sw_fl.o
-rvu_nicpf-$(CONFIG_OCTEONTX_SWITCH) += switch/sw_nb.o switch/sw_fib.o
+rvu_nicpf-$(CONFIG_OCTEONTX_SWITCH) += switch/sw_nb.o switch/sw_fib.o \
+ switch/sw_nb_v4.o
+# sw_nb_v6.o calls IPv6 symbols exported by the ipv6 module; only link it
+# when those symbols are reachable (IPv6 built-in, or both driver and IPv6
+# are modules).
+ifeq ($(CONFIG_IPV6),y)
+rvu_nicpf-$(CONFIG_OCTEONTX_SWITCH) += switch/sw_nb_v6.o
+else ifneq ($(CONFIG_IPV6),)
+ifeq ($(CONFIG_OCTEONTX2_PF),m)
+rvu_nicpf-$(CONFIG_OCTEONTX_SWITCH) += switch/sw_nb_v6.o
+endif
+endif
rvu_nicvf-y := otx2_vf.o
rvu_rep-y := rep.o
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
index 257a2ae6a53e..96ec58c50843 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
@@ -15,6 +15,7 @@
#include "cn10k.h"
#include "otx2_reg.h"
#include "rep.h"
+#include "switch/sw_nb.h"
#define DRV_NAME "rvu_rep"
#define DRV_STRING "Marvell RVU Representor Driver"
@@ -399,22 +400,62 @@ static void rvu_rep_get_stats64(struct net_device *dev,
static int rvu_eswitch_config(struct otx2_nic *priv, u8 ena)
{
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+ struct net_device *netdev = priv->netdev;
+#endif
struct devlink_port_attrs attrs = {};
struct esw_cfg_req *req;
+ int mbox_err;
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+ int err;
+#endif
rvu_rep_devlink_set_switch_id(priv, &attrs.switch_id);
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+ /* Disable unregisters PF notifiers before ESW_CFG clears rep_mode on
+ * the AF. unregister_*_notifier() removes each block synchronously,
+ * so there is no window where the AF considers the eswitch off while
+ * sw_nb_* handlers remain registered and could still send mailbox
+ * traffic (that race existed only when disable ran after the mailbox).
+ */
+ if (ena) {
+ err = otx2_sw_nb_register(netdev);
+ if (err)
+ return err;
+ } else {
+ /* TODO: On disable, notifiers are unregistered before ESW_CFG. If
+ * mailbox allocation fails below, restore otx2_sw_nb_register()
+ * so software notifiers are not abandoned while hardware remains
+ * in eswitch mode.
+ */
+ err = otx2_sw_nb_unregister(netdev);
+ if (err)
+ return err;
+ }
+#endif
+
mutex_lock(&priv->mbox.lock);
req = otx2_mbox_alloc_msg_esw_cfg(&priv->mbox);
if (!req) {
mutex_unlock(&priv->mbox.lock);
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+ if (ena)
+ otx2_sw_nb_unregister(netdev);
+#endif
return -ENOMEM;
}
req->ena = ena;
memcpy(req->switch_id, attrs.switch_id.id, attrs.switch_id.id_len);
- otx2_sync_mbox_msg(&priv->mbox);
+ mbox_err = otx2_sync_mbox_msg(&priv->mbox);
mutex_unlock(&priv->mbox.lock);
- return 0;
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+ if (ena && mbox_err)
+ otx2_sw_nb_unregister(netdev);
+#endif
+
+ return mbox_err;
}
static netdev_tx_t rvu_rep_xmit(struct sk_buff *skb, struct net_device *dev)
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
index 426a42011930..b51d8d2d01b8 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
@@ -4,18 +4,555 @@
* Copyright (C) 2026 Marvell.
*
*/
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <net/switchdev.h>
+#include <net/netevent.h>
+#include <net/arp.h>
+#include <net/route.h>
+#include <linux/inetdevice.h>
+#include <net/addrconf.h>
+
+#include "../otx2_reg.h"
+#include "../otx2_common.h"
+#include "../otx2_struct.h"
+#include "../cn10k.h"
#include "sw_nb.h"
+#include "sw_fdb.h"
+#include "sw_fib.h"
+#include "sw_fl.h"
+#include "sw_nb_v4.h"
+#include "sw_nb_v6.h"
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
-int otx2_sw_nb_unregister(void)
+/* PF netdev for netdev_* logging when notifier info has no device */
+static struct net_device *sw_nb_pf_netdev;
+/* Notifier registration is toggled only from rvu_eswitch_config(), which is
+ * reached exclusively when switchdev mode is enabled on the RVU eswitch
+ * representor PF (PCI_DEVID_RVU_REP). The sole call path is:
+ *
+ * DEVLINK_CMD_ESWITCH_MODE_SET
+ * -> otx2_devlink_eswitch_mode_set() [otx2_rep_dev() only]
+ * -> rvu_rep_create() / rvu_rep_destroy()
+ * -> rvu_eswitch_config(ena = 1) -> otx2_sw_nb_register()
+ * -> rvu_eswitch_config(ena = 0) -> otx2_sw_nb_unregister()
+ *
+ * On disable, otx2_sw_nb_unregister() runs before the ESW_CFG mailbox so flush
+ * paths in sw_fdb/fib/fl_deinit() can still reach hardware.
+ *
+ * Other OcteonTX2 netdev PFs/VFs also have a devlink, but their
+ * eswitch_mode_set handler returns -EOPNOTSUPP. The AF rvu_devlink
+ * eswitch_mode_set does not register these notifiers. There is exactly
+ * one RVU_REP PCI function (and netdev devlink) per RVU, and devlink
+ * core holds devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET
+ * handler, so this path cannot run concurrently on the same device.
+ * otx2_sw_nb_registered further ensures at most one active registration.
+ */
+static bool otx2_sw_nb_registered;
+
+static const char *sw_nb_cmd2str[OTX2_CMD_MAX] = {
+ [OTX2_DEV_UP] = "OTX2_DEV_UP",
+ [OTX2_DEV_DOWN] = "OTX2_DEV_DOWN",
+ [OTX2_DEV_CHANGE] = "OTX2_DEV_CHANGE",
+ [OTX2_NEIGH_UPDATE] = "OTX2_NEIGH_UPDATE",
+ [OTX2_FIB_ENTRY_REPLACE] = "OTX2_FIB_ENTRY_REPLACE",
+ [OTX2_FIB_ENTRY_ADD] = "OTX2_FIB_ENTRY_ADD",
+ [OTX2_FIB_ENTRY_DEL] = "OTX2_FIB_ENTRY_DEL",
+ [OTX2_FIB_ENTRY_APPEND] = "OTX2_FIB_ENTRY_APPEND",
+};
+
+const char *sw_nb_get_cmd2str(int cmd)
+{
+ return sw_nb_cmd2str[cmd];
+}
+EXPORT_SYMBOL(sw_nb_get_cmd2str);
+
+bool sw_nb_is_cavium_dev(struct net_device *netdev)
+{
+ struct pci_dev *pdev;
+ struct device *dev;
+
+ dev = netdev->dev.parent;
+ if (!dev || dev->bus != &pci_bus_type)
+ return false;
+
+ pdev = to_pci_dev(dev);
+ if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
+ return false;
+
+ return true;
+}
+
+/* Resolve the Cavium PF netdev used to reach the switch AF for offload.
+ *
+ * For a bridge master netdev, any Cavium netdev enslaved to the bridge is
+ * sufficient: callers only need a PF netdev to obtain the switch AF mailbox
+ * context (pcifunc). Bridge-specific information is tagged separately in
+ * the offload entry (entry->bridge), so walking every lower netdev is not
+ * required here.
+ *
+ * Only a single level of netdev nesting is resolved (bridge lower dev or
+ * VLAN real dev). Nested topologies such as VLAN-over-bridge are not
+ * supported; offload will not work for those configurations.
+ */
+struct net_device *sw_nb_resolve_pf_dev(struct net_device *dev)
+{
+ struct net_device *pf_dev = dev;
+ struct list_head *iter;
+
+ rcu_read_lock();
+
+ if (netif_is_bridge_master(dev)) {
+ iter = &dev->adj_list.lower;
+ pf_dev = netdev_next_lower_dev_rcu(dev, &iter);
+ if (!pf_dev)
+ pf_dev = dev;
+ } else if (is_vlan_dev(dev)) {
+ pf_dev = vlan_dev_real_dev(dev);
+ }
+
+ rcu_read_unlock();
+
+ if (!sw_nb_is_cavium_dev(pf_dev))
+ return NULL;
+
+ return pf_dev;
+}
+
+static int sw_nb_check_slaves(struct net_device *dev,
+ struct netdev_nested_priv *priv)
{
+ int *cnt;
+
+ if (!priv->flags)
+ return 0;
+
+ priv->flags &= sw_nb_is_cavium_dev(dev);
+ if (priv->flags) {
+ cnt = priv->data;
+ (*cnt)++;
+ }
+
return 0;
}
-int otx2_sw_nb_register(void)
+/* Switch offload has no network namespace support. The global notifiers
+ * registered below are not scoped to a netns, and sw_nb_is_cavium_dev()
+ * matches any Cavium PCI netdev without checking dev_net(). All netdevs
+ * involved in offload (PF/VF ports, bridge members, VLANs, neighbours,
+ * and routes) must therefore reside in &init_net for offload to work.
+ */
+bool sw_nb_is_valid_dev(struct net_device *netdev)
+{
+ struct netdev_nested_priv priv;
+ struct net_device *br;
+ int cnt = 0;
+ bool valid;
+
+ priv.flags = true;
+ priv.data = &cnt;
+
+ rcu_read_lock();
+
+ if (netif_is_bridge_master(netdev) || is_vlan_dev(netdev)) {
+ netdev_walk_all_lower_dev_rcu(netdev, sw_nb_check_slaves, &priv);
+ valid = priv.flags && cnt;
+ rcu_read_unlock();
+ return valid;
+ }
+
+ if (netif_is_bridge_port(netdev)) {
+ br = netdev_master_upper_dev_get_rcu(netdev);
+ if (!br) {
+ rcu_read_unlock();
+ return false;
+ }
+ netdev_walk_all_lower_dev_rcu(br, sw_nb_check_slaves, &priv);
+ valid = priv.flags && cnt;
+ rcu_read_unlock();
+ return valid;
+ }
+
+ rcu_read_unlock();
+
+ return sw_nb_is_cavium_dev(netdev);
+}
+
+static int sw_nb_fdb_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
+ struct switchdev_notifier_fdb_info *fdb_info = ptr;
+
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ switch (event) {
+ case SWITCHDEV_FDB_ADD_TO_DEVICE:
+ if (fdb_info->is_local)
+ break;
+ break;
+
+ case SWITCHDEV_FDB_DEL_TO_DEVICE:
+ if (fdb_info->is_local)
+ break;
+ break;
+
+ default:
+ return NOTIFY_DONE;
+ }
+
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block sw_nb_fdb = {
+ .notifier_call = sw_nb_fdb_event,
+};
+
+static void __maybe_unused
+sw_nb_fib_event_dump(unsigned long event, void *ptr)
+{
+ struct fib_entry_notifier_info *fen_info = ptr;
+ struct net_device *log_dev;
+ struct fib_nh *fib_nh;
+ struct fib_info *fi;
+ int i;
+
+ fi = fen_info->fi;
+ log_dev = (fi && fi->fib_nhs) ? fi->fib_nh->fib_nh_dev : sw_nb_pf_netdev;
+ if (log_dev)
+ netdev_info(log_dev, "%s: FIB event=%lu dst=%pI4h dstlen=%d type=%u\n",
+ __func__, event, &fen_info->dst, fen_info->dst_len,
+ fen_info->type);
+
+ if (!fi)
+ return;
+
+ fib_nh = fi->fib_nh;
+ for (i = 0; i < fi->fib_nhs; i++, fib_nh++) {
+ if (!fib_nh->fib_nh_dev)
+ continue;
+ netdev_info(fib_nh->fib_nh_dev,
+ "%s: dev=%s saddr=%pI4n gw=%pI4n\n",
+ __func__, fib_nh->fib_nh_dev->name,
+ &fib_nh->nh_saddr, &fib_nh->fib_nh_gw4);
+ }
+}
+
+#define SWITCH_NB_FIB_EVENT_DUMP(...) \
+ sw_nb_fib_event_dump(__VA_ARGS__)
+
+int sw_nb_fib_event_to_otx2_event(int event, struct net_device *netdev)
+{
+ switch (event) {
+ case FIB_EVENT_ENTRY_REPLACE:
+ return OTX2_FIB_ENTRY_REPLACE;
+ case FIB_EVENT_ENTRY_ADD:
+ return OTX2_FIB_ENTRY_ADD;
+ case FIB_EVENT_ENTRY_DEL:
+ return OTX2_FIB_ENTRY_DEL;
+ default:
+ break;
+ }
+
+ netdev_err(netdev, "Wrong FIB event %d\n", event);
+ return -1;
+}
+
+static int sw_nb_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct fib_notifier_info *info = ptr;
+
+ switch (event) {
+ case FIB_EVENT_ENTRY_REPLACE:
+ case FIB_EVENT_ENTRY_ADD:
+ case FIB_EVENT_ENTRY_DEL:
+ break;
+ default:
+ if (sw_nb_pf_netdev)
+ netdev_dbg(sw_nb_pf_netdev,
+ "%s: Won't process FIB event %lu\n",
+ __func__, event);
+ return NOTIFY_DONE;
+ }
+
+ switch (info->family) {
+ case AF_INET:
+ return sw_nb_v4_fib_event(nb, event, ptr);
+#if IS_REACHABLE(CONFIG_IPV6)
+ case AF_INET6:
+ return sw_nb_v6_fib_event(nb, event, ptr);
+#endif
+ default:
+ break;
+ }
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block sw_nb_fib = {
+ .notifier_call = sw_nb_fib_event,
+};
+
+static int sw_nb_net_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct neighbour *n = ptr;
+
+ if (!sw_nb_is_valid_dev(n->dev))
+ return NOTIFY_DONE;
+
+ if (event != NETEVENT_NEIGH_UPDATE)
+ return NOTIFY_DONE;
+
+ switch (n->tbl->family) {
+ case AF_INET:
+ return sw_nb_net_v4_neigh_update(nb, event, ptr);
+#if IS_REACHABLE(CONFIG_IPV6)
+ case AF_INET6:
+ return sw_nb_net_v6_neigh_update(nb, event, ptr);
+#endif
+ default:
+ break;
+ }
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block sw_nb_netevent = {
+ .notifier_call = sw_nb_net_event,
+
+};
+
+int sw_nb_inetaddr_event_to_otx2_event(int event, struct net_device *netdev)
+{
+ switch (event) {
+ case NETDEV_CHANGE:
+ return OTX2_DEV_CHANGE;
+ case NETDEV_UP:
+ return OTX2_DEV_UP;
+ case NETDEV_DOWN:
+ return OTX2_DEV_DOWN;
+ default:
+ break;
+ }
+ netdev_dbg(netdev, "%s: Wrong interaddr event %d\n",
+ __func__, event);
+ return -1;
+}
+
+static struct notifier_block sw_nb_v4_inetaddr = {
+ .notifier_call = sw_nb_v4_inetaddr_event,
+};
+
+#if IS_REACHABLE(CONFIG_IPV6)
+static struct notifier_block sw_nb_v6_inetaddr = {
+ .notifier_call = sw_nb_v6_inetaddr_event,
+};
+#endif
+
+static int sw_nb_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+ struct in_device *idev;
+ struct inet6_dev *i6dev;
+
+ if (event != NETDEV_CHANGE &&
+ event != NETDEV_UP &&
+ event != NETDEV_DOWN) {
+ return NOTIFY_DONE;
+ }
+
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ idev = __in_dev_get_rtnl(dev);
+ if (idev)
+ sw_nb_v4_netdev_event(unused, event, ptr);
+
+#if IS_REACHABLE(CONFIG_IPV6)
+ i6dev = __in6_dev_get(dev);
+ if (i6dev)
+ sw_nb_v6_netdev_event(unused, event, ptr);
+#endif
+
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block sw_nb_netdev = {
+ .notifier_call = sw_nb_netdev_event,
+};
+
+int otx2_sw_nb_unregister(struct net_device *netdev)
+{
+ int err, ret = 0;
+
+ if (!otx2_sw_nb_registered)
+ return 0;
+
+ err = unregister_switchdev_notifier(&sw_nb_fdb);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister switchdev nb\n");
+ ret = err;
+ }
+
+ err = unregister_fib_notifier(&init_net, &sw_nb_fib);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister fib nb\n");
+ if (!ret)
+ ret = err;
+ }
+
+ err = unregister_netevent_notifier(&sw_nb_netevent);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister netevent\n");
+ if (!ret)
+ ret = err;
+ }
+
+ err = unregister_inetaddr_notifier(&sw_nb_v4_inetaddr);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister addr event\n");
+ if (!ret)
+ ret = err;
+ }
+
+#if IS_REACHABLE(CONFIG_IPV6)
+ err = unregister_inet6addr_notifier(&sw_nb_v6_inetaddr);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister addr event\n");
+ if (!ret)
+ ret = err;
+ }
+#endif
+
+ err = unregister_netdevice_notifier(&sw_nb_netdev);
+ if (err) {
+ netdev_err(netdev, "Failed to unregister netdev notifier\n");
+ if (!ret)
+ ret = err;
+ }
+
+ sw_fl_deinit();
+ otx2_sw_fib_deinit();
+ sw_fdb_deinit();
+
+ sw_nb_pf_netdev = NULL;
+ otx2_sw_nb_registered = false;
+
+ return ret;
+}
+EXPORT_SYMBOL(otx2_sw_nb_unregister);
+
+/* Concurrent registration from multiple devlink instances cannot occur on a
+ * given RVU: only the RVU_REP netdev devlink reaches this function (see
+ * comment above). The AF and PF/VF devlinks do not call otx2_sw_nb_register(),
+ * and their eswitch_mode_set handlers return -EOPNOTSUPP. devlink core
+ * holds devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler,
+ * so two threads cannot enter here concurrently on that single rep devlink.
+ * A second call after successful registration returns -EBUSY before any
+ * notifier or workqueue state is modified.
+ */
+int otx2_sw_nb_register(struct net_device *netdev)
{
+ int err;
+
+ /* Notifier blocks are global and only one RVU_REP may register at a
+ * time (switch offload is init_net-wide; see comment at file top).
+ * A second RVU card gets -EBUSY here by design. Concurrent calls on
+ * the same RVU_REP cannot happen: only that netdev's devlink reaches
+ * this function (otx2_rep_dev()), and devlink core holds
+ * devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler.
+ * No extra lock is needed to protect the notifier chains.
+ */
+ if (otx2_sw_nb_registered)
+ return -EBUSY;
+
+ sw_nb_pf_netdev = netdev;
+
+ err = sw_fdb_init();
+ if (err)
+ goto err_clear;
+
+ err = otx2_sw_fib_init();
+ if (err)
+ goto err_fdb;
+
+ err = sw_fl_init();
+ if (err)
+ goto err_fib;
+
+ err = register_switchdev_notifier(&sw_nb_fdb);
+ if (err) {
+ netdev_err(netdev, "Failed to register switchdev nb\n");
+ goto err_helpers;
+ }
+
+ err = register_fib_notifier(&init_net, &sw_nb_fib, NULL, NULL);
+ if (err) {
+ netdev_err(netdev, "Failed to register fb notifier block\n");
+ goto err1;
+ }
+
+ err = register_netevent_notifier(&sw_nb_netevent);
+ if (err) {
+ netdev_err(netdev, "Failed to register netevent\n");
+ goto err2;
+ }
+
+#if IS_REACHABLE(CONFIG_IPV6)
+ err = register_inet6addr_notifier(&sw_nb_v6_inetaddr);
+ if (err) {
+ netdev_err(netdev, "Failed to register addr event\n");
+ goto err3;
+ }
+#endif
+
+ err = register_inetaddr_notifier(&sw_nb_v4_inetaddr);
+ if (err) {
+ netdev_err(netdev, "Failed to register addr event\n");
+ goto err4;
+ }
+
+ err = register_netdevice_notifier(&sw_nb_netdev);
+ if (err) {
+ netdev_err(netdev, "Failed to register netdevice nb\n");
+ goto err5;
+ }
+
+ otx2_sw_nb_registered = true;
+
return 0;
+
+err5:
+ unregister_inetaddr_notifier(&sw_nb_v4_inetaddr);
+
+err4:
+#if IS_REACHABLE(CONFIG_IPV6)
+ unregister_inet6addr_notifier(&sw_nb_v6_inetaddr);
+
+err3:
+#endif
+ unregister_netevent_notifier(&sw_nb_netevent);
+
+err2:
+ unregister_fib_notifier(&init_net, &sw_nb_fib);
+
+err1:
+ unregister_switchdev_notifier(&sw_nb_fdb);
+
+err_helpers:
+ sw_fl_deinit();
+err_fib:
+ otx2_sw_fib_deinit();
+err_fdb:
+ sw_fdb_deinit();
+err_clear:
+ sw_nb_pf_netdev = NULL;
+ return err;
}
+EXPORT_SYMBOL(otx2_sw_nb_register);
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
index 0ba29f76fd41..39435f23427c 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
@@ -9,12 +9,41 @@
#include <linux/kconfig.h>
+struct net_device;
+struct otx2_nic;
+struct af2pf_fdb_refresh_req;
+struct msg_rsp;
+
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
-int otx2_sw_nb_register(void);
-int otx2_sw_nb_unregister(void);
+enum {
+ OTX2_DEV_UP = 1,
+ OTX2_DEV_DOWN,
+ OTX2_DEV_CHANGE,
+ OTX2_NEIGH_UPDATE,
+ OTX2_FIB_ENTRY_REPLACE,
+ OTX2_FIB_ENTRY_ADD,
+ OTX2_FIB_ENTRY_DEL,
+ OTX2_FIB_ENTRY_APPEND,
+ OTX2_CMD_MAX,
+};
+
+int otx2_sw_nb_register(struct net_device *netdev);
+int otx2_sw_nb_unregister(struct net_device *netdev);
+bool sw_nb_is_valid_dev(struct net_device *netdev);
+struct net_device *sw_nb_resolve_pf_dev(struct net_device *dev);
+
+int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp);
+
+bool sw_nb_is_cavium_dev(struct net_device *netdev);
+int sw_nb_fib_event_to_otx2_event(int event, struct net_device *netdev);
+int sw_nb_inetaddr_event_to_otx2_event(int event, struct net_device *netdev);
+
+const char *sw_nb_get_cmd2str(int cmd);
#else
-static inline int otx2_sw_nb_register(void) { return 0; }
-static inline int otx2_sw_nb_unregister(void) { return 0; }
+static inline int otx2_sw_nb_register(struct net_device *netdev) { return 0; }
+static inline int otx2_sw_nb_unregister(struct net_device *netdev) { return 0; }
#endif
#endif /* SW_NB_H_ */
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
new file mode 100644
index 000000000000..31009e00121f
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
@@ -0,0 +1,360 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <net/switchdev.h>
+#include <net/netevent.h>
+#include <net/arp.h>
+#include <net/route.h>
+#include <linux/inetdevice.h>
+
+#include "../otx2_reg.h"
+#include "../otx2_common.h"
+#include "../otx2_struct.h"
+#include "../cn10k.h"
+#include "sw_nb.h"
+#include "sw_fdb.h"
+#include "sw_fib.h"
+#include "sw_fl.h"
+#include "sw_nb_v4.h"
+
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+
+int sw_nb_v4_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+ struct netdev_hw_addr *dev_addr;
+ struct net_device *pf_dev;
+ struct in_device *idev;
+ struct in_ifaddr *ifa;
+ struct fib_entry *entry;
+ struct otx2_nic *pf;
+
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ idev = __in_dev_get_rtnl(dev);
+ if (!idev || !idev->ifa_list)
+ return NOTIFY_DONE;
+
+ /* Switch offload supports a single IPv4 address per interface for now. */
+ ifa = rtnl_dereference(idev->ifa_list);
+
+ entry = kcalloc(1, sizeof(*entry), GFP_KERNEL);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ entry->cmd = sw_nb_inetaddr_event_to_otx2_event(event, dev);
+ entry->dst = ifa->ifa_address;
+ entry->dst_len = 32;
+ entry->mac_valid = 1;
+ entry->host = 1;
+
+ pf_dev = sw_nb_resolve_pf_dev(dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ if (netif_is_bridge_master(dev)) {
+ entry->bridge = 1;
+ } else if (is_vlan_dev(dev)) {
+ entry->vlan_valid = 1;
+ entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
+ }
+
+ pf = netdev_priv(pf_dev);
+ entry->port_id = pf->pcifunc;
+
+ rcu_read_lock();
+ for_each_dev_addr(dev, dev_addr) {
+ ether_addr_copy(entry->mac, dev_addr->addr);
+ break;
+ }
+ rcu_read_unlock();
+
+ netdev_dbg(dev, "%s: pushing netdev event from HOST interface address %pI4n, %pM, dev=%s\n",
+ __func__, &entry->dst, entry->mac, dev->name);
+ kfree(entry);
+
+ return NOTIFY_DONE;
+}
+
+int sw_nb_v4_inetaddr_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct in_ifaddr *ifa = (struct in_ifaddr *)ptr;
+ struct net_device *dev = ifa->ifa_dev->dev;
+ struct netdev_hw_addr *dev_addr;
+ struct net_device *pf_dev;
+ struct fib_entry *entry;
+ struct otx2_nic *pf;
+
+ if (event != NETDEV_CHANGE &&
+ event != NETDEV_UP &&
+ event != NETDEV_DOWN) {
+ return NOTIFY_DONE;
+ }
+
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ /* On NETDEV_DOWN the deleted address is passed in ifa; ifa_list may
+ * already be empty when the last address is removed.
+ */
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ entry->cmd = sw_nb_inetaddr_event_to_otx2_event(event, dev);
+ entry->dst = ifa->ifa_address;
+ entry->dst_len = 32;
+ entry->mac_valid = 1;
+ entry->host = 1;
+
+ pf_dev = sw_nb_resolve_pf_dev(dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ if (netif_is_bridge_master(dev)) {
+ entry->bridge = 1;
+ } else if (is_vlan_dev(dev)) {
+ entry->vlan_valid = 1;
+ entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
+ }
+
+ pf = netdev_priv(pf_dev);
+ entry->port_id = pf->pcifunc;
+
+ rcu_read_lock();
+ for_each_dev_addr(dev, dev_addr) {
+ ether_addr_copy(entry->mac, dev_addr->addr);
+ break;
+ }
+ rcu_read_unlock();
+
+ netdev_dbg(dev, "%s: pushing inetaddr event from HOST interface address %pI4n, %pM, %s\n",
+ __func__, &entry->dst, entry->mac, dev->name);
+
+ kfree(entry);
+ return NOTIFY_DONE;
+}
+
+int sw_nb_v4_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev, *pf_dev = NULL, *nh_pf_dev;
+ struct fib_entry_notifier_info *fen_info = ptr;
+ struct fib_entry *entries, *iter;
+ struct netdev_hw_addr *dev_addr;
+ struct neighbour *neigh;
+ struct fib_nh *fib_nh;
+ struct fib_info *fi;
+ struct otx2_nic *pf;
+ __be32 *haddr;
+ int hcnt = 0;
+ int cnt, i;
+
+ /* Process only UNICAST routes add or del */
+ if (fen_info->type != RTN_UNICAST)
+ return NOTIFY_DONE;
+
+ fi = fen_info->fi;
+ if (!fi)
+ return NOTIFY_DONE;
+
+ if (fi->fib_nh_is_v6) {
+ struct net_device *log_dev = (fi->fib_nhs > 0) ?
+ fi->fib_nh->fib_nh_dev : NULL;
+
+ if (log_dev)
+ netdev_dbg(log_dev, "%s: Received v6 notification\n",
+ __func__);
+ return NOTIFY_DONE;
+ }
+
+ /* TODO: External nexthop routes (fi->nh set, fi->fib_nhs == 0) are not
+ * yet supported for switch offload. Only embedded fi->fib_nh[] paths
+ * are walked below; nhid and nexthop-group installs are intentionally
+ * skipped until fib_info_num_path()/fib_info_nhc() handling is added.
+ */
+ entries = kcalloc(fi->fib_nhs, sizeof(*entries), GFP_ATOMIC);
+ if (!entries)
+ return NOTIFY_DONE;
+
+ haddr = kcalloc(fi->fib_nhs, sizeof(*haddr), GFP_ATOMIC);
+ if (!haddr) {
+ kfree(entries);
+ return NOTIFY_DONE;
+ }
+
+ iter = entries;
+ fib_nh = fi->fib_nh;
+ for (i = 0; i < fi->fib_nhs; i++, fib_nh++) {
+ dev = fib_nh->fib_nh_dev;
+
+ if (!dev)
+ continue;
+
+ if (dev->type != ARPHRD_ETHER)
+ continue;
+
+ if (!sw_nb_is_valid_dev(dev))
+ continue;
+
+ iter->cmd = sw_nb_fib_event_to_otx2_event(event, dev);
+ iter->dst = htonl(fen_info->dst);
+ iter->dst_len = fen_info->dst_len;
+ iter->gw = fib_nh->fib_nh_gw4;
+
+ netdev_dbg(dev, "%s: FIB route Rule cmd=%llu dst=%pI4n dst_len=%u gw=%pI4n\n",
+ __func__, iter->cmd, &iter->dst, iter->dst_len, &iter->gw);
+
+ nh_pf_dev = sw_nb_resolve_pf_dev(dev);
+ if (!nh_pf_dev)
+ continue;
+ pf_dev = nh_pf_dev;
+
+ if (netif_is_bridge_master(dev)) {
+ iter->bridge = 1;
+ } else if (is_vlan_dev(dev)) {
+ iter->vlan_valid = 1;
+ iter->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
+ }
+
+ pf = netdev_priv(pf_dev);
+ iter->port_id = pf->pcifunc;
+
+ /* Point-to-point routes, including default routes with no
+ * gateway, are not supported for switch offload.
+ */
+ if (!fib_nh->fib_nh_gw4)
+ continue;
+ iter->gw_valid = 1;
+
+ if (fib_nh->nh_saddr)
+ haddr[hcnt++] = fib_nh->nh_saddr;
+
+ rcu_read_lock();
+ neigh = ip_neigh_gw4(fib_nh->fib_nh_dev, fib_nh->fib_nh_gw4);
+ if (!neigh || IS_ERR(neigh)) {
+ rcu_read_unlock();
+ continue;
+ }
+
+ neigh_ha_snapshot(iter->mac, neigh, fib_nh->fib_nh_dev);
+ if (is_valid_ether_addr(iter->mac))
+ iter->mac_valid = 1;
+
+ iter++;
+ rcu_read_unlock();
+ }
+
+ cnt = iter - entries;
+ if (!cnt) {
+ kfree(entries);
+ kfree(haddr);
+ return NOTIFY_DONE;
+ }
+
+ if (pf_dev)
+ netdev_dbg(pf_dev, "pf_dev is %s cnt=%d\n", pf_dev->name, cnt);
+ kfree(entries);
+
+ if (!hcnt) {
+ kfree(haddr);
+ return NOTIFY_DONE;
+ }
+
+ if (!pf_dev) {
+ kfree(haddr);
+ return NOTIFY_DONE;
+ }
+
+ entries = kcalloc(hcnt, sizeof(*entries), GFP_ATOMIC);
+ if (!entries) {
+ kfree(haddr);
+ return NOTIFY_DONE;
+ }
+
+ iter = entries;
+
+ /* Host routes reuse pf_dev/pf from the last resolved Cavium netdev:
+ * pf_dev only identifies the switch AF mailbox context for switchdev
+ * programming; any previously resolved Cavium netdev is sufficient.
+ */
+ for (i = 0; i < hcnt; i++, iter++) {
+ iter->cmd = sw_nb_fib_event_to_otx2_event(event, pf_dev);
+ iter->dst = haddr[i];
+ iter->dst_len = 32;
+ iter->mac_valid = 1;
+ iter->host = 1;
+ iter->port_id = pf->pcifunc;
+
+ rcu_read_lock();
+ for_each_dev_addr(pf_dev, dev_addr) {
+ ether_addr_copy(iter->mac, dev_addr->addr);
+ break;
+ }
+ rcu_read_unlock();
+
+ netdev_dbg(pf_dev, "%s: FIB host Rule cmd=%llu dst=%pI4n dst_len=%u %s\n",
+ __func__, iter->cmd, &iter->dst, iter->dst_len,
+ pf_dev->name);
+ }
+ kfree(entries);
+ kfree(haddr);
+ return NOTIFY_DONE;
+}
+
+int sw_nb_net_v4_neigh_update(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct net_device *pf_dev;
+ struct neighbour *n = ptr;
+ struct fib_entry *entry;
+ struct otx2_nic *pf;
+
+ if (n->tbl != &arp_tbl)
+ return NOTIFY_DONE;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ entry->cmd = OTX2_NEIGH_UPDATE;
+ entry->dst = *(__be32 *)n->primary_key;
+ entry->dst_len = n->tbl->key_len * 8;
+ entry->mac_valid = 1;
+ entry->nud_state = n->nud_state;
+ neigh_ha_snapshot(entry->mac, n, n->dev);
+
+ pf_dev = sw_nb_resolve_pf_dev(n->dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ if (netif_is_bridge_master(n->dev)) {
+ entry->bridge = 1;
+ } else if (is_vlan_dev(n->dev)) {
+ entry->vlan_valid = 1;
+ entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(n->dev));
+ }
+
+ pf = netdev_priv(pf_dev);
+ entry->port_id = pf->pcifunc;
+
+ kfree(entry);
+ return NOTIFY_DONE;
+}
+
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.h
new file mode 100644
index 000000000000..c6dbf4b93a9a
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_NB_V4_H_
+#define SW_NB_V4_H_
+
+int sw_nb_v4_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_net_v4_neigh_update(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_v4_inetaddr_event(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_v4_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr);
+#endif // SW_NB_V4_H__
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
new file mode 100644
index 000000000000..3497e60aedbe
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Marvell RVU switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <net/switchdev.h>
+#include <net/netevent.h>
+#include <net/arp.h>
+#include <net/route.h>
+#include <linux/inetdevice.h>
+#include <net/addrconf.h>
+#include <net/ip6_fib.h>
+#include <net/nexthop.h>
+
+#include "../otx2_reg.h"
+#include "../otx2_common.h"
+#include "../otx2_struct.h"
+#include "../cn10k.h"
+#include "sw_nb.h"
+#include "sw_fdb.h"
+#include "sw_fib.h"
+#include "sw_fl.h"
+#include "sw_nb_v6.h"
+
+#if IS_ENABLED(CONFIG_IPV6) && IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+
+int sw_nb_v6_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+{
+ struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+ struct netdev_hw_addr *dev_addr;
+ struct net_device *pf_dev;
+ struct inet6_ifaddr *ifp;
+ struct inet6_dev *i6dev;
+ struct fib_entry *entry;
+ struct in6_addr addr;
+ struct otx2_nic *pf;
+ bool found = false;
+ u32 prefix_len;
+
+ i6dev = __in6_dev_get(dev);
+ if (!i6dev)
+ return NOTIFY_DONE;
+
+ /* addr_list is RCU-protected; hold rcu_read_lock() while walking it.
+ * Address updates from SLAAC/DAD may occur under idev->lock without
+ * RTNL, so the list must be read with an RCU-safe helper.
+ */
+ rcu_read_lock();
+ /* Switch offload supports a single IPv6 address per interface for now.
+ * Skip link-local entries and use the first global address on the list.
+ */
+ list_for_each_entry_rcu(ifp, &i6dev->addr_list, if_list) {
+ if (ipv6_addr_type(&ifp->addr) & IPV6_ADDR_LINKLOCAL)
+ continue;
+
+ addr = ifp->addr;
+ prefix_len = ifp->prefix_len;
+ found = true;
+ break;
+ }
+ rcu_read_unlock();
+
+ if (!found)
+ return NOTIFY_DONE;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_KERNEL);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ pf_dev = sw_nb_resolve_pf_dev(dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ entry->cmd = sw_nb_inetaddr_event_to_otx2_event(event, dev);
+ memcpy(entry->dst6, &addr, sizeof(entry->dst6));
+ entry->dst6_plen = prefix_len;
+ entry->host = 1;
+ entry->ipv6 = 1;
+
+ pf = netdev_priv(pf_dev);
+ entry->port_id = pf->pcifunc;
+
+ rcu_read_lock();
+ for_each_dev_addr(dev, dev_addr) {
+ entry->mac_valid = 1;
+ ether_addr_copy(entry->mac, dev_addr->addr);
+ break;
+ }
+ rcu_read_unlock();
+
+ netdev_dbg(dev, "netdev event addr=%pI6c plen=%u mac=%pM\n",
+ &addr, prefix_len, entry->mac);
+ kfree(entry);
+ return NOTIFY_DONE;
+}
+
+int sw_nb_v6_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct fib6_entry_notifier_info *f6_eni;
+ struct fib_notifier_info *info = ptr;
+ struct net_device *fib_dev, *pf_dev;
+ struct fib_entry *entry;
+ struct fib6_info *f6i;
+ struct neighbour *neigh;
+ struct fib6_nh *nh6;
+ struct rt6key *key;
+ struct otx2_nic *pf;
+
+ f6_eni = container_of(info, struct fib6_entry_notifier_info, info);
+ f6i = f6_eni->rt;
+
+ fib_dev = fib6_info_nh_dev(f6i);
+
+ if (!fib_dev)
+ return NOTIFY_DONE;
+
+ if (fib_dev->type != ARPHRD_ETHER)
+ return NOTIFY_DONE;
+
+ if (!sw_nb_is_valid_dev(fib_dev))
+ return NOTIFY_DONE;
+
+ if (f6i->fib6_type != RTN_UNICAST)
+ return NOTIFY_DONE;
+
+ key = &f6i->fib6_dst;
+ /* TODO: vlan and bridge support */
+ if (ipv6_addr_type(&key->addr) & IPV6_ADDR_LINKLOCAL)
+ return NOTIFY_DONE;
+
+ netdev_dbg(fib_dev, "fib6dst rt6key.addr=%pI6c len=%d\n", &key->addr,
+ key->plen);
+
+ netdev_dbg(fib_dev, "fib6flags=%#x proto=%u type=%u\n",
+ f6i->fib6_flags, f6i->fib6_protocol, f6i->fib6_type);
+
+ nh6 = f6i->nh ? nexthop_fib6_nh(f6i->nh) : f6i->fib6_nh;
+ if (nh6->fib_nh_gw_family != AF_INET6)
+ return NOTIFY_DONE;
+
+ netdev_dbg(nh6->fib_nh_dev ? nh6->fib_nh_dev : fib_dev,
+ "nh family=%u dev=%s gw=%pI6c gwfamily=%u\n",
+ nh6->fib_nh_family,
+ nh6->fib_nh_dev ? nh6->fib_nh_dev->name : "No dev",
+ &nh6->fib_nh_gw6, nh6->fib_nh_gw_family);
+
+ pf_dev = sw_nb_resolve_pf_dev(fib_dev);
+ if (!pf_dev)
+ return NOTIFY_DONE;
+
+ pf = netdev_priv(pf_dev);
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ entry->cmd = sw_nb_fib_event_to_otx2_event(event, fib_dev);
+ entry->ipv6 = 1;
+ entry->port_id = pf->pcifunc;
+ memcpy(entry->dst6, &key->addr, sizeof(entry->dst6));
+ entry->dst6_plen = key->plen;
+
+ memcpy(entry->gw6, &nh6->fib_nh_gw6, sizeof(nh6->fib_nh_gw6));
+ entry->gw_valid = !!(ipv6_addr_type(&nh6->fib_nh_gw6) & IPV6_ADDR_UNICAST);
+
+ /* TODO: No replay mechanism yet when the gateway neighbor is unresolved.
+ * If ip_neigh_gw6() returns NULL the route is skipped here; add replay
+ * from the neighbor update handler once nexthop resolution completes.
+ */
+ rcu_read_lock();
+ neigh = ip_neigh_gw6(fib_dev, &nh6->fib_nh_gw6);
+ if (!neigh || IS_ERR(neigh)) {
+ rcu_read_unlock();
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ neigh_ha_snapshot(entry->mac, neigh, fib_dev);
+ if (is_valid_ether_addr(entry->mac)) {
+ entry->mac_valid = 1;
+ netdev_dbg(fib_dev, "fib found MAC=%pM\n", entry->mac);
+ }
+
+ rcu_read_unlock();
+ kfree(entry);
+
+ return NOTIFY_DONE;
+}
+
+int sw_nb_net_v6_neigh_update(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct net_device *pf_dev;
+ struct neighbour *n = ptr;
+ struct fib_entry *entry;
+ struct otx2_nic *pf;
+
+ if (n->tbl != &nd_tbl)
+ return NOTIFY_DONE;
+
+ if (ipv6_addr_type((struct in6_addr *)n->primary_key) & IPV6_ADDR_LINKLOCAL)
+ return NOTIFY_DONE;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ pf_dev = sw_nb_resolve_pf_dev(n->dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ pf = netdev_priv(pf_dev);
+
+ entry->cmd = OTX2_NEIGH_UPDATE;
+ entry->dst6_plen = n->tbl->key_len * 8;
+ memcpy(entry->dst6, (struct in6_addr *)n->primary_key,
+ sizeof(entry->dst6));
+ entry->ipv6 = 1;
+ entry->nud_state = n->nud_state;
+ neigh_ha_snapshot(entry->mac, n, n->dev);
+ entry->mac_valid = 1;
+ entry->port_id = pf->pcifunc;
+
+ netdev_dbg(n->dev, "v6 neigh update %pI6c mac=%pM plen=%u\n",
+ (struct in6_addr *)n->primary_key, entry->mac,
+ n->tbl->key_len * 8);
+ kfree(entry);
+
+ return NOTIFY_DONE;
+}
+
+int sw_nb_v6_inetaddr_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ struct inet6_ifaddr *ifa6 = (struct inet6_ifaddr *)ptr;
+ struct net_device *dev = ifa6->idev->dev;
+ struct netdev_hw_addr *dev_addr;
+ struct net_device *pf_dev;
+ struct fib_entry *entry;
+ struct otx2_nic *pf;
+
+ if (event != NETDEV_CHANGE &&
+ event != NETDEV_UP &&
+ event != NETDEV_DOWN) {
+ return NOTIFY_DONE;
+ }
+
+ if (dev->type != ARPHRD_ETHER)
+ return NOTIFY_DONE;
+
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ if (ipv6_addr_type(&ifa6->addr) & IPV6_ADDR_LINKLOCAL)
+ return NOTIFY_DONE;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return NOTIFY_DONE;
+
+ pf_dev = sw_nb_resolve_pf_dev(dev);
+ if (!pf_dev) {
+ kfree(entry);
+ return NOTIFY_DONE;
+ }
+
+ pf = netdev_priv(pf_dev);
+
+ entry->cmd = sw_nb_inetaddr_event_to_otx2_event(event, dev);
+ memcpy(entry->dst6, &ifa6->addr, sizeof(entry->dst6));
+ entry->dst6_plen = ifa6->prefix_len;
+ entry->mac_valid = 1;
+ entry->host = 1;
+ entry->ipv6 = 1;
+ entry->port_id = pf->pcifunc;
+
+ rcu_read_lock();
+ for_each_dev_addr(dev, dev_addr) {
+ ether_addr_copy(entry->mac, dev_addr->addr);
+ entry->mac_valid = 1;
+ break;
+ }
+ rcu_read_unlock();
+
+ netdev_dbg(dev, "inetaddr addr=%pI6c len=%u %pM\n",
+ &ifa6->addr, ifa6->prefix_len, entry->mac);
+ kfree(entry);
+
+ return NOTIFY_DONE;
+}
+#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
new file mode 100644
index 000000000000..f73efc98c311
--- /dev/null
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Marvell switch driver
+ *
+ * Copyright (C) 2026 Marvell.
+ *
+ */
+#ifndef SW_NB_V6_H_
+#define SW_NB_V6_H_
+
+int sw_nb_v6_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_net_v6_neigh_update(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_v6_inetaddr_event(struct notifier_block *nb,
+ unsigned long event, void *ptr);
+
+int sw_nb_v6_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr);
+#endif // SW_NB_V6_H__
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
` (5 preceding siblings ...)
2026-08-31 13:19 ` [PATCH v9 net-next 6/8] octeontx2-pf: switch: Register notifiers for switch offload Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,7/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox Ratheesh Kannoth
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Handle switchdev FDB add and delete notifications on the PF by queuing
work that sends fdb_notify mailbox messages to the AF. The AF queues
those updates and pushes L2 rules toward the switchdev image with
af2swdev notify messages when firmware is ready.
Teach the AF swdev2af path to initialize L2 offload workqueues on
firmware up/down and to accept refresh requests that enqueue FDB
entries for AF to PF mailbox delivery. Add an AF to PF (and VF) upstream
message for FDB refresh, handle it in the VF driver, and treat it like
the CGX link event when acknowledging mailbox completion in the AF.
On refresh, invoke the switchdev notifier so the host bridge can learn
the updated FDB entry.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../net/ethernet/marvell/octeontx2/af/mbox.h | 2 +
.../net/ethernet/marvell/octeontx2/af/rvu.c | 3 +
.../marvell/octeontx2/af/switch/rvu_sw.c | 59 +-
.../marvell/octeontx2/af/switch/rvu_sw.h | 2 +
.../marvell/octeontx2/af/switch/rvu_sw_l2.c | 608 ++++++++++++++++++
.../marvell/octeontx2/af/switch/rvu_sw_l2.h | 4 +
.../ethernet/marvell/octeontx2/nic/otx2_pf.c | 2 +
.../ethernet/marvell/octeontx2/nic/otx2_vf.c | 49 ++
.../marvell/octeontx2/nic/switch/sw_fdb.c | 262 +++++++-
.../marvell/octeontx2/nic/switch/sw_fdb.h | 3 +
.../marvell/octeontx2/nic/switch/sw_nb.c | 12 +-
.../marvell/octeontx2/nic/switch/sw_nb.h | 8 +-
12 files changed, 1004 insertions(+), 10 deletions(-)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index 2aa1aa6599a5..8f7b2962a212 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -2015,6 +2015,7 @@ struct af2pf_fdb_refresh_req {
struct mbox_msghdr hdr;
u16 pcifunc;
u8 mac[6];
+ u64 flags;
};
struct iface_info {
@@ -2054,6 +2055,7 @@ struct fl_info {
struct swdev2af_notify_req {
struct mbox_msghdr hdr;
u64 msg_type;
+/* Mutually exclusive message selectors (not a combinable bitmask). */
#define SWDEV2AF_MSG_TYPE_FW_STATUS BIT_ULL(0)
#define SWDEV2AF_MSG_TYPE_REFRESH_FDB BIT_ULL(1)
#define SWDEV2AF_MSG_TYPE_REFRESH_FL BIT_ULL(2)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
index e4d13adc2896..9d0c99d4c064 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
@@ -23,6 +23,7 @@
#include "cn20k/reg.h"
#include "cn20k/api.h"
#include "cn20k/npc.h"
+#include "switch/rvu_sw.h"
#define DRV_NAME "rvu_af"
#define DRV_STRING "Marvell OcteonTX2 RVU Admin Function Driver"
@@ -3863,8 +3864,10 @@ static void rvu_remove(struct pci_dev *pdev)
rvu_cgx_exit(rvu);
rvu_fwdata_exit(rvu);
rvu_mcs_exit(rvu);
+ rvu_sw_shutdown();
rvu_mbox_destroy(&rvu->afpf_wq_info);
rvu_disable_sriov(rvu);
+ rvu_sw_clear_shutdown();
rvu_reset_all_blocks(rvu);
rvu_free_hw_resources(rvu);
rvu_clear_rvum_blk_revid(rvu);
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
index 2451eb57ec4c..71f113bded5e 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
@@ -9,6 +9,8 @@
#include "rvu.h"
#include "rvu_sw.h"
+#include "rvu_sw_l2.h"
+#include "rvu_sw_fl.h"
/*
* rep_cnt and rep2pfvf_map are populated once when the representor driver
@@ -33,9 +35,64 @@ u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc)
FIELD_PREP(GENMASK_ULL(15, 0), pcifunc);
}
+static bool rvu_sw_swdev2af_msg_valid(u64 msg_type)
+{
+ return msg_type == SWDEV2AF_MSG_TYPE_FW_STATUS ||
+ msg_type == SWDEV2AF_MSG_TYPE_REFRESH_FDB ||
+ msg_type == SWDEV2AF_MSG_TYPE_REFRESH_FL;
+}
+
+static int rvu_sw_swdev2af_sender_check(struct rvu *rvu,
+ struct swdev2af_notify_req *req,
+ u64 msg_type)
+{
+ u16 sender = req->hdr.pcifunc;
+
+ if (!rvu_sw_swdev2af_msg_valid(msg_type))
+ return -EINVAL;
+
+ if (msg_type == SWDEV2AF_MSG_TYPE_FW_STATUS && req->fw_up)
+ return 0;
+
+ if (!rvu_is_switch_pcifunc(rvu, sender))
+ return -EPERM;
+
+ return 0;
+}
+
int rvu_mbox_handler_swdev2af_notify(struct rvu *rvu,
struct swdev2af_notify_req *req,
struct msg_rsp *rsp)
{
- return 0;
+ int rc;
+
+ rc = rvu_sw_swdev2af_sender_check(rvu, req, req->msg_type);
+ if (rc)
+ return rc;
+
+ switch (req->msg_type) {
+ case SWDEV2AF_MSG_TYPE_FW_STATUS:
+ rc = rvu_sw_l2_init_offl_wq(rvu, req->hdr.pcifunc, req->fw_up);
+ break;
+
+ case SWDEV2AF_MSG_TYPE_REFRESH_FDB:
+ rc = rvu_sw_l2_fdb_list_entry_add(rvu, req->pcifunc, req->mac);
+ break;
+
+ default:
+ rc = -EOPNOTSUPP;
+ break;
+ }
+
+ return rc;
+}
+
+void rvu_sw_shutdown(void)
+{
+ rvu_sw_l2_shutdown();
+}
+
+void rvu_sw_clear_shutdown(void)
+{
+ rvu_sw_l2_clear_shutdown();
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
index e9ad32c84576..539af01d917e 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.h
@@ -12,5 +12,7 @@
#define RVU_SW_INVALID_PORT_ID ((u32)~0U)
u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc);
+void rvu_sw_shutdown(void);
+void rvu_sw_clear_shutdown(void);
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
index 5f805bfa81ed..448a442a6ffb 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
@@ -4,11 +4,619 @@
* Copyright (C) 2026 Marvell.
*
*/
+
+#include <linux/bitfield.h>
#include "rvu.h"
+#include "rvu_sw.h"
+#include "rvu_sw_l2.h"
+
+#define M(_name, _id, _fn_name, _req_type, _rsp_type) \
+static struct _req_type __maybe_unused \
+*otx2_mbox_alloc_msg_ ## _fn_name(struct rvu *rvu, int devid) \
+{ \
+ struct _req_type *req; \
+ \
+ req = (struct _req_type *)otx2_mbox_alloc_msg_rsp( \
+ &rvu->afpf_wq_info.mbox_up, devid, sizeof(struct _req_type), \
+ sizeof(struct _rsp_type)); \
+ if (!req) \
+ return NULL; \
+ req->hdr.sig = OTX2_MBOX_REQ_SIG; \
+ req->hdr.id = _id; \
+ return req; \
+}
+
+MBOX_UP_AF2SWDEV_MESSAGES
+MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES
+#undef M
+
+#define RVU_SW_L2_LIST_MAX 4096
+
+struct l2_entry {
+ struct list_head list;
+ u64 flags;
+ u32 port_id;
+ u8 mac[ETH_ALEN];
+};
+
+static bool going_down;
+/* Serialize going_down, control workqueue alloc, and queue_work() so
+ * teardown cannot NULL the workqueue while a mailbox handler is between
+ * the going_down check and queue_work().
+ */
+static DEFINE_MUTEX(rvu_sw_l2_ctrl_lock);
+
+static DEFINE_MUTEX(l2_offl_list_lock);
+static LIST_HEAD(l2_offl_lh);
+static atomic_t l2_offl_list_cnt = ATOMIC_INIT(0);
+
+static DEFINE_MUTEX(fdb_refresh_list_lock);
+static LIST_HEAD(fdb_refresh_lh);
+static atomic_t fdb_refresh_list_cnt = ATOMIC_INIT(0);
+
+struct rvu_sw_l2_work {
+ struct rvu *rvu;
+ struct work_struct work;
+};
+
+struct rvu_sw_l2_ctrl_work {
+ struct work_struct work;
+ struct rvu *rvu;
+ u16 pcifunc;
+ bool fw_up;
+};
+
+/* Work queue for switchdev message handling. There is only one RVU AF
+ * and one switch block per SoC; rvu_probe() enforces a single AF bind via
+ * device_bound, so one global workqueue instance per type is sufficient.
+ */
+static struct rvu_sw_l2_work l2_offl_work;
+static struct workqueue_struct *rvu_sw_l2_offl_wq;
+
+static struct rvu_sw_l2_work fdb_refresh_work;
+static struct workqueue_struct *fdb_refresh_wq;
+
+/* Serialize FW bring-up/teardown outside the AF mailbox handler. The
+ * handler runs under rvu->mbox_lock, while offload/refresh workers take
+ * the same lock to send messages; synchronous teardown there deadlocks.
+ */
+static struct workqueue_struct *rvu_sw_l2_ctrl_wq;
+
+static bool fw_is_up;
+static DEFINE_SPINLOCK(rvu_sw_l2_state_lock);
+
+static void rvu_sw_l2_list_cnt_warn(struct device *dev, atomic_t *cnt,
+ const char *name)
+{
+ int n = atomic_read(cnt);
+
+ if (n < 0)
+ dev_warn(dev, "L2 %s list count underflow: %d\n", name, n);
+ else if (n > RVU_SW_L2_LIST_MAX)
+ dev_warn(dev, "L2 %s list count overflow: %d (max %d)\n",
+ name, n, RVU_SW_L2_LIST_MAX);
+}
+
+static void rvu_sw_l2_list_cnt_inc(struct device *dev, atomic_t *cnt,
+ const char *name)
+{
+ atomic_inc(cnt);
+ rvu_sw_l2_list_cnt_warn(dev, cnt, name);
+}
+
+static void rvu_sw_l2_list_cnt_dec(struct device *dev, atomic_t *cnt,
+ const char *name)
+{
+ atomic_dec(cnt);
+ rvu_sw_l2_list_cnt_warn(dev, cnt, name);
+}
+
+static void rvu_sw_l2_destroy_wqs(struct rvu *rvu)
+{
+ struct workqueue_struct *offl_wq, *refresh_wq;
+ struct l2_entry *entry;
+
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ rvu->rswitch.flags &= ~RVU_SWITCH_FLAG_FW_READY;
+ fw_is_up = false;
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+
+ mutex_lock(&fdb_refresh_list_lock);
+ refresh_wq = fdb_refresh_wq;
+ fdb_refresh_wq = NULL;
+ mutex_unlock(&fdb_refresh_list_lock);
+
+ if (refresh_wq) {
+ cancel_work_sync(&fdb_refresh_work.work);
+ destroy_workqueue(refresh_wq);
+
+ mutex_lock(&fdb_refresh_list_lock);
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &fdb_refresh_list_cnt,
+ "fdb refresh");
+ while (1) {
+ entry = list_first_entry_or_null(&fdb_refresh_lh,
+ struct l2_entry, list);
+ if (!entry)
+ break;
+
+ list_del_init(&entry->list);
+ kfree(entry);
+ }
+ atomic_set(&fdb_refresh_list_cnt, 0);
+ mutex_unlock(&fdb_refresh_list_lock);
+ }
+
+ mutex_lock(&l2_offl_list_lock);
+ offl_wq = rvu_sw_l2_offl_wq;
+ rvu_sw_l2_offl_wq = NULL;
+ mutex_unlock(&l2_offl_list_lock);
+
+ if (offl_wq) {
+ cancel_work_sync(&l2_offl_work.work);
+ destroy_workqueue(offl_wq);
+
+ mutex_lock(&l2_offl_list_lock);
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &l2_offl_list_cnt, "offload");
+ while (1) {
+ entry = list_first_entry_or_null(&l2_offl_lh,
+ struct l2_entry, list);
+ if (!entry)
+ break;
+
+ list_del_init(&entry->list);
+ kfree(entry);
+ }
+ atomic_set(&l2_offl_list_cnt, 0);
+ mutex_unlock(&l2_offl_list_lock);
+ }
+
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ rvu->rswitch.pcifunc = 0;
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+}
+
+/* High-frequency link state transitions or aggressive FDB
+ * aging intervals can induce rapid fdb churn. To prevent
+ * thrashing, inhibit hardware offloading of these transient
+ * forwarding states to the switching ASIC. Events are queued
+ * at the tail and processed from the head; when enqueueing a
+ * new operation, drop older pending opposite operations for the
+ * same MAC and port that have not yet reached hardware. When an
+ * opposite entry is removed, the new operation is dropped as well.
+ */
+static bool rvu_sw_l2_offl_coalesce_pending_locked(struct rvu *rvu,
+ struct l2_entry *new_entry)
+{
+ u64 opposite = (new_entry->flags & OTX2_FDB_ADD) ? OTX2_FDB_DEL : OTX2_FDB_ADD;
+ struct l2_entry *entry, *tmp;
+ bool coalesced = false;
+
+ lockdep_assert_held(&l2_offl_list_lock);
+
+ list_for_each_entry_safe(entry, tmp, &l2_offl_lh, list) {
+ if (!ether_addr_equal(new_entry->mac, entry->mac))
+ continue;
+
+ if (new_entry->port_id != entry->port_id)
+ continue;
+
+ if (!(entry->flags & opposite))
+ continue;
+
+ list_del_init(&entry->list);
+ rvu_sw_l2_list_cnt_dec(rvu->dev, &l2_offl_list_cnt, "offload");
+ kfree(entry);
+ coalesced = true;
+ }
+
+ return coalesced;
+}
+
+static int rvu_sw_l2_offl_rule_push(struct rvu *rvu, struct l2_entry *l2_entry)
+{
+ struct af2swdev_notify_req *req;
+ int swdev_pf;
+
+ swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
+
+ mutex_lock(&rvu->mbox_lock);
+ req = otx2_mbox_alloc_msg_af2swdev_notify(rvu, swdev_pf);
+ if (!req) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -ENOMEM;
+ }
+
+ ether_addr_copy(req->mac, l2_entry->mac);
+ req->flags = l2_entry->flags;
+ req->port_id = l2_entry->port_id;
+
+ otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, swdev_pf);
+ otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, swdev_pf);
+
+ mutex_unlock(&rvu->mbox_lock);
+ return 0;
+}
+
+static int rvu_sw_l2_fdb_refresh_send(struct rvu *rvu, u16 pcifunc, u8 *mac)
+{
+ struct af2pf_fdb_refresh_req *req;
+ int pf, vf;
+
+ if (!is_pf_func_valid(rvu, pcifunc))
+ return -EINVAL;
+
+ pf = rvu_get_pf(rvu->pdev, pcifunc);
+ vf = (pcifunc & RVU_PFVF_FUNC_MASK) - 1;
+
+ mutex_lock(&rvu->mbox_lock);
+
+ /*
+ * FDB refresh topology (VM bridge + SR-IOV VF ports + HW offload):
+ *
+ * VM: br0 with eth0/eth1 (CGX VFs passed through via SR-IOV)
+ * Host: switch HW forwards between VF switch ports; accelerated
+ * traffic is not received on eth0/eth1 in the VM.
+ *
+ * br0 still maintains a software FDB with ageing. After the AF
+ * programs hardware, refresh must reach the VM so
+ * SWITCHDEV_FDB_ADD_TO_BRIDGE is raised on the bridge port netdev
+ * (eth0/eth1), keeping br0 FDB entries alive.
+ *
+ * For CGX VFs (pf != 0), hdr.pcifunc carries the target VF
+ * identity so the parent PF mailbox forwards the message to the
+ * guest VF driver (otx2_pfaf_mbox_up_handler()). Routing refresh
+ * into the VM is intentional: the bridge owning the FDB is in the
+ * guest, not on a host representor.
+ *
+ * This differs from the host-side switchdev model where a Linux
+ * bridge on the host uses representor netdevs (RVU_REP) as ports.
+ *
+ * AF-managed VFs on PF 0 (e.g. LBK, pf == 0): delivered directly
+ * via the AF VF mailbox; these VFs have no host PF mailbox.
+ */
+ if (pf != 0) {
+ if (pf >= rvu->afpf_wq_info.mbox_up.ndevs) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -EINVAL;
+ }
+
+ req = otx2_mbox_alloc_msg_af2pf_fdb_refresh(rvu, pf);
+ if (!req) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -ENOMEM;
+ }
+
+ req->hdr.pcifunc = pcifunc;
+ ether_addr_copy(req->mac, mac);
+ req->pcifunc = pcifunc;
+ req->flags = OTX2_FDB_ADD;
+
+ otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, pf);
+ otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, pf);
+ } else {
+ if (vf < 0 || vf >= rvu->afvf_wq_info.mbox_up.ndevs) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -EINVAL;
+ }
+
+ req = (struct af2pf_fdb_refresh_req *)
+ otx2_mbox_alloc_msg_rsp(&rvu->afvf_wq_info.mbox_up, vf,
+ sizeof(*req), sizeof(struct msg_rsp));
+ if (!req) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -ENOMEM;
+ }
+ req->hdr.sig = OTX2_MBOX_REQ_SIG;
+ req->hdr.id = MBOX_MSG_AF2PF_FDB_REFRESH;
+
+ req->hdr.pcifunc = pcifunc;
+ ether_addr_copy(req->mac, mac);
+ req->pcifunc = pcifunc;
+ req->flags = OTX2_FDB_ADD;
+
+ otx2_mbox_wait_for_zero(&rvu->afvf_wq_info.mbox_up, vf);
+ otx2_mbox_msg_send_up(&rvu->afvf_wq_info.mbox_up, vf);
+ }
+
+ mutex_unlock(&rvu->mbox_lock);
+
+ return 0;
+}
+
+static void rvu_sw_l2_fdb_refresh_wq_handler(struct work_struct *work)
+{
+ struct rvu_sw_l2_work *fdb_work;
+ struct l2_entry *l2_entry;
+
+ fdb_work = container_of(work, struct rvu_sw_l2_work, work);
+
+ while (1) {
+ mutex_lock(&fdb_refresh_list_lock);
+ l2_entry = list_first_entry_or_null(&fdb_refresh_lh,
+ struct l2_entry, list);
+ if (!l2_entry) {
+ mutex_unlock(&fdb_refresh_list_lock);
+ return;
+ }
+
+ list_del_init(&l2_entry->list);
+ rvu_sw_l2_list_cnt_dec(fdb_work->rvu->dev, &fdb_refresh_list_cnt,
+ "fdb refresh");
+ mutex_unlock(&fdb_refresh_list_lock);
+
+ rvu_sw_l2_fdb_refresh_send(fdb_work->rvu, l2_entry->port_id,
+ l2_entry->mac);
+ kfree(l2_entry);
+ }
+}
+
+static void rvu_sw_l2_offl_rule_wq_handler(struct work_struct *work)
+{
+ struct rvu_sw_l2_work *offl_work;
+ struct l2_entry *l2_entry;
+ int budget = 16;
+
+ offl_work = container_of(work, struct rvu_sw_l2_work, work);
+
+ while (budget--) {
+ mutex_lock(&l2_offl_list_lock);
+ l2_entry = list_first_entry_or_null(&l2_offl_lh, struct l2_entry, list);
+ if (!l2_entry) {
+ mutex_unlock(&l2_offl_list_lock);
+ return;
+ }
+
+ list_del_init(&l2_entry->list);
+ rvu_sw_l2_list_cnt_dec(offl_work->rvu->dev, &l2_offl_list_cnt,
+ "offload");
+ mutex_unlock(&l2_offl_list_lock);
+
+ if (rvu_sw_l2_offl_rule_push(offl_work->rvu, l2_entry))
+ dev_err(offl_work->rvu->dev,
+ "%s: Error to push l2 rule\n",
+ __func__);
+ /*
+ * TODO: Requeue l2_entry on transient rvu_sw_l2_offl_rule_push()
+ * errors (e.g. ENOMEM, -EBUSY) to keep hardware FDB in sync with
+ * the bridge. Drop-on-failure is known deferred work.
+ */
+ kfree(l2_entry);
+ }
+
+ mutex_lock(&l2_offl_list_lock);
+ if (rvu_sw_l2_offl_wq && atomic_read(&l2_offl_list_cnt))
+ queue_work(rvu_sw_l2_offl_wq, &l2_offl_work.work);
+ mutex_unlock(&l2_offl_list_lock);
+}
+
+static void rvu_sw_l2_ctrl_work_handler(struct work_struct *work)
+{
+ struct rvu_sw_l2_ctrl_work *ctrl;
+ struct rvu_switch *rswitch;
+ struct rvu *rvu;
+ u16 pcifunc;
+ bool fw_up;
+
+ ctrl = container_of(work, struct rvu_sw_l2_ctrl_work, work);
+ rvu = ctrl->rvu;
+ pcifunc = ctrl->pcifunc;
+ fw_up = ctrl->fw_up;
+ kfree(ctrl);
+
+ rswitch = &rvu->rswitch;
+
+ if (!fw_up) {
+ rvu_sw_l2_destroy_wqs(rvu);
+ return;
+ }
+
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ if (fw_is_up && rvu_sw_l2_offl_wq && fdb_refresh_wq) {
+ rswitch->pcifunc = pcifunc;
+ rswitch->flags |= RVU_SWITCH_FLAG_FW_READY;
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+ return;
+ }
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+
+ if (rvu_sw_l2_offl_wq || fdb_refresh_wq)
+ rvu_sw_l2_destroy_wqs(rvu);
+
+ l2_offl_work.rvu = rvu;
+ INIT_WORK(&l2_offl_work.work, rvu_sw_l2_offl_rule_wq_handler);
+ rvu_sw_l2_offl_wq = alloc_workqueue("swdev_rvu_sw_l2_offl_wq", 0, 0);
+ if (!rvu_sw_l2_offl_wq) {
+ dev_err(rvu->dev, "L2 offl workqueue allocation failed\n");
+ return;
+ }
+
+ fdb_refresh_work.rvu = rvu;
+ INIT_WORK(&fdb_refresh_work.work, rvu_sw_l2_fdb_refresh_wq_handler);
+ fdb_refresh_wq = alloc_workqueue("swdev_fdb_refresh_wq", 0, 0);
+ if (!fdb_refresh_wq) {
+ dev_err(rvu->dev, "fdb refresh workqueue allocation failed\n");
+ destroy_workqueue(rvu_sw_l2_offl_wq);
+ rvu_sw_l2_offl_wq = NULL;
+ return;
+ }
+
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ fw_is_up = true;
+ rswitch->pcifunc = pcifunc;
+ rswitch->flags |= RVU_SWITCH_FLAG_FW_READY;
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+}
+
+int rvu_sw_l2_init_offl_wq(struct rvu *rvu, u16 pcifunc, bool fw_up)
+{
+ struct rvu_sw_l2_ctrl_work *ctrl;
+ struct workqueue_struct *wq;
+ int err = 0;
+
+ mutex_lock(&rvu_sw_l2_ctrl_lock);
+ if (going_down)
+ goto unlock;
+
+ if (!fw_up) {
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ rvu->rswitch.flags &= ~RVU_SWITCH_FLAG_FW_READY;
+ fw_is_up = false;
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+ }
+
+ if (!rvu_sw_l2_ctrl_wq) {
+ rvu_sw_l2_ctrl_wq = alloc_ordered_workqueue("rvu_sw_l2_ctrl",
+ WQ_MEM_RECLAIM);
+ if (!rvu_sw_l2_ctrl_wq) {
+ err = -ENOMEM;
+ goto unlock;
+ }
+ }
+ wq = rvu_sw_l2_ctrl_wq;
+
+ ctrl = kzalloc_obj(*ctrl);
+ if (!ctrl) {
+ err = -ENOMEM;
+ goto unlock;
+ }
+
+ INIT_WORK(&ctrl->work, rvu_sw_l2_ctrl_work_handler);
+ ctrl->rvu = rvu;
+ ctrl->pcifunc = pcifunc;
+ ctrl->fw_up = fw_up;
+
+ queue_work(wq, &ctrl->work);
+
+unlock:
+ mutex_unlock(&rvu_sw_l2_ctrl_lock);
+ return err;
+}
+
+int rvu_sw_l2_fdb_list_entry_add(struct rvu *rvu, u16 pcifunc, u8 *mac)
+{
+ struct workqueue_struct *wq;
+ struct l2_entry *l2_entry;
+
+ if (!is_pf_func_valid(rvu, pcifunc))
+ return -EINVAL;
+
+ if (atomic_read(&fdb_refresh_list_cnt) >= RVU_SW_L2_LIST_MAX) {
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &fdb_refresh_list_cnt,
+ "fdb refresh");
+ return -ENOMEM;
+ }
+
+ l2_entry = kcalloc(1, sizeof(*l2_entry), GFP_KERNEL);
+ if (!l2_entry)
+ return -ENOMEM;
+
+ l2_entry->port_id = pcifunc;
+ ether_addr_copy(l2_entry->mac, mac);
+
+ mutex_lock(&fdb_refresh_list_lock);
+ wq = fdb_refresh_wq;
+ if (!wq) {
+ mutex_unlock(&fdb_refresh_list_lock);
+ kfree(l2_entry);
+ return -EINVAL;
+ }
+
+ if (atomic_read(&fdb_refresh_list_cnt) >= RVU_SW_L2_LIST_MAX) {
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &fdb_refresh_list_cnt,
+ "fdb refresh");
+ mutex_unlock(&fdb_refresh_list_lock);
+ kfree(l2_entry);
+ return -ENOMEM;
+ }
+ list_add_tail(&l2_entry->list, &fdb_refresh_lh);
+ rvu_sw_l2_list_cnt_inc(rvu->dev, &fdb_refresh_list_cnt, "fdb refresh");
+ queue_work(wq, &fdb_refresh_work.work);
+ mutex_unlock(&fdb_refresh_list_lock);
+
+ return 0;
+}
int rvu_mbox_handler_fdb_notify(struct rvu *rvu,
struct fdb_notify_req *req,
struct msg_rsp *rsp)
{
+ struct workqueue_struct *wq;
+ struct l2_entry *l2_entry;
+ u32 port_id;
+
+ spin_lock_bh(&rvu_sw_l2_state_lock);
+ if (!(rvu->rswitch.flags & RVU_SWITCH_FLAG_FW_READY)) {
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+ return 0;
+ }
+ spin_unlock_bh(&rvu_sw_l2_state_lock);
+
+ port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
+ if (port_id == RVU_SW_INVALID_PORT_ID)
+ return -EINVAL;
+
+ if (atomic_read(&l2_offl_list_cnt) >= RVU_SW_L2_LIST_MAX) {
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &l2_offl_list_cnt, "offload");
+ return -ENOMEM;
+ }
+
+ l2_entry = kcalloc(1, sizeof(*l2_entry), GFP_KERNEL);
+ if (!l2_entry)
+ return -ENOMEM;
+
+ l2_entry->port_id = port_id;
+ ether_addr_copy(l2_entry->mac, req->mac);
+ l2_entry->flags = req->flags;
+
+ mutex_lock(&l2_offl_list_lock);
+ wq = rvu_sw_l2_offl_wq;
+ if (!wq) {
+ mutex_unlock(&l2_offl_list_lock);
+ kfree(l2_entry);
+ return 0;
+ }
+
+ if (atomic_read(&l2_offl_list_cnt) >= RVU_SW_L2_LIST_MAX) {
+ rvu_sw_l2_list_cnt_warn(rvu->dev, &l2_offl_list_cnt, "offload");
+ mutex_unlock(&l2_offl_list_lock);
+ kfree(l2_entry);
+ return -ENOMEM;
+ }
+ if (rvu_sw_l2_offl_coalesce_pending_locked(rvu, l2_entry)) {
+ mutex_unlock(&l2_offl_list_lock);
+ kfree(l2_entry);
+ return 0;
+ }
+ list_add_tail(&l2_entry->list, &l2_offl_lh);
+ rvu_sw_l2_list_cnt_inc(rvu->dev, &l2_offl_list_cnt, "offload");
+ queue_work(wq, &l2_offl_work.work);
+ mutex_unlock(&l2_offl_list_lock);
+
return 0;
}
+
+void rvu_sw_l2_shutdown(void)
+{
+ struct workqueue_struct *wq;
+
+ mutex_lock(&rvu_sw_l2_ctrl_lock);
+ going_down = true;
+ wq = rvu_sw_l2_ctrl_wq;
+ rvu_sw_l2_ctrl_wq = NULL;
+ mutex_unlock(&rvu_sw_l2_ctrl_lock);
+
+ if (wq) {
+ flush_workqueue(wq);
+ destroy_workqueue(wq);
+ }
+
+ if (fdb_refresh_wq || rvu_sw_l2_offl_wq)
+ rvu_sw_l2_destroy_wqs(l2_offl_work.rvu);
+}
+
+void rvu_sw_l2_clear_shutdown(void)
+{
+ mutex_lock(&rvu_sw_l2_ctrl_lock);
+ going_down = false;
+ mutex_unlock(&rvu_sw_l2_ctrl_lock);
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
index ff28612150c9..3c250272e026 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.h
@@ -8,4 +8,8 @@
#ifndef RVU_SW_L2_H
#define RVU_SW_L2_H
+int rvu_sw_l2_init_offl_wq(struct rvu *rvu, u16 pcifunc, bool fw_up);
+int rvu_sw_l2_fdb_list_entry_add(struct rvu *rvu, u16 pcifunc, u8 *mac);
+void rvu_sw_l2_shutdown(void);
+void rvu_sw_l2_clear_shutdown(void);
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
index c995f2900859..6ee19aca194c 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c
@@ -28,6 +28,7 @@
#include <rvu_trace.h>
#include "cn10k_ipsec.h"
#include "otx2_xsk.h"
+#include "switch/sw_nb.h"
#define DRV_NAME "rvu_nicpf"
#define DRV_STRING "Marvell RVU NIC Physical Function Driver"
@@ -993,6 +994,7 @@ static int otx2_process_mbox_msg_up(struct otx2_nic *pf,
MBOX_UP_CGX_MESSAGES
MBOX_UP_MCS_MESSAGES
MBOX_UP_REP_MESSAGES
+MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES
#undef M
break;
default:
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
index fcdf891f90b5..1d86cac5a7e8 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
@@ -9,6 +9,7 @@
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/net_tstamp.h>
+#include <net/switchdev.h>
#include "otx2_common.h"
#include "otx2_reg.h"
@@ -114,6 +115,38 @@ static void otx2vf_vfaf_mbox_handler(struct work_struct *work)
}
}
+#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+static int otx2vf_mbox_af2pf_fdb_refresh(struct otx2_nic *vf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp)
+{
+ struct switchdev_notifier_fdb_info item = {0};
+
+ /* VM bridge + HW offload: vf->netdev is a br0 port in the guest.
+ * SWITCHDEV_FDB_ADD_TO_BRIDGE on this netdev refreshes the guest
+ * bridge FDB even when accelerated traffic bypasses eth0/eth1 RX
+ * (see rvu_sw_l2_fdb_refresh_send()).
+ */
+ item.addr = req->mac;
+ item.info.dev = vf->netdev;
+ if (req->flags & OTX2_FDB_DEL)
+ call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_BRIDGE,
+ item.info.dev, &item.info, NULL);
+ else
+ call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE,
+ item.info.dev, &item.info, NULL);
+
+ return 0;
+}
+#else
+static int otx2vf_mbox_af2pf_fdb_refresh(struct otx2_nic *vf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
+#endif
+
static int otx2vf_process_mbox_msg_up(struct otx2_nic *vf,
struct mbox_msghdr *req)
{
@@ -141,6 +174,22 @@ static int otx2vf_process_mbox_msg_up(struct otx2_nic *vf,
err = otx2_mbox_up_handler_cgx_link_event(
vf, (struct cgx_link_info_msg *)req, rsp);
return err;
+
+ case MBOX_MSG_AF2PF_FDB_REFRESH:
+ rsp = (struct msg_rsp *)otx2_mbox_alloc_msg(&vf->mbox.mbox_up, 0,
+ sizeof(struct msg_rsp));
+ if (!rsp)
+ return -ENOMEM;
+
+ rsp->hdr.id = MBOX_MSG_AF2PF_FDB_REFRESH;
+ rsp->hdr.sig = OTX2_MBOX_RSP_SIG;
+ rsp->hdr.pcifunc = req->pcifunc;
+ rsp->hdr.rc = 0;
+ err = otx2vf_mbox_af2pf_fdb_refresh(vf,
+ (struct af2pf_fdb_refresh_req *)req,
+ rsp);
+ return err;
+
default:
otx2_reply_invalid_msg(&vf->mbox.mbox_up, 0, 0, req->id);
return -ENODEV;
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
index 500451e85b50..e5e20b08ee8e 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
@@ -4,16 +4,276 @@
* Copyright (C) 2026 Marvell.
*
*/
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <linux/pci.h>
+#include <net/switchdev.h>
+#include <net/netevent.h>
+#include <net/arp.h>
+
+#include "../otx2_reg.h"
+#include "../otx2_common.h"
+#include "../otx2_struct.h"
+#include "../cn10k.h"
+#include "../rep.h"
+#include "sw_nb.h"
#include "sw_fdb.h"
-#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+#if !IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+
+int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp)
+{
+ return 0;
+}
+
+#else
+
+#define SW_FDB_LIST_MAX 4096
+
+static DEFINE_SPINLOCK(sw_fdb_llock);
+static LIST_HEAD(sw_fdb_lh);
+static atomic_t sw_fdb_list_cnt = ATOMIC_INIT(0);
+
+struct sw_fdb_list_entry {
+ struct list_head list;
+ u64 flags;
+ struct pci_dev *pdev;
+ struct net_device *dev;
+ netdevice_tracker dev_tracker;
+ u8 mac[ETH_ALEN];
+ bool add_fdb;
+};
+
+static struct workqueue_struct *sw_fdb_wq;
+static struct work_struct sw_fdb_work;
+
+static void sw_fdb_list_cnt_warn(struct net_device *netdev)
+{
+ int n = atomic_read(&sw_fdb_list_cnt);
+
+ if (n < 0)
+ netdev_warn(netdev, "FDB list count underflow: %d\n", n);
+ else if (n > SW_FDB_LIST_MAX)
+ netdev_warn(netdev, "FDB list count overflow: %d (max %d)\n",
+ n, SW_FDB_LIST_MAX);
+}
+
+static int sw_fdb_list_count(void)
+{
+ return atomic_read(&sw_fdb_list_cnt);
+}
+
+static void sw_fdb_list_cnt_inc(struct net_device *netdev)
+{
+ atomic_inc(&sw_fdb_list_cnt);
+ sw_fdb_list_cnt_warn(netdev);
+}
+
+static void sw_fdb_list_cnt_dec(struct net_device *netdev)
+{
+ atomic_dec(&sw_fdb_list_cnt);
+ sw_fdb_list_cnt_warn(netdev);
+}
+
+static struct otx2_nic *sw_fdb_netdev_to_nic(struct net_device *dev)
+{
+ struct device *parent = dev->dev.parent;
+
+ if (parent && parent->bus == &pci_bus_type) {
+ struct pci_dev *pdev = to_pci_dev(parent);
+
+ if (otx2_rep_dev(pdev)) {
+ struct rep_dev *rep = netdev_priv(dev);
+
+ return rep->mdev;
+ }
+ }
+
+ return netdev_priv(dev);
+}
+
+static int sw_fdb_add_or_del(struct otx2_nic *pf,
+ const unsigned char *addr,
+ bool add_fdb)
+{
+ struct fdb_notify_req *req;
+ int rc;
+
+ mutex_lock(&pf->mbox.lock);
+ req = otx2_mbox_alloc_msg_fdb_notify(&pf->mbox);
+ if (!req) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ ether_addr_copy(req->mac, addr);
+ req->flags = add_fdb ? OTX2_FDB_ADD : OTX2_FDB_DEL;
+
+ rc = otx2_sync_mbox_msg(&pf->mbox);
+out:
+ mutex_unlock(&pf->mbox.lock);
+ return rc;
+}
+
+static void sw_fdb_entry_free(struct sw_fdb_list_entry *entry)
+{
+ netdev_put(entry->dev, &entry->dev_tracker);
+ pci_dev_put(entry->pdev);
+ kfree(entry);
+}
+
+static void sw_fdb_wq_handler(struct work_struct *work)
+{
+ struct sw_fdb_list_entry *entry;
+ struct otx2_nic *pf;
+ struct workqueue_struct *wq;
+ LIST_HEAD(tlist);
+
+ spin_lock(&sw_fdb_llock);
+ list_splice_init(&sw_fdb_lh, &tlist);
+ spin_unlock(&sw_fdb_llock);
+
+ while ((entry =
+ list_first_entry_or_null(&tlist,
+ struct sw_fdb_list_entry,
+ list)) != NULL) {
+ list_del_init(&entry->list);
+ sw_fdb_list_cnt_dec(entry->dev);
+
+ spin_lock(&sw_fdb_llock);
+ wq = sw_fdb_wq;
+ spin_unlock(&sw_fdb_llock);
+
+ pf = wq ? pci_get_drvdata(entry->pdev) : NULL;
+ if (pf && sw_fdb_add_or_del(pf, entry->mac, entry->add_fdb))
+ netdev_err(entry->dev,
+ "Error to add/del fdb %pM entry\n",
+ entry->mac);
+ /*
+ * TODO: Requeue entry on transient sw_fdb_add_or_del() failure so
+ * the switch FDB stays aligned with the bridge. Drop-on-failure
+ * is known deferred work.
+ */
+ sw_fdb_entry_free(entry);
+ }
+
+ spin_lock(&sw_fdb_llock);
+ wq = sw_fdb_wq;
+ if (wq && !list_empty(&sw_fdb_lh))
+ queue_work(wq, &sw_fdb_work);
+ spin_unlock(&sw_fdb_llock);
+}
+
+int sw_fdb_add_to_list(struct net_device *dev, u8 *mac, bool add_fdb)
+{
+ struct otx2_nic *pf = sw_fdb_netdev_to_nic(dev);
+ struct sw_fdb_list_entry *entry;
+ struct workqueue_struct *wq;
+
+ if (!pf)
+ return -EINVAL;
+
+ if (sw_fdb_list_count() >= SW_FDB_LIST_MAX)
+ return -ENOMEM;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ return -ENOMEM;
+
+ ether_addr_copy(entry->mac, mac);
+ entry->add_fdb = add_fdb;
+ entry->pdev = pci_dev_get(pf->pdev);
+ entry->dev = dev;
+ netdev_hold(dev, &entry->dev_tracker, GFP_ATOMIC);
+
+ spin_lock(&sw_fdb_llock);
+ wq = sw_fdb_wq;
+ if (!wq) {
+ spin_unlock(&sw_fdb_llock);
+ sw_fdb_entry_free(entry);
+ return -EINVAL;
+ }
+
+ if (sw_fdb_list_count() >= SW_FDB_LIST_MAX) {
+ spin_unlock(&sw_fdb_llock);
+ sw_fdb_entry_free(entry);
+ return -ENOMEM;
+ }
+
+ list_add_tail(&entry->list, &sw_fdb_lh);
+ sw_fdb_list_cnt_inc(dev);
+ queue_work(wq, &sw_fdb_work);
+ spin_unlock(&sw_fdb_llock);
+
+ return 0;
+}
+
int sw_fdb_init(void)
{
+ INIT_WORK(&sw_fdb_work, sw_fdb_wq_handler);
+ sw_fdb_wq = alloc_workqueue("sw_fdb_wq", 0, 0);
+ if (!sw_fdb_wq)
+ return -ENOMEM;
+
return 0;
}
void sw_fdb_deinit(void)
{
+ struct sw_fdb_list_entry *entry;
+ struct workqueue_struct *wq;
+ LIST_HEAD(tlist);
+
+ spin_lock(&sw_fdb_llock);
+ wq = sw_fdb_wq;
+ sw_fdb_wq = NULL;
+ spin_unlock(&sw_fdb_llock);
+
+ if (!wq)
+ return;
+
+ cancel_work_sync(&sw_fdb_work);
+ destroy_workqueue(wq);
+
+ spin_lock(&sw_fdb_llock);
+ list_splice_init(&sw_fdb_lh, &tlist);
+ spin_unlock(&sw_fdb_llock);
+
+ while ((entry =
+ list_first_entry_or_null(&tlist,
+ struct sw_fdb_list_entry,
+ list)) != NULL) {
+ list_del_init(&entry->list);
+ sw_fdb_list_cnt_dec(entry->dev);
+ sw_fdb_entry_free(entry);
+ }
}
+int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp)
+{
+ struct switchdev_notifier_fdb_info item = {0};
+
+ /* FDB refresh is raised from the switch offload path (AF) after
+ * switchdev FDB updates. PF-local ports are refreshed on pf->netdev.
+ * TODO: When req->hdr.pcifunc targets a guest VF (VM-bridged offload),
+ * forward the refresh via the PF-VF mailbox instead of applying it to
+ * pf->netdev; otherwise guest-owned MACs may age out prematurely (see
+ * rvu_sw_l2_fdb_refresh_send()).
+ */
+ item.addr = req->mac;
+ item.info.dev = pf->netdev;
+ if (req->flags & OTX2_FDB_DEL)
+ call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_BRIDGE,
+ item.info.dev, &item.info, NULL);
+ else
+ call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE,
+ item.info.dev, &item.info, NULL);
+
+ return 0;
+}
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
index dc427e8ab7c6..3083135c782c 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.h
@@ -9,7 +9,10 @@
#include <linux/kconfig.h>
+struct net_device;
+
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+int sw_fdb_add_to_list(struct net_device *dev, u8 *mac, bool add_fdb);
void sw_fdb_deinit(void);
int sw_fdb_init(void);
#else
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
index b51d8d2d01b8..c947f30becc8 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
@@ -191,13 +191,17 @@ static int sw_nb_fdb_event(struct notifier_block *unused,
switch (event) {
case SWITCHDEV_FDB_ADD_TO_DEVICE:
- if (fdb_info->is_local)
- break;
- break;
-
case SWITCHDEV_FDB_DEL_TO_DEVICE:
if (fdb_info->is_local)
break;
+ /* dev is the bridge port that learned the FDB
+ * (SWITCHDEV_FDB_*_TO_DEVICE), not the bridge master.
+ * sw_nb_is_valid_dev() limits this to Cavium-offloaded
+ * setups; only Cavium PF/representor netdevs are supported
+ * as bridge ports today (VLAN/virt under bridge is TODO).
+ */
+ sw_fdb_add_to_list(dev, (u8 *)fdb_info->addr,
+ event == SWITCHDEV_FDB_ADD_TO_DEVICE);
break;
default:
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
index 39435f23427c..cb87c8ca56fe 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
@@ -14,6 +14,10 @@ struct otx2_nic;
struct af2pf_fdb_refresh_req;
struct msg_rsp;
+int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
+ struct af2pf_fdb_refresh_req *req,
+ struct msg_rsp *rsp);
+
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
enum {
OTX2_DEV_UP = 1,
@@ -32,10 +36,6 @@ int otx2_sw_nb_unregister(struct net_device *netdev);
bool sw_nb_is_valid_dev(struct net_device *netdev);
struct net_device *sw_nb_resolve_pf_dev(struct net_device *dev);
-int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
- struct af2pf_fdb_refresh_req *req,
- struct msg_rsp *rsp);
-
bool sw_nb_is_cavium_dev(struct net_device *netdev);
int sw_nb_fib_event_to_otx2_event(int event, struct net_device *netdev);
int sw_nb_inetaddr_event_to_otx2_event(int event, struct net_device *netdev);
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH v9 net-next 8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
` (6 preceding siblings ...)
2026-08-31 13:19 ` [PATCH v9 net-next 7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev Ratheesh Kannoth
@ 2026-08-31 13:19 ` Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,8/8] " netdev-bot+sashiko
7 siblings, 1 reply; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-08-31 13:19 UTC (permalink / raw)
To: linux-kernel, netdev
Cc: andrew+netdev, davem, edumazet, kuba, pabeni, sgoutham,
Ratheesh Kannoth
Queue IPv4/IPv6 FIB-derived updates from the switch notifier path
and handle fib_notify in the RVU AF by batching fib_entry
structures and sending them to the switch PF through the
AF-to-switchdev FIB_CMD. Require the switch firmware to
be ready before accepting offload work.
Signed-off-by: Ratheesh Kannoth <rkannoth@marvell.com>
---
.../net/ethernet/marvell/octeontx2/af/mbox.h | 8 +-
.../marvell/octeontx2/af/switch/rvu_sw.c | 4 +-
.../marvell/octeontx2/af/switch/rvu_sw_l3.c | 292 ++++++++++++++++++
.../marvell/octeontx2/af/switch/rvu_sw_l3.h | 2 +
.../marvell/octeontx2/nic/switch/sw_fib.c | 242 +++++++++++++++
.../marvell/octeontx2/nic/switch/sw_fib.h | 14 +
.../marvell/octeontx2/nic/switch/sw_nb.c | 8 +-
.../marvell/octeontx2/nic/switch/sw_nb_v4.c | 192 +++++++-----
.../marvell/octeontx2/nic/switch/sw_nb_v6.c | 21 +-
.../marvell/octeontx2/nic/switch/sw_nb_v6.h | 31 +-
10 files changed, 720 insertions(+), 94 deletions(-)
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
index 8f7b2962a212..d63dd57999ae 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
@@ -1915,18 +1915,20 @@ struct fib_entry {
__be32 gw;
__be32 gw6[4];
};
- u16 port_id;
+ u32 port_id;
u8 nud_state;
u8 rsvd3;
u8 mac[ETH_ALEN];
u16 rsvd4; /* explicit tail padding */
};
+#define RVU_SW_L3_ENTRY_MAX 12
+
struct fib_notify_req {
struct mbox_msghdr hdr;
u16 cnt;
u16 rsvd[3]; /* explicit padding for entry[] 8-byte alignment */
- struct fib_entry entry[16];
+ struct fib_entry entry[RVU_SW_L3_ENTRY_MAX];
};
struct fl_tuple {
@@ -2000,7 +2002,7 @@ struct af2swdev_notify_req {
struct {
u8 cnt;
u8 rsvd[7]; /* explicit padding before fib_entry[] */
- struct fib_entry entry[12];
+ struct fib_entry entry[RVU_SW_L3_ENTRY_MAX];
};
struct {
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
index 71f113bded5e..12f77ffc3eb7 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
@@ -6,10 +6,10 @@
*/
#include <linux/bitfield.h>
-
#include "rvu.h"
#include "rvu_sw.h"
#include "rvu_sw_l2.h"
+#include "rvu_sw_l3.h"
#include "rvu_sw_fl.h"
/*
@@ -90,9 +90,11 @@ int rvu_mbox_handler_swdev2af_notify(struct rvu *rvu,
void rvu_sw_shutdown(void)
{
rvu_sw_l2_shutdown();
+ rvu_sw_l3_shutdown();
}
void rvu_sw_clear_shutdown(void)
{
rvu_sw_l2_clear_shutdown();
+ rvu_sw_l3_clear_shutdown();
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
index 2b798d5f0644..32735ae68e1d 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
@@ -4,11 +4,303 @@
* Copyright (C) 2026 Marvell.
*
*/
+
+#include <linux/bitfield.h>
#include "rvu.h"
+#include "rvu_sw.h"
+#include "rvu_sw_l3.h"
+
+static struct af2swdev_notify_req __maybe_unused
+*otx2_mbox_alloc_msg_af2swdev_notify(struct rvu *rvu, int devid)
+{
+ struct af2swdev_notify_req *req;
+
+ req = (struct af2swdev_notify_req *)
+ otx2_mbox_alloc_msg_rsp(&rvu->afpf_wq_info.mbox_up, devid,
+ sizeof(*req), sizeof(struct msg_rsp));
+ if (!req)
+ return NULL;
+ req->hdr.sig = OTX2_MBOX_REQ_SIG;
+ req->hdr.id = MBOX_MSG_AF2SWDEV;
+ return req;
+}
+
+struct l3_entry {
+ struct list_head list;
+ /* Always this AF driver's rvu; stored for clarity only (single RVU). */
+ struct rvu *rvu;
+ u32 port_id;
+ int cnt;
+ struct fib_entry entry[];
+};
+
+static DEFINE_MUTEX(l3_offl_llock);
+static LIST_HEAD(l3_offl_lh);
+static bool going_down;
+
+static struct workqueue_struct *sw_l3_offl_wq;
+static void sw_l3_offl_work_handler(struct work_struct *work);
+static DECLARE_DELAYED_WORK(l3_offl_work, sw_l3_offl_work_handler);
+
+/*
+ * FIB offload to the switch ASIC: one octeontx2 AF driver instance, one
+ * switch PF (switchdev), and one sw_l3_offl_wq per SoC.
+ */
+
+static void rvu_sw_l3_drain_list(struct list_head *lh)
+{
+ struct l3_entry *entry;
+
+ while ((entry = list_first_entry_or_null(lh, struct l3_entry, list))) {
+ list_del(&entry->list);
+ kfree(entry);
+ }
+}
+
+static void rvu_sw_l3_queue_work_delay_locked(unsigned long delay_jiffies)
+{
+ lockdep_assert_held(&l3_offl_llock);
+
+ if (sw_l3_offl_wq && !going_down)
+ queue_delayed_work(sw_l3_offl_wq, &l3_offl_work, delay_jiffies);
+}
+
+static void rvu_sw_l3_queue_work_delay(unsigned long delay_jiffies)
+{
+ mutex_lock(&l3_offl_llock);
+ rvu_sw_l3_queue_work_delay_locked(delay_jiffies);
+ mutex_unlock(&l3_offl_llock);
+}
+
+static void rvu_sw_l3_queue_work_locked(void)
+{
+ rvu_sw_l3_queue_work_delay_locked(msecs_to_jiffies(10));
+}
+
+static void rvu_sw_l3_queue_work(void)
+{
+ rvu_sw_l3_queue_work_delay(msecs_to_jiffies(10));
+}
+
+static int rvu_sw_l3_ensure_wq(void)
+{
+ lockdep_assert_held(&l3_offl_llock);
+
+ if (going_down)
+ return -ENODEV;
+
+ if (sw_l3_offl_wq)
+ return 0;
+
+ sw_l3_offl_wq = alloc_workqueue("sw_af_fib_wq", 0, 0);
+ if (!sw_l3_offl_wq)
+ return -ENOMEM;
+
+ return 0;
+}
+
+static int rvu_sw_l3_offl_rule_push(struct list_head *lh)
+{
+ struct af2swdev_notify_req *req;
+ struct fib_entry *entry, *dst;
+ struct l3_entry *l3_entry;
+ struct rvu *rvu;
+ int tot_cnt = 0;
+ int swdev_pf;
+ int sz, cnt, i;
+ bool rc;
+
+ BUILD_BUG_ON(sizeof_field(struct af2swdev_notify_req, entry) !=
+ sizeof(struct fib_entry) * RVU_SW_L3_ENTRY_MAX);
+ BUILD_BUG_ON(sizeof_field(struct fib_notify_req, entry) !=
+ sizeof(struct fib_entry) * RVU_SW_L3_ENTRY_MAX);
+
+ l3_entry = list_first_entry_or_null(lh, struct l3_entry, list);
+ if (!l3_entry)
+ return 0;
+
+ /*
+ * Octeontx2 has a single AF (one struct rvu) per RVU chip. All queued
+ * entries therefore share the same rvu and the same switch PF below.
+ * Host PF identity is carried per fib_entry (port_id), not by picking
+ * a different switch PF here.
+ */
+ rvu = l3_entry->rvu;
+ swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
+
+ mutex_lock(&rvu->mbox_lock);
+ req = otx2_mbox_alloc_msg_af2swdev_notify(rvu, swdev_pf);
+ if (!req) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -ENOMEM;
+ }
+
+ dst = &req->entry[0];
+ /*
+ * Batch fib_entry records from multiple host PF notifies into one
+ * af2swdev message. Safe on octeontx2: every l3_entry targets the
+ * same switch PF; egress port is encoded in each fib_entry.port_id.
+ *
+ * Entries are removed from lh and freed once copied into the mbox
+ * buffer, before the send attempt. If otx2_mbox_wait_for_zero() or
+ * the upstream send fails, that batch is lost with no replay path and
+ * the switch FIB may diverge from the host; tolerating that is a
+ * known limitation for now.
+ */
+ while ((l3_entry =
+ list_first_entry_or_null(lh,
+ struct l3_entry, list)) != NULL) {
+ entry = l3_entry->entry;
+ cnt = l3_entry->cnt;
+
+ /* af2swdev_notify_req.entry[] holds RVU_SW_L3_ENTRY_MAX slots;
+ * stop before copying the next l3_entry when the mbox buffer
+ * would overflow. Leftovers stay on lh and are re-queued.
+ */
+ if (tot_cnt + cnt > RVU_SW_L3_ENTRY_MAX)
+ break;
+
+ sz = sizeof(*entry) * cnt;
+
+ memcpy(dst, entry, sz);
+ for (i = 0; i < cnt; i++)
+ dst[i].port_id = l3_entry->port_id;
+ tot_cnt += cnt;
+ dst += cnt;
+
+ list_del_init(&l3_entry->list);
+ kfree(l3_entry);
+ }
+ if (!tot_cnt) {
+ mutex_unlock(&rvu->mbox_lock);
+ return -EINVAL;
+ }
+
+ req->flags = OTX2_FIB_CMD;
+ req->cnt = tot_cnt;
+
+ rc = otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, swdev_pf);
+ if (rc)
+ otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, swdev_pf);
+
+ mutex_unlock(&rvu->mbox_lock);
+ return rc ? 0 : -EFAULT;
+}
+
+static void sw_l3_offl_work_handler(struct work_struct *work)
+{
+ struct list_head l3lh;
+
+ INIT_LIST_HEAD(&l3lh);
+
+ mutex_lock(&l3_offl_llock);
+ if (list_empty(&l3_offl_lh)) {
+ mutex_unlock(&l3_offl_llock);
+ return;
+ }
+ if (going_down) {
+ rvu_sw_l3_drain_list(&l3_offl_lh);
+ mutex_unlock(&l3_offl_llock);
+ return;
+ }
+ list_splice_init(&l3_offl_lh, &l3lh);
+ mutex_unlock(&l3_offl_llock);
+
+ if (rvu_sw_l3_offl_rule_push(&l3lh))
+ pr_err("%s: Error to push rules\n", __func__);
+
+ /* rvu_sw_l3_offl_rule_push() may leave entries when a batch is full. */
+ if (!list_empty(&l3lh)) {
+ mutex_lock(&l3_offl_llock);
+ if (!going_down && sw_l3_offl_wq) {
+ list_splice(&l3lh, &l3_offl_lh);
+ mutex_unlock(&l3_offl_llock);
+ rvu_sw_l3_queue_work_delay(msecs_to_jiffies(100));
+ } else {
+ rvu_sw_l3_drain_list(&l3lh);
+ mutex_unlock(&l3_offl_llock);
+ }
+ return;
+ }
+
+ mutex_lock(&l3_offl_llock);
+ if (!going_down && !list_empty(&l3_offl_lh))
+ rvu_sw_l3_queue_work_locked();
+ mutex_unlock(&l3_offl_llock);
+}
int rvu_mbox_handler_fib_notify(struct rvu *rvu,
struct fib_notify_req *req,
struct msg_rsp *rsp)
{
+ struct l3_entry *l3_entry;
+ int sz, rc;
+
+ if (!(rvu->rswitch.flags & RVU_SWITCH_FLAG_FW_READY))
+ return -EAGAIN;
+
+ /* Reject notifies larger than the source fib_notify_req.entry[]. */
+ if (!req->cnt || req->cnt > RVU_SW_L3_ENTRY_MAX)
+ return -EINVAL;
+
+ sz = req->cnt * sizeof(struct fib_entry);
+
+ l3_entry = kcalloc(1, sizeof(*l3_entry) + sz, GFP_KERNEL);
+ if (!l3_entry)
+ return -ENOMEM;
+
+ l3_entry->port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
+ l3_entry->rvu = rvu;
+ l3_entry->cnt = req->cnt;
+ INIT_LIST_HEAD(&l3_entry->list);
+ memcpy(l3_entry->entry, req->entry, sz);
+
+ /* Host PFs on this RVU share one AF and one switch PF offload path. */
+ mutex_lock(&l3_offl_llock);
+ if (going_down) {
+ mutex_unlock(&l3_offl_llock);
+ kfree(l3_entry);
+ return -ENODEV;
+ }
+
+ rc = rvu_sw_l3_ensure_wq();
+ if (rc) {
+ mutex_unlock(&l3_offl_llock);
+ kfree(l3_entry);
+ return rc;
+ }
+
+ list_add_tail(&l3_entry->list, &l3_offl_lh);
+ mutex_unlock(&l3_offl_llock);
+ rvu_sw_l3_queue_work();
+
return 0;
}
+
+void rvu_sw_l3_shutdown(void)
+{
+ struct workqueue_struct *wq;
+
+ mutex_lock(&l3_offl_llock);
+ going_down = true;
+ wq = sw_l3_offl_wq;
+ sw_l3_offl_wq = NULL;
+ mutex_unlock(&l3_offl_llock);
+
+ if (!wq)
+ return;
+
+ cancel_delayed_work_sync(&l3_offl_work);
+ destroy_workqueue(wq);
+
+ mutex_lock(&l3_offl_llock);
+ rvu_sw_l3_drain_list(&l3_offl_lh);
+ mutex_unlock(&l3_offl_llock);
+}
+
+void rvu_sw_l3_clear_shutdown(void)
+{
+ mutex_lock(&l3_offl_llock);
+ going_down = false;
+ mutex_unlock(&l3_offl_llock);
+}
diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
index ac8c4f9ba5ac..03836560077f 100644
--- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
+++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.h
@@ -8,4 +8,6 @@
#ifndef RVU_SW_L3_H
#define RVU_SW_L3_H
+void rvu_sw_l3_shutdown(void);
+void rvu_sw_l3_clear_shutdown(void);
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
index f4c47111d763..318f7b68b8e4 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
@@ -8,13 +8,255 @@
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/etherdevice.h>
+#include <net/switchdev.h>
+#include <net/netevent.h>
+#include <net/arp.h>
+#include <net/route.h>
+
+#include "../otx2_reg.h"
+#include "../otx2_common.h"
+#include "../otx2_struct.h"
+#include "../cn10k.h"
+#include "sw_nb.h"
+
+#define SW_FIB_LIST_MAX 4096
+#define SW_FIB_NOTIFY_RETRY_MAX 100
+
+/*
+ * One switch PF registers notifiers via otx2_sw_nb_register(); a second call
+ * returns -EBUSY. A single sw_fib_wq therefore serves the one switchdev
+ * instance on octeontx2, matching the FDB offload path.
+ */
+static DEFINE_SPINLOCK(sw_fib_llock);
+static LIST_HEAD(sw_fib_lh);
+static atomic_t sw_fib_list_cnt = ATOMIC_INIT(0);
+
+static struct workqueue_struct *sw_fib_wq;
+static void sw_fib_work_handler(struct work_struct *work);
+static DECLARE_DELAYED_WORK(sw_fib_work, sw_fib_work_handler);
+
+struct sw_fib_list_entry {
+ struct list_head lh;
+ struct otx2_nic *pf;
+ netdevice_tracker dev_tracker;
+ int cnt;
+ int retries;
+ struct fib_entry *entry;
+};
+
+static void sw_fib_list_cnt_warn(struct net_device *netdev)
+{
+ int n = atomic_read(&sw_fib_list_cnt);
+
+ if (n < 0)
+ netdev_warn(netdev, "FIB list count underflow: %d\n", n);
+ else if (n > SW_FIB_LIST_MAX)
+ netdev_warn(netdev, "FIB list count overflow: %d (max %d)\n",
+ n, SW_FIB_LIST_MAX);
+}
+
+static int sw_fib_list_count(void)
+{
+ return atomic_read(&sw_fib_list_cnt);
+}
+
+static void sw_fib_list_cnt_inc(struct net_device *netdev)
+{
+ atomic_inc(&sw_fib_list_cnt);
+ sw_fib_list_cnt_warn(netdev);
+}
+
+static void sw_fib_list_cnt_dec(struct net_device *netdev)
+{
+ atomic_dec(&sw_fib_list_cnt);
+ sw_fib_list_cnt_warn(netdev);
+}
+
+static void sw_fib_list_entry_destroy(struct sw_fib_list_entry *lentry)
+{
+ struct net_device *dev = lentry->pf->netdev;
+
+ sw_fib_list_cnt_dec(dev);
+ netdev_put(dev, &lentry->dev_tracker);
+ kfree(lentry->entry);
+ kfree(lentry);
+}
+
+static int sw_fib_notify(struct otx2_nic *pf,
+ int cnt,
+ struct fib_entry *entry)
+{
+ struct fib_notify_req *req;
+ int rc;
+
+ if (cnt > RVU_SW_L3_ENTRY_MAX)
+ return -EINVAL;
+
+ mutex_lock(&pf->mbox.lock);
+ req = otx2_mbox_alloc_msg_fib_notify(&pf->mbox);
+ if (!req) {
+ rc = -ENOMEM;
+ goto out;
+ }
+
+ req->cnt = cnt;
+ memcpy(req->entry, entry, sizeof(*entry) * cnt);
+
+ rc = otx2_sync_mbox_msg(&pf->mbox);
+out:
+ mutex_unlock(&pf->mbox.lock);
+ return rc;
+}
+
+static void sw_fib_work_handler(struct work_struct *work)
+{
+ struct sw_fib_list_entry *lentry;
+ LIST_HEAD(tlist);
+
+ spin_lock_bh(&sw_fib_llock);
+ list_splice_init(&sw_fib_lh, &tlist);
+ spin_unlock_bh(&sw_fib_llock);
+
+ while ((lentry =
+ list_first_entry_or_null(&tlist,
+ struct sw_fib_list_entry, lh)) != NULL) {
+ list_del_init(&lentry->lh);
+ if (sw_fib_notify(lentry->pf, lentry->cnt, lentry->entry)) {
+ struct net_device *dev = lentry->pf->netdev;
+
+ lentry->retries++;
+ spin_lock_bh(&sw_fib_llock);
+ if (sw_fib_wq && lentry->retries < SW_FIB_NOTIFY_RETRY_MAX) {
+ /*
+ * TODO: Preserve strict FIB notify ordering on
+ * retry. Requeuing a failed ADD at the tail
+ * while continuing the batch lets a later DEL
+ * for the same route succeed first; when the
+ * ADD is retried the switch can keep a stale
+ * route the kernel already deleted.
+ */
+ netdev_err(dev,
+ "Failed to notify FIB update to AF, will retry (%d/%d)\n",
+ lentry->retries, SW_FIB_NOTIFY_RETRY_MAX);
+ list_add_tail(&lentry->lh, &sw_fib_lh);
+ queue_delayed_work(sw_fib_wq, &sw_fib_work,
+ msecs_to_jiffies(100));
+ spin_unlock_bh(&sw_fib_llock);
+ continue;
+ }
+ spin_unlock_bh(&sw_fib_llock);
+ netdev_err(dev,
+ "Failed to notify FIB update to AF, giving up after %d tries\n",
+ lentry->retries);
+ sw_fib_list_entry_destroy(lentry);
+ continue;
+ }
+ sw_fib_list_entry_destroy(lentry);
+ }
+
+ spin_lock_bh(&sw_fib_llock);
+ if (!list_empty(&sw_fib_lh) && sw_fib_wq)
+ queue_delayed_work(sw_fib_wq, &sw_fib_work,
+ msecs_to_jiffies(10));
+ spin_unlock_bh(&sw_fib_llock);
+}
+
+int sw_fib_add_to_list(struct net_device *dev,
+ struct fib_entry *entry, int cnt)
+{
+ struct otx2_nic *pf = netdev_priv(dev);
+ struct sw_fib_list_entry *lentry;
+ struct workqueue_struct *wq;
+
+ if (cnt <= 0 || cnt > RVU_SW_L3_ENTRY_MAX) {
+ kfree(entry);
+ return -EINVAL;
+ }
+
+ spin_lock_bh(&sw_fib_llock);
+ if (!sw_fib_wq) {
+ spin_unlock_bh(&sw_fib_llock);
+ kfree(entry);
+ return -EINVAL;
+ }
+ spin_unlock_bh(&sw_fib_llock);
+
+ if (sw_fib_list_count() >= SW_FIB_LIST_MAX) {
+ kfree(entry);
+ return -ENOMEM;
+ }
+
+ lentry = kcalloc(1, sizeof(*lentry), GFP_ATOMIC);
+ if (!lentry) {
+ kfree(entry);
+ return -ENOMEM;
+ }
+
+ lentry->pf = pf;
+ lentry->cnt = cnt;
+ lentry->entry = entry;
+ INIT_LIST_HEAD(&lentry->lh);
+ netdev_hold(dev, &lentry->dev_tracker, GFP_ATOMIC);
+
+ spin_lock_bh(&sw_fib_llock);
+ wq = sw_fib_wq;
+ if (wq) {
+ list_add_tail(&lentry->lh, &sw_fib_lh);
+ sw_fib_list_cnt_inc(dev);
+ queue_delayed_work(wq, &sw_fib_work,
+ msecs_to_jiffies(10));
+ }
+ spin_unlock_bh(&sw_fib_llock);
+
+ if (!wq) {
+ netdev_put(dev, &lentry->dev_tracker);
+ kfree(lentry);
+ kfree(entry);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
int otx2_sw_fib_init(void)
{
+ sw_fib_wq = alloc_workqueue("sw_pf_fib_wq", 0, 0);
+ if (!sw_fib_wq)
+ return -ENOMEM;
+
return 0;
}
void otx2_sw_fib_deinit(void)
{
+ struct sw_fib_list_entry *lentry;
+ struct workqueue_struct *wq;
+ LIST_HEAD(tlist);
+
+ spin_lock_bh(&sw_fib_llock);
+ wq = sw_fib_wq;
+ sw_fib_wq = NULL;
+ spin_unlock_bh(&sw_fib_llock);
+
+ if (!wq)
+ return;
+
+ cancel_delayed_work_sync(&sw_fib_work);
+ destroy_workqueue(wq);
+
+ spin_lock_bh(&sw_fib_llock);
+ list_splice_init(&sw_fib_lh, &tlist);
+ spin_unlock_bh(&sw_fib_llock);
+
+ while ((lentry =
+ list_first_entry_or_null(&tlist,
+ struct sw_fib_list_entry, lh)) != NULL) {
+ list_del_init(&lentry->lh);
+ sw_fib_list_entry_destroy(lentry);
+ }
}
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
index 448d5612133e..046fdee42674 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.h
@@ -8,11 +8,25 @@
#define SW_FIB_H_
#include <linux/kconfig.h>
+#include <linux/slab.h>
+
+struct fib_entry;
+struct net_device;
#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
+int sw_fib_add_to_list(struct net_device *dev,
+ struct fib_entry *entry, int cnt);
void otx2_sw_fib_deinit(void);
int otx2_sw_fib_init(void);
#else
+static inline int sw_fib_add_to_list(struct net_device *dev,
+ struct fib_entry *entry, int cnt)
+{
+ (void)dev;
+ (void)cnt;
+ kfree(entry);
+ return 0;
+}
static inline void otx2_sw_fib_deinit(void) {}
static inline int otx2_sw_fib_init(void) { return 0; }
#endif
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
index c947f30becc8..b512d4152ac8 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
@@ -185,6 +185,7 @@ static int sw_nb_fdb_event(struct notifier_block *unused,
{
struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
struct switchdev_notifier_fdb_info *fdb_info = ptr;
+ int rc = 0;
if (!sw_nb_is_valid_dev(dev))
return NOTIFY_DONE;
@@ -200,14 +201,17 @@ static int sw_nb_fdb_event(struct notifier_block *unused,
* setups; only Cavium PF/representor netdevs are supported
* as bridge ports today (VLAN/virt under bridge is TODO).
*/
- sw_fdb_add_to_list(dev, (u8 *)fdb_info->addr,
- event == SWITCHDEV_FDB_ADD_TO_DEVICE);
+ rc = sw_fdb_add_to_list(dev, (u8 *)fdb_info->addr,
+ event == SWITCHDEV_FDB_ADD_TO_DEVICE);
break;
default:
return NOTIFY_DONE;
}
+ if (rc)
+ netdev_err(dev, "%s: Error to add to list\n", __func__);
+
return NOTIFY_DONE;
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
index 31009e00121f..7ee3a98fc50d 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
@@ -12,6 +12,7 @@
#include <net/arp.h>
#include <net/route.h>
#include <linux/inetdevice.h>
+#include <net/nexthop.h>
#include "../otx2_reg.h"
#include "../otx2_common.h"
@@ -43,7 +44,13 @@ int sw_nb_v4_netdev_event(struct notifier_block *unused,
if (!idev || !idev->ifa_list)
return NOTIFY_DONE;
- /* Switch offload supports a single IPv4 address per interface for now. */
+ if (!sw_nb_is_valid_dev(dev))
+ return NOTIFY_DONE;
+
+ /* Switch offload supports a single IPv4 address per interface for
+ * now. Only the head of ifa_list is offloaded on netdev events;
+ * secondary addresses are not supported by the hardware path.
+ */
ifa = rtnl_dereference(idev->ifa_list);
entry = kcalloc(1, sizeof(*entry), GFP_KERNEL);
@@ -69,6 +76,10 @@ int sw_nb_v4_netdev_event(struct notifier_block *unused,
entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
}
+ /* Switch offload is only enabled on OcteonTX2/CN10K SoCs. pf_dev is an
+ * octeontx2 PF or representor netdev, so netdev_priv() is otx2_nic even
+ * though sw_nb_is_cavium_dev() matches the shared Cavium PCI vendor ID.
+ */
pf = netdev_priv(pf_dev);
entry->port_id = pf->pcifunc;
@@ -81,7 +92,7 @@ int sw_nb_v4_netdev_event(struct notifier_block *unused,
netdev_dbg(dev, "%s: pushing netdev event from HOST interface address %pI4n, %pM, dev=%s\n",
__func__, &entry->dst, entry->mac, dev->name);
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
@@ -106,7 +117,8 @@ int sw_nb_v4_inetaddr_event(struct notifier_block *nb,
return NOTIFY_DONE;
/* On NETDEV_DOWN the deleted address is passed in ifa; ifa_list may
- * already be empty when the last address is removed.
+ * already be empty when the last address is unlinked before the
+ * notifier runs.
*/
entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
if (!entry)
@@ -144,24 +156,27 @@ int sw_nb_v4_inetaddr_event(struct notifier_block *nb,
netdev_dbg(dev, "%s: pushing inetaddr event from HOST interface address %pI4n, %pM, %s\n",
__func__, &entry->dst, entry->mac, dev->name);
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
int sw_nb_v4_fib_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
- struct net_device *dev, *pf_dev = NULL, *nh_pf_dev;
struct fib_entry_notifier_info *fen_info = ptr;
- struct fib_entry *entries, *iter;
+ struct net_device *host_pf_dev = NULL;
struct netdev_hw_addr *dev_addr;
+ struct net_device *nh_pf_dev;
+ struct fib_nh_common *nhc;
struct neighbour *neigh;
+ struct fib_entry *entry;
+ struct net_device *dev;
struct fib_nh *fib_nh;
struct fib_info *fi;
struct otx2_nic *pf;
+ int i, cnt, nhs;
__be32 *haddr;
int hcnt = 0;
- int cnt, i;
/* Process only UNICAST routes add or del */
if (fen_info->type != RTN_UNICAST)
@@ -171,13 +186,17 @@ int sw_nb_v4_fib_event(struct notifier_block *nb,
if (!fi)
return NOTIFY_DONE;
+ nhs = fib_info_num_path(fi);
+
if (fi->fib_nh_is_v6) {
- struct net_device *log_dev = (fi->fib_nhs > 0) ?
- fi->fib_nh->fib_nh_dev : NULL;
+ if (nhs > 0) {
+ nhc = fib_info_nhc(fi, 0);
- if (log_dev)
- netdev_dbg(log_dev, "%s: Received v6 notification\n",
- __func__);
+ if (nhc->nhc_dev)
+ netdev_dbg(nhc->nhc_dev,
+ "%s: Received v6 notification\n",
+ __func__);
+ }
return NOTIFY_DONE;
}
@@ -186,19 +205,16 @@ int sw_nb_v4_fib_event(struct notifier_block *nb,
* are walked below; nhid and nexthop-group installs are intentionally
* skipped until fib_info_num_path()/fib_info_nhc() handling is added.
*/
- entries = kcalloc(fi->fib_nhs, sizeof(*entries), GFP_ATOMIC);
- if (!entries)
+ if (!nhs)
return NOTIFY_DONE;
- haddr = kcalloc(fi->fib_nhs, sizeof(*haddr), GFP_ATOMIC);
- if (!haddr) {
- kfree(entries);
+ haddr = kcalloc(nhs, sizeof(*haddr), GFP_ATOMIC);
+ if (!haddr)
return NOTIFY_DONE;
- }
- iter = entries;
- fib_nh = fi->fib_nh;
- for (i = 0; i < fi->fib_nhs; i++, fib_nh++) {
+ for (i = 0; i < nhs; i++) {
+ nhc = fib_info_nhc(fi, i);
+ fib_nh = container_of(nhc, struct fib_nh, nh_common);
dev = fib_nh->fib_nh_dev;
if (!dev)
@@ -210,107 +226,118 @@ int sw_nb_v4_fib_event(struct notifier_block *nb,
if (!sw_nb_is_valid_dev(dev))
continue;
- iter->cmd = sw_nb_fib_event_to_otx2_event(event, dev);
- iter->dst = htonl(fen_info->dst);
- iter->dst_len = fen_info->dst_len;
- iter->gw = fib_nh->fib_nh_gw4;
-
- netdev_dbg(dev, "%s: FIB route Rule cmd=%llu dst=%pI4n dst_len=%u gw=%pI4n\n",
- __func__, iter->cmd, &iter->dst, iter->dst_len, &iter->gw);
-
nh_pf_dev = sw_nb_resolve_pf_dev(dev);
if (!nh_pf_dev)
continue;
- pf_dev = nh_pf_dev;
+
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ break;
+
+ entry->cmd = sw_nb_fib_event_to_otx2_event(event, dev);
+ entry->dst = htonl(fen_info->dst);
+ entry->dst_len = fen_info->dst_len;
+ entry->gw = fib_nh->fib_nh_gw4;
if (netif_is_bridge_master(dev)) {
- iter->bridge = 1;
+ entry->bridge = 1;
} else if (is_vlan_dev(dev)) {
- iter->vlan_valid = 1;
- iter->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
+ entry->vlan_valid = 1;
+ entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
}
- pf = netdev_priv(pf_dev);
- iter->port_id = pf->pcifunc;
+ pf = netdev_priv(nh_pf_dev);
+ entry->port_id = pf->pcifunc;
/* Point-to-point routes, including default routes with no
* gateway, are not supported for switch offload.
*/
- if (!fib_nh->fib_nh_gw4)
+ if (!fib_nh->fib_nh_gw4) {
+ if (!entry->dst && !entry->dst_len) {
+ kfree(entry);
+ continue;
+ }
+ sw_fib_add_to_list(nh_pf_dev, entry, 1);
continue;
- iter->gw_valid = 1;
+ }
+
+ entry->gw_valid = 1;
if (fib_nh->nh_saddr)
haddr[hcnt++] = fib_nh->nh_saddr;
+ /* TODO: No replay mechanism yet when the gateway neighbor is
+ * unresolved. If ip_neigh_gw4() returns NULL the route is
+ * skipped here; sw_nb_net_v4_neigh_update() only pushes the
+ * MAC and does not replay the dropped route configuration.
+ */
rcu_read_lock();
neigh = ip_neigh_gw4(fib_nh->fib_nh_dev, fib_nh->fib_nh_gw4);
- if (!neigh || IS_ERR(neigh)) {
+ if (IS_ERR_OR_NULL(neigh)) {
rcu_read_unlock();
+ kfree(entry);
continue;
}
- neigh_ha_snapshot(iter->mac, neigh, fib_nh->fib_nh_dev);
- if (is_valid_ether_addr(iter->mac))
- iter->mac_valid = 1;
-
- iter++;
+ neigh_ha_snapshot(entry->mac, neigh, fib_nh->fib_nh_dev);
+ if (is_valid_ether_addr(entry->mac))
+ entry->mac_valid = 1;
rcu_read_unlock();
- }
- cnt = iter - entries;
- if (!cnt) {
- kfree(entries);
- kfree(haddr);
- return NOTIFY_DONE;
+ netdev_dbg(dev, "%s: FIB route Rule cmd=%llu dst=%pI4n dst_len=%u gw=%pI4n\n",
+ __func__, entry->cmd, &entry->dst, entry->dst_len,
+ &entry->gw);
+ sw_fib_add_to_list(nh_pf_dev, entry, 1);
}
- if (pf_dev)
- netdev_dbg(pf_dev, "pf_dev is %s cnt=%d\n", pf_dev->name, cnt);
- kfree(entries);
-
if (!hcnt) {
kfree(haddr);
return NOTIFY_DONE;
}
- if (!pf_dev) {
- kfree(haddr);
- return NOTIFY_DONE;
- }
+ for (i = 0; i < hcnt; i++) {
+ host_pf_dev = NULL;
+ for (cnt = 0; cnt < nhs; cnt++) {
+ nhc = fib_info_nhc(fi, cnt);
+ fib_nh = container_of(nhc, struct fib_nh, nh_common);
+ if (fib_nh->nh_saddr != haddr[i])
+ continue;
+ /* Skip blackhole or unresolved nexthops with no device. */
+ if (!fib_nh->fib_nh_dev)
+ continue;
+ host_pf_dev = sw_nb_resolve_pf_dev(fib_nh->fib_nh_dev);
+ break;
+ }
- entries = kcalloc(hcnt, sizeof(*entries), GFP_ATOMIC);
- if (!entries) {
- kfree(haddr);
- return NOTIFY_DONE;
- }
+ if (!host_pf_dev)
+ continue;
- iter = entries;
+ entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
+ if (!entry)
+ break;
- /* Host routes reuse pf_dev/pf from the last resolved Cavium netdev:
- * pf_dev only identifies the switch AF mailbox context for switchdev
- * programming; any previously resolved Cavium netdev is sufficient.
- */
- for (i = 0; i < hcnt; i++, iter++) {
- iter->cmd = sw_nb_fib_event_to_otx2_event(event, pf_dev);
- iter->dst = haddr[i];
- iter->dst_len = 32;
- iter->mac_valid = 1;
- iter->host = 1;
- iter->port_id = pf->pcifunc;
+ pf = netdev_priv(host_pf_dev);
+ entry->cmd = sw_nb_fib_event_to_otx2_event(event, host_pf_dev);
+ entry->dst = haddr[i];
+ entry->dst_len = 32;
+ entry->mac_valid = 1;
+ entry->host = 1;
+ entry->port_id = pf->pcifunc;
rcu_read_lock();
- for_each_dev_addr(pf_dev, dev_addr) {
- ether_addr_copy(iter->mac, dev_addr->addr);
+ for_each_dev_addr(host_pf_dev, dev_addr) {
+ ether_addr_copy(entry->mac, dev_addr->addr);
break;
}
rcu_read_unlock();
- netdev_dbg(pf_dev, "%s: FIB host Rule cmd=%llu dst=%pI4n dst_len=%u %s\n",
- __func__, iter->cmd, &iter->dst, iter->dst_len,
- pf_dev->name);
+ netdev_dbg(host_pf_dev,
+ "%s: FIB host Rule cmd=%llu dst=%pI4n dst_len=%u %s\n",
+ __func__, entry->cmd, &entry->dst, entry->dst_len,
+ host_pf_dev->name);
+ sw_fib_add_to_list(host_pf_dev, entry, 1);
}
- kfree(entries);
+
kfree(haddr);
return NOTIFY_DONE;
}
@@ -326,6 +353,9 @@ int sw_nb_net_v4_neigh_update(struct notifier_block *nb,
if (n->tbl != &arp_tbl)
return NOTIFY_DONE;
+ if (!sw_nb_is_valid_dev(n->dev))
+ return NOTIFY_DONE;
+
entry = kcalloc(1, sizeof(*entry), GFP_ATOMIC);
if (!entry)
return NOTIFY_DONE;
@@ -353,7 +383,7 @@ int sw_nb_net_v4_neigh_update(struct notifier_block *nb,
pf = netdev_priv(pf_dev);
entry->port_id = pf->pcifunc;
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
index 3497e60aedbe..2648bde47c18 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
@@ -97,15 +97,15 @@ int sw_nb_v6_netdev_event(struct notifier_block *unused,
netdev_dbg(dev, "netdev event addr=%pI6c plen=%u mac=%pM\n",
&addr, prefix_len, entry->mac);
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
int sw_nb_v6_fib_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
- struct fib6_entry_notifier_info *f6_eni;
struct fib_notifier_info *info = ptr;
+ struct fib6_entry_notifier_info *f6_eni;
struct net_device *fib_dev, *pf_dev;
struct fib_entry *entry;
struct fib6_info *f6i;
@@ -143,6 +143,11 @@ int sw_nb_v6_fib_event(struct notifier_block *nb,
f6i->fib6_flags, f6i->fib6_protocol, f6i->fib6_type);
nh6 = f6i->nh ? nexthop_fib6_nh(f6i->nh) : f6i->fib6_nh;
+ /*
+ * TODO: Offload directly connected IPv6 subnets without an IPv6
+ * gateway. fib_nh_gw_family is only AF_INET6 when RTF_GATEWAY is set,
+ * so connected routes are dropped here today.
+ */
if (nh6->fib_nh_gw_family != AF_INET6)
return NOTIFY_DONE;
@@ -174,10 +179,14 @@ int sw_nb_v6_fib_event(struct notifier_block *nb,
/* TODO: No replay mechanism yet when the gateway neighbor is unresolved.
* If ip_neigh_gw6() returns NULL the route is skipped here; add replay
* from the neighbor update handler once nexthop resolution completes.
+ *
+ * TODO: Bypass gateway neighbour lookup for directly connected IPv6
+ * routes, similar to sw_nb_v4_fib_event(). Unconditional ip_neigh_gw6()
+ * is incorrect when no gateway is configured.
*/
rcu_read_lock();
neigh = ip_neigh_gw6(fib_dev, &nh6->fib_nh_gw6);
- if (!neigh || IS_ERR(neigh)) {
+ if (IS_ERR_OR_NULL(neigh)) {
rcu_read_unlock();
kfree(entry);
return NOTIFY_DONE;
@@ -189,8 +198,8 @@ int sw_nb_v6_fib_event(struct notifier_block *nb,
netdev_dbg(fib_dev, "fib found MAC=%pM\n", entry->mac);
}
+ sw_fib_add_to_list(pf_dev, entry, 1);
rcu_read_unlock();
- kfree(entry);
return NOTIFY_DONE;
}
@@ -234,7 +243,7 @@ int sw_nb_net_v6_neigh_update(struct notifier_block *nb,
netdev_dbg(n->dev, "v6 neigh update %pI6c mac=%pM plen=%u\n",
(struct in6_addr *)n->primary_key, entry->mac,
n->tbl->key_len * 8);
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
@@ -294,7 +303,7 @@ int sw_nb_v6_inetaddr_event(struct notifier_block *nb,
netdev_dbg(dev, "inetaddr addr=%pI6c len=%u %pM\n",
&ifa6->addr, ifa6->prefix_len, entry->mac);
- kfree(entry);
+ sw_fib_add_to_list(pf_dev, entry, 1);
return NOTIFY_DONE;
}
diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
index f73efc98c311..78c0df5eb880 100644
--- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
+++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.h
@@ -7,6 +7,9 @@
#ifndef SW_NB_V6_H_
#define SW_NB_V6_H_
+#include <linux/kconfig.h>
+
+#if IS_ENABLED(CONFIG_IPV6)
int sw_nb_v6_fib_event(struct notifier_block *nb,
unsigned long event, void *ptr);
@@ -18,4 +21,30 @@ int sw_nb_v6_inetaddr_event(struct notifier_block *nb,
int sw_nb_v6_netdev_event(struct notifier_block *unused,
unsigned long event, void *ptr);
-#endif // SW_NB_V6_H__
+#else
+static inline int sw_nb_v6_fib_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ return NOTIFY_DONE;
+}
+
+static inline int sw_nb_net_v6_neigh_update(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ return NOTIFY_DONE;
+}
+
+static inline int sw_nb_v6_inetaddr_event(struct notifier_block *nb,
+ unsigned long event, void *ptr)
+{
+ return NOTIFY_DONE;
+}
+
+static inline int sw_nb_v6_netdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+{
+ return NOTIFY_DONE;
+}
+#endif
+
+#endif /* SW_NB_V6_H_ */
--
2.43.0
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [v9,net-next,1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files
2026-08-31 13:19 ` [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider:
Critical: 0 · High: 1 · Medium: 1 (1) · Low: 0
- [High] The four new messages are registered in the generic AF ID range
(0x010-0x013) and are dispatched by rvu_process_mbox_msg(), which…
- [Medium] rvu_mbox_handler_fl_notify() and
rvu_mbox_handler_fl_get_stats() (af/switch/rvu_sw_fl.c:9-21) register…
Pre-existing issues:
- [Medium] rvu_process_mbox_msg() casts the received message to `struct
_req_type *` purely on the basis of the sender-supplied req->id, and…
--
octeontx2-af: switch: Add AF to switch mbox and skeleton files
This patch adds four new AF mailbox messages (FDB_NOTIFY, FIB_NOTIFY,
FL_NOTIFY, FL_GET_STATS) with their request/response structures, plus a new
switch/ subdirectory holding skeleton handler functions that currently just
return 0.
A few questions about the new message IDs and the stub handlers below.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> index cece197d1074b..854696d2a35f1 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> @@ -164,6 +164,14 @@ M(PTP_GET_CAP, 0x00c, ptp_get_cap, msg_req, ptp_get_cap_rsp) \
> M(GET_REP_CNT, 0x00d, get_rep_cnt, msg_req, get_rep_cnt_rsp) \
> M(ESW_CFG, 0x00e, esw_cfg, esw_cfg_req, msg_rsp) \
> M(REP_EVENT_NOTIFY, 0x00f, rep_event_notify, rep_event, msg_rsp) \
> +M(FDB_NOTIFY, 0x010, fdb_notify, \
> + fdb_notify_req, msg_rsp) \
> +M(FIB_NOTIFY, 0x011, fib_notify, \
> + fib_notify_req, msg_rsp) \
> +M(FL_NOTIFY, 0x012, fl_notify, \
> + fl_notify_req, msg_rsp) \
> +M(FL_GET_STATS, 0x013, fl_get_stats, \
> + fl_get_stats_req, fl_get_stats_rsp) \
[Severity: High]
Should these four IDs be restricted to a privileged sender?
They are added to the generic AF ID range, and rvu_process_mbox_msg() in
af/rvu.c dispatches on req->id after only checking req->sig, with no
per-ID or per-sender filtering. __rvu_mbox_handler() feeds both TYPE_AFPF
and TYPE_AFVF messages into that same dispatcher, and the only
sanitisation it does for a VF is to stamp the sender's own identity:
case TYPE_AFVF:
msg->pcifunc &=
~(RVU_PFVF_FUNC_MASK << RVU_PFVF_FUNC_SHIFT);
msg->pcifunc |= (devid << RVU_PFVF_FUNC_SHIFT) + 1;
break;
err = rvu_process_mbox_msg(mbox, devid, msg);
So any PF and any VF, including a VF assigned to an untrusted guest via
SR-IOV, can issue these switch control-plane messages. None of the new
handlers looks at who sent the message.
The stubs make this harmless today, but as the series makes the handlers
functional, rvu_mbox_handler_fdb_notify() ends up gating only on
RVU_SWITCH_FLAG_FW_READY plus
port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
if (port_id == RVU_SW_INVALID_PORT_ID)
return -EINVAL;
...
ether_addr_copy(l2_entry->mac, req->mac);
l2_entry->flags = req->flags;
which every representor-mapped PF and VF satisfies, after which the
caller-supplied MAC and OTX2_FDB_ADD/OTX2_FDB_DEL are programmed toward
the switch for that port. rvu_mbox_handler_fib_notify() similarly accepts
caller-supplied route entries from any PF/VF.
Compare with the sibling handler added in the same series, which does
perform the check:
if (!rvu_is_switch_pcifunc(rvu, sender))
return -EPERM;
and with rvu_mbox_handler_esw_cfg() in af/rvu_rep.c, which ignores
requests whose hdr.pcifunc differs from rvu->rep_pcifunc. Would an
equivalent sender check on the new handlers be appropriate?
[ ... ]
> @@ -1812,6 +1820,116 @@ struct rep_event {
> struct rep_evt_data evt_data;
> };
>
[ ... ]
> +struct fib_notify_req {
> + struct mbox_msghdr hdr;
> + u16 cnt;
> + u16 rsvd[3]; /* explicit padding for entry[] 8-byte alignment */
> + struct fib_entry entry[16];
> +};
[Severity: Medium]
This isn't a problem introduced by this patch, but it does enlarge the
worst case for it.
rvu_process_mbox_msg() casts the received message to struct _req_type *
based purely on the sender-supplied req->id:
err = rvu_mbox_handler_ ## _fn_name(rvu, \
(struct _req_type *)req, \
rsp);
and __rvu_mbox_handler() walks the message list using the sender-supplied
offset without bounding it against the region:
offset = mbox->rx_start + msg->next_msgoff;
struct fib_notify_req is 1176 bytes here, which becomes by far the largest
AF request payload, so it widens how far past a minimally sized message a
handler can be made to read once it dereferences req->entry[]. At the end
of the series rvu_mbox_handler_fib_notify() memcpy()s req->cnt *
sizeof(struct fib_entry) out of the shared mbox region and forwards it to
the switch OS.
The actionable fix here belongs in the mbox core (validating next_msgoff
and the available message size against the declared request type) rather
than in these handlers, so this is noted for information only.
[ ... ]
> +struct fl_get_stats_req {
> + struct mbox_msghdr hdr;
> + u64 cookie;
> +};
> +
> +struct fl_get_stats_rsp {
> + struct mbox_msghdr hdr;
> + u64 pkts_diff;
> +};
> +
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c
> new file mode 100644
> index 0000000000000..1f8b82a84a5dd
> --- /dev/null
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_fl.c
> @@ -0,0 +1,21 @@
[ ... ]
> +int rvu_mbox_handler_fl_get_stats(struct rvu *rvu,
> + struct fl_get_stats_req *req,
> + struct fl_get_stats_rsp *rsp)
> +{
> + return 0;
> +}
> +
> +int rvu_mbox_handler_fl_notify(struct rvu *rvu,
> + struct fl_notify_req *req,
> + struct msg_rsp *rsp)
> +{
> + return 0;
> +}
[Severity: Medium]
Should these two stubs report success?
Once FL_NOTIFY and FL_GET_STATS are registered in MBOX_MESSAGES, the
dispatcher no longer answers with otx2_reply_invalid_msg(); instead it
presets a success code and only overrides it if the handler fails:
if (rsp) {
...
rsp->hdr.rc = 0;
}
err = rvu_mbox_handler_ ## _fn_name(rvu, (struct _req_type *)req, rsp);
if (rsp && err)
rsp->hdr.rc = err;
Since otx2_mbox_alloc_msg_rsp() zeroes the whole response region:
/* Clear the whole msg region */
memset(msghdr, 0, size);
FL_GET_STATS answers every query with pkts_diff = 0 and rc = 0, so a
requesting peer cannot tell a genuine zero delta from a discarded request
and may age the flow out. Likewise, rvu_mbox_handler_fl_notify() ignores
cookie, flags, features and tuple entirely, yet acknowledges the flow
install or delete as done, so a peer that stops software forwarding after
a successful FL_NOTIFY would blackhole that flow.
Both handlers are still identical at the end of the series, so this is not
a transient state within the series. Would returning -EOPNOTSUPP, or
holding back the ID registration until the handlers are implemented, be
preferable?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,2/8] octeontx2-af: switch: Add switch dev to AF mboxes
2026-08-31 13:19 ` [PATCH v9 net-next 2/8] octeontx2-af: switch: Add switch dev to AF mboxes Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 11 potential
issue(s) to consider:
Critical: 0 · High: 1 (1) · Medium: 7 · Low: 3
- [High] The new IFACE_GET_INFO mailbox (MBOX_MSG_IFACE_GET_INFO, 0x014)
performs no capability/ownership check on the requester.
- [Medium] The commit message describes the patch as only *defining*
mailboxes so switch software can query information from AF ("These…
- [Medium] Memory leak of the devres-allocated `struct qmem` descriptor
on qmem_alloc() partial failure.
- [Medium] Inconsistent locking for the same fields: the new helper
nix_ctx_assign() publishes pfvf->rss_ctx, pfvf->cq_ints_ctx and…
- [Medium] rvu_mbox_handler_iface_get_info() walks every PF/VF and reads
other pcifuncs' rvu_pfvf state (tx_chan_cnt, rx_chan_base/cnt,…
- [Medium] The two new bulk mailbox handlers emit non-ratelimited
dev_err() messages driven entirely by attacker-supplied request…
- [Medium] rvu_mbox_handler_iface_get_info() publishes info->tx_link =
nix_get_tx_link(rvu, pcifunc) (rvu.c:2071 for PFs, rvu.c:2104 for…
- [Medium] Silent Memory Leak in Context Assignment and Free Paths
- [Low] Stub mailbox handlers acknowledge success without implementing
their advertised operation: rvu_mbox_handler_swdev2af_notify() in…
- [Low] The new UP message groups MBOX_UP_AF2SWDEV_MESSAGES (AF2SWDEV,
0xEF1) and MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES (AF2PF_FDB_REFRESH,…
- [Low] The new code takes the NIX admin-queue lock with
spin_lock_irqsave() (rvu_iface_get_qcnts() in rvu.c, nix_qctx_assign()…
Pre-existing issues:
- [High] The patch introduces the convention "pfvf->{rq,sq,cq}_ctx and
*_bmap are published/cleared under block->aq->lock" so the new…
--
octeontx2-af: switch: Add switch dev to AF mboxes
This adds new AF mailboxes (IFACE_GET_INFO, SWDEV2AF_NOTIFY,
NPC_MCAM_FLOW_DEL_N_FREE, NPC_MCAM_GET_MUL_STATS, NPC_MCAM_GET_FEATURES)
and two AF initiated UP message groups so switch software can query
information from AF. It also reworks how the NIX LF context memory is
published and freed in rvu_nix.c. A few questions below.
[Severity: Medium]
The changelog says:
"Switch needs various information from AF driver. These mboxes are
defined to query those from AF driver."
Two parts of the patch look wider than that description.
NPC_MCAM_FLOW_DEL_N_FREE (0x6020) is not a query. Its handler deletes
flows and frees up to 256 MCAM entries per message.
rvu_nix.c also changes the generic NIX LF context lifecycle used by every
PF/VF: new helpers nix_qctx_assign() and nix_ctx_assign(), nix_ctx_free()
converted to snapshot-and-clear under aq->lock with a new early return
when aq is NULL, and per-allocation qmem_free() error handling in
rvu_mbox_handler_nix_lf_alloc().
Could the changelog mention the state changing mailbox and the NIX
context rework, or could the rvu_nix.c rework move into its own patch so
it can be reviewed and backported on its own?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> index 854696d2a35f1..e45e6e93ed087 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> @@ -172,6 +172,10 @@ M(FL_NOTIFY, 0x012, fl_notify, \
> fl_notify_req, msg_rsp) \
> M(FL_GET_STATS, 0x013, fl_get_stats, \
> fl_get_stats_req, fl_get_stats_rsp) \
> +M(IFACE_GET_INFO, 0x014, iface_get_info, msg_req, \
> + iface_get_info_rsp) \
> +M(SWDEV2AF_NOTIFY, 0x015, swdev2af_notify, \
> + swdev2af_notify_req, msg_rsp) \
> /* CGX mbox IDs (range 0x200 - 0x3FF) */ \
> M(CGX_START_RXTX, 0x200, cgx_start_rxtx, msg_req, msg_rsp) \
> M(CGX_STOP_RXTX, 0x201, cgx_stop_rxtx, msg_req, msg_rsp) \
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> index 74c041ab5280a..1402beccf6618 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> @@ -1990,6 +1990,140 @@ int rvu_mbox_handler_msix_offset(struct rvu *rvu, struct msg_req *req,
> return 0;
> }
>
> +static void rvu_iface_get_qcnts(struct rvu *rvu, struct rvu_pfvf *pfvf,
> + struct iface_info *info)
> +{
> + struct admin_queue *aq;
> + unsigned long flags;
> +
> + info->sq_cnt = 0;
> + info->cq_cnt = 0;
> + info->rq_cnt = 0;
> +
> + aq = rvu->hw->block[pfvf->nix_blkaddr].aq;
> + if (!aq)
> + return;
> +
> + spin_lock_irqsave(&aq->lock, flags);
[Severity: Low]
Is the irqsave variant needed for this lock? Every pre-existing user of
block->aq->lock takes it with plain spin_lock(), for example
rvu_nix_blk_aq_enq_inst(), and that holder keeps the lock across
nix_aq_enqueue_wait(), which busy waits on AQ completion for up to its
1000 iteration timeout.
rvu_iface_get_qcnts() is called once per reported interface, so up to
IFACE_MAX (288) acquisitions per IFACE_GET_INFO message, each of which can
spin with interrupts disabled for that polling window. Would plain
spin_lock() be enough here, matching the existing users?
> +
> + /* Use each LF queue context size; bitmaps are sized to qsize longs. */
> + if (pfvf->sq_ctx && pfvf->sq_bmap)
> + info->sq_cnt = bitmap_weight(pfvf->sq_bmap, pfvf->sq_ctx->qsize);
> + if (pfvf->cq_ctx && pfvf->cq_bmap)
> + info->cq_cnt = bitmap_weight(pfvf->cq_bmap, pfvf->cq_ctx->qsize);
> + if (pfvf->rq_ctx && pfvf->rq_bmap)
> + info->rq_cnt = bitmap_weight(pfvf->rq_bmap, pfvf->rq_ctx->qsize);
> +
> + spin_unlock_irqrestore(&aq->lock, flags);
> +}
> +
> +int rvu_mbox_handler_iface_get_info(struct rvu *rvu, struct msg_req *req,
> + struct iface_get_info_rsp *rsp)
> +{
> + struct iface_info *info;
> + bool truncated = false;
> + struct rvu_pfvf *pfvf;
> + int pf, vf, numvfs;
> + int tot = 0;
> + u16 pcifunc;
> + u64 cfg;
> +
> + /* Read-only topology snapshot for switch software; any PF/VF may
> + * request it. Only channel and queue counts already visible to the
> + * requester through AF are reported.
> + */
[Severity: High]
Should this mailbox be restricted to the switch pcifunc? The handler never
looks at req->hdr.pcifunc, and the loop below walks every PF and every VF,
returning each function's pcifunc id, NPC rx/tx channel base and counts, tx
link, NIX block and live sq/cq/rq counts.
MBOX_MSG_IFACE_GET_INFO is registered in the generic MBOX_MESSAGES list, so
any RVU PF or VF can send it, including a VF assigned to an untrusted guest.
Is the second sentence of the comment accurate? The rvu_pfvf state of
*other* functions is AF private, so the requester has no other way to read
another function's channel base or queue counts. Other AF handlers that
touch a different function's state gate on npc_mcam_verify_entry() or
is_pffunc_map_valid(), and the SWDEV2AF_NOTIFY handler added later in the
series gates on rvu_is_switch_pcifunc(). Would the same kind of gate fit
here?
> + rsp->cnt = 0;
> + rsp->truncated = 0;
> + memset(rsp->rsvd, 0, sizeof(rsp->rsvd));
> + /* Preserve mbox_msghdr fields pre-filled by the mbox framework. */
> + memset(rsp->info, 0, sizeof(rsp->info));
> + info = rsp->info;
> + for (pf = 0; pf < rvu->hw->total_pfs; pf++) {
> + if (tot >= IFACE_MAX) {
> + truncated = true;
> + goto done;
> + }
> +
> + cfg = rvu_read64(rvu, BLKADDR_RVUM, RVU_PRIV_PFX_CFG(pf));
> + numvfs = (cfg >> 12) & 0xFF;
> +
> + /* Skip not enabled PFs */
> + if (!(cfg & BIT_ULL(20)))
> + goto chk_vfs;
> +
> + /* If Admin function, check on VFs */
> + if (cfg & BIT_ULL(21))
> + goto chk_vfs;
> +
> + pcifunc = rvu_make_pcifunc(rvu->pdev, pf, 0);
> + pfvf = rvu_get_pfvf(rvu, pcifunc);
> +
> + /* Populate iff at least one Tx channel */
> + if (!pfvf->tx_chan_cnt)
> + goto chk_vfs;
> +
> + info->is_vf = 0;
> + info->pcifunc = pcifunc;
> + info->rx_chan_base = pfvf->rx_chan_base;
> + info->rx_chan_cnt = pfvf->rx_chan_cnt;
> + info->tx_chan_base = pfvf->tx_chan_base;
> + info->tx_chan_cnt = pfvf->tx_chan_cnt;
[Severity: Medium]
Can this snapshot of another function's rvu_pfvf be torn? tx_chan_cnt,
rx_chan_base/cnt, tx_chan_base/cnt and nix_blkaddr of PF/VF X are written
by X's own NIX_LF_ALLOC / NIX_LF_FREE processing in nix_interface_init()
and the teardown path, which runs on a different mailbox worker, and
nothing here is serialized against it:
if (!pfvf->tx_chan_cnt)
goto chk_vfs;
...
info->rx_chan_base = pfvf->rx_chan_base;
info->tx_chan_base = pfvf->tx_chan_base;
so the interface can be reported because tx_chan_cnt was still non-zero,
with channel bases that the other path has already reprogrammed.
Related, rvu_iface_get_qcnts() picks which lock to take from an unlocked
read of pfvf->nix_blkaddr:
aq = rvu->hw->block[pfvf->nix_blkaddr].aq;
if (!aq)
return;
spin_lock_irqsave(&aq->lock, flags);
On a part with NIX0 and NIX1, if that field does not agree with the block
whose aq->lock nix_ctx_free() takes, the pfvf->sq_ctx->qsize dereference is
not protected against the qmem_free() there. Would taking rvu->rsrc_lock
around the whole walk, or deriving the lock from state that is stable under
the lock being taken, close both of these?
> + info->tx_link = nix_get_tx_link(rvu, pcifunc);
[Severity: Medium]
Is nix_get_tx_link() the right source for the value exported here? For LBK
VFs it ignores pfvf->lbkid and always returns the first LBK link:
if (is_lbk_vf(rvu, pcifunc)) {/* LBK links */
return hw->cgx_links;
while nix_interface_init() programs and reports the link as
hw->cgx_links + lbkid, with lbkid = vf & 0x1 ? 0 : 1 when
rvu->hw->lbk_links > 1. On a part with more than one LBK link, does this
collapse LBK1 onto LBK0 and contradict the tx_link the same AF already
returned to that interface in its NIX_LF_ALLOC response?
nix_get_tx_link() also has no is_rep_dev() case, while nix_get_txschq_range()
treats representors as LBK, so a representor transmitting over
RVU_SWITCH_LBK_CHAN gets reported with a CGX or SDP link. Should the new
consumer derive tx_link from pfvf->lbkid plus the representor mapping
instead?
> + if (is_sdp_pfvf(rvu, pcifunc))
> + info->is_sdp = 1;
> +
> + rvu_iface_get_qcnts(rvu, pfvf, info);
> +
> + if (pfvf->nix_blkaddr == BLKADDR_NIX0)
> + info->nix = 0;
> + else
> + info->nix = 1;
> +
> + info++;
> + tot++;
> +
> chk_vfs:
[ ... VF loop repeats the same reads ... ]
> +done:
> + rsp->cnt = tot;
> + rsp->truncated = truncated;
> +
> + return 0;
> +}
> +
> int rvu_mbox_handler_free_rsrc_cnt(struct rvu *rvu, struct msg_req *req,
> struct free_rsrcs_rsp *rsp)
> {
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> index 153eb57bad067..b8f4ad160afcf 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> @@ -912,33 +911,78 @@ static void nix_setup_lso(struct rvu *rvu, struct nix_hw *nix_hw, int blkaddr)
> nix_hw->lso.in_use++;
> }
>
> +static void nix_qctx_assign(struct rvu *rvu, int blkaddr, struct qmem **ctx,
> + unsigned long **bmap, struct qmem *new_ctx,
> + unsigned long *new_bmap)
> +{
> + struct admin_queue *aq = rvu->hw->block[blkaddr].aq;
> + unsigned long flags;
> +
> + if (!aq)
> + return;
> +
> + spin_lock_irqsave(&aq->lock, flags);
> + *ctx = new_ctx;
> + *bmap = new_bmap;
> + spin_unlock_irqrestore(&aq->lock, flags);
> +}
> +
> +static void nix_ctx_assign(struct rvu *rvu, struct qmem **ctx,
> + struct qmem *new_ctx)
> +{
> + mutex_lock(&rvu->rsrc_lock);
> + *ctx = new_ctx;
> + mutex_unlock(&rvu->rsrc_lock);
> +}
[Severity: Medium]
nix_qctx_assign() returns void and gives up silently when
rvu->hw->block[blkaddr].aq is NULL, but by the time it is called the
caller has already done all the allocating. In
rvu_mbox_handler_nix_lf_alloc() the only references to the freshly
qmem_alloc()'d rq_ctx/sq_ctx/cq_ctx and the kcalloc()'d
rq_bmap/sq_bmap/cq_bmap are the locals, so if the assignment is
skipped pfvf->rq_ctx and friends stay NULL, the handler keeps going
and returns 0, and nothing ever frees the DMA memory or the bitmaps:
nix_ctx_free() at teardown only looks at the pfvf->* pointers, which
are still NULL. Worse, NIX_AF_LFX_RQS_BASE / _SQS_BASE / _CQS_BASE
have already been programmed with those iovas at that point, so the
LF is left pointing at memory the driver no longer tracks, and it is
only reclaimed on AF unbind (devm) or never (kcalloc).
The same !aq test in nix_ctx_free() compounds it from the other side,
since that path then frees nothing at all.
Can block[blkaddr].aq actually be NULL here? blkaddr came from
rvu_get_blkaddr(rvu, BLKTYPE_NIX, pcifunc) a few lines earlier and the
LF was reset via rvu_lf_reset(), so if the AQ is guaranteed to exist
for an implemented NIX block, the check reads as dead code and a
WARN_ON_ONCE() would document that better. If it can be NULL, should
nix_qctx_assign() return int so the caller can qmem_free(new_ctx),
kfree(new_bmap) and goto free_mem with an error instead of leaking
and reporting success?
[Severity: Medium]
Do these two helpers protect the same fields with different locks?
nix_ctx_assign() publishes pfvf->rss_ctx, pfvf->cq_ints_ctx and
pfvf->nix_qints_ctx under rvu->rsrc_lock, while nix_ctx_free() below reads
and NULLs exactly those three fields under
rvu->hw->block[pfvf->nix_blkaddr].aq->lock and then calls qmem_free() on
them. The two lock sets are disjoint, so the publish path and the teardown
path do not exclude each other, and no reader of those three fields takes
rvu->rsrc_lock.
rvu_mbox_handler_nix_lf_alloc() runs on the mailbox worker, and
nix_ctx_free() is reached from rvu_nix_lf_teardown() on rvu->flr_wq, so if
an FLR lands while NIX_LF_ALLOC is in flight:
teardown (aq->lock) alloc (rsrc_lock)
rss_ctx = pfvf->rss_ctx; /* still NULL */
pfvf->rss_ctx = NULL;
pfvf->rss_ctx = rss_ctx;
qmem_free(...) /* nothing freed */
either the freshly published qmem is left attached to a destroyed LF and
never freed, or pfvf->rss_ctx ends up pointing at memory already passed to
qmem_free(), which the NIX_AQ_ENQ path only checks with
if (!pfvf->rss_ctx). Since nix_qctx_assign() already uses aq->lock for
rq/sq/cq, should all six pointers use that one lock?
> static void nix_ctx_free(struct rvu *rvu, struct rvu_pfvf *pfvf)
> {
> - kfree(pfvf->rq_bmap);
> - kfree(pfvf->sq_bmap);
> - kfree(pfvf->cq_bmap);
> - if (pfvf->rq_ctx)
> - qmem_free(rvu->dev, pfvf->rq_ctx);
> - if (pfvf->sq_ctx)
> - qmem_free(rvu->dev, pfvf->sq_ctx);
> - if (pfvf->cq_ctx)
> - qmem_free(rvu->dev, pfvf->cq_ctx);
> - if (pfvf->rss_ctx)
> - qmem_free(rvu->dev, pfvf->rss_ctx);
> - if (pfvf->nix_qints_ctx)
> - qmem_free(rvu->dev, pfvf->nix_qints_ctx);
> - if (pfvf->cq_ints_ctx)
> - qmem_free(rvu->dev, pfvf->cq_ints_ctx);
> + struct admin_queue *aq = rvu->hw->block[pfvf->nix_blkaddr].aq;
> + unsigned long *rq_bmap, *sq_bmap, *cq_bmap;
> + struct qmem *rq_ctx, *sq_ctx, *cq_ctx;
> + struct qmem *rss_ctx, *nix_qints_ctx, *cq_ints_ctx;
> + unsigned long flags;
> +
> + if (!aq)
> + return;
[Severity: Medium]
Does this early return skip the frees? Previously nix_ctx_free() released
the three bitmaps and all six qmem allocations unconditionally; now nothing
is freed when rvu->hw->block[pfvf->nix_blkaddr].aq is NULL. Is aq
guaranteed non-NULL for every caller of nix_ctx_free(), including the
teardown path for a NIX block with no admin queue?
> +
> + spin_lock_irqsave(&aq->lock, flags);
> + rq_bmap = pfvf->rq_bmap;
> + sq_bmap = pfvf->sq_bmap;
> + cq_bmap = pfvf->cq_bmap;
> + rq_ctx = pfvf->rq_ctx;
> + sq_ctx = pfvf->sq_ctx;
> + cq_ctx = pfvf->cq_ctx;
> + rss_ctx = pfvf->rss_ctx;
> + nix_qints_ctx = pfvf->nix_qints_ctx;
> + cq_ints_ctx = pfvf->cq_ints_ctx;
>
> pfvf->rq_bmap = NULL;
> - pfvf->cq_bmap = NULL;
> pfvf->sq_bmap = NULL;
> + pfvf->cq_bmap = NULL;
> pfvf->rq_ctx = NULL;
> pfvf->sq_ctx = NULL;
> pfvf->cq_ctx = NULL;
> pfvf->rss_ctx = NULL;
> pfvf->nix_qints_ctx = NULL;
> pfvf->cq_ints_ctx = NULL;
> + spin_unlock_irqrestore(&aq->lock, flags);
> +
> + kfree(rq_bmap);
> + kfree(sq_bmap);
> + kfree(cq_bmap);
[Severity: High]
This is a pre-existing race, not introduced by this patch, but the new
convention here makes the bitmap users look protected when they are not.
rvu_nix_blk_aq_enq_inst() validates the context pointer *before* it takes
aq->lock:
case NIX_AQ_CTYPE_RQ:
if (!pfvf->rq_ctx || req->qidx >= pfvf->rq_ctx->qsize)
rc = NIX_AF_ERR_AQ_ENQUEUE;
and then, inside the lock, dereferences the bitmap:
if (req->ctype == NIX_AQ_CTYPE_RQ && req->rq.ena)
__set_bit(req->qidx, pfvf->rq_bmap);
If an FLR runs __rvu_flr_handler() -> rvu_nix_lf_teardown() ->
nix_ctx_free() on rvu->flr_wq while that mailbox message is in flight, the
pointer is NULLed under aq->lock and the bitmap is kfree()d right after the
unlock, so the __set_bit() is either a write through NULL or a write to
freed memory.
nix_lf_hwctx_disable() also reads pfvf->cq_ctx->qsize and caches the bmap
pointer with no lock, and cn20k/mbox_init.c:rvu_alloc_cint_qint_mem() still
assigns pfvf->cq_ints_ctx and pfvf->nix_qints_ctx directly rather than going
through nix_ctx_assign(). Should those accessors be converted in the same
patch that introduces the convention, so the pointer is fetched under the
lock that now guards it?
> + if (rq_ctx)
> + qmem_free(rvu->dev, rq_ctx);
> + if (sq_ctx)
> + qmem_free(rvu->dev, sq_ctx);
> + if (cq_ctx)
> + qmem_free(rvu->dev, cq_ctx);
> + if (rss_ctx)
> + qmem_free(rvu->dev, rss_ctx);
> + if (nix_qints_ctx)
> + qmem_free(rvu->dev, nix_qints_ctx);
> + if (cq_ints_ctx)
> + qmem_free(rvu->dev, cq_ints_ctx);
> }
>
> static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
> @@ -946,6 +990,7 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
> int rss_sz, int rss_grps, int hwctx_size,
> u64 way_mask, bool tag_lsb_as_adder)
> {
> + struct qmem *rss_ctx;
> int err, grp, num_indices;
> u64 val;
>
> @@ -955,12 +1000,12 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
> num_indices = rss_sz * rss_grps;
>
> /* Alloc NIX RSS HW context memory and config the base */
> - err = qmem_alloc(rvu->dev, &pfvf->rss_ctx, num_indices, hwctx_size);
> + err = qmem_alloc(rvu->dev, &rss_ctx, num_indices, hwctx_size);
> if (err)
> return err;
[Severity: Medium]
Does this leak the struct qmem descriptor when qmem_alloc() fails halfway?
qmem_alloc() publishes the devm_kzalloc()'d descriptor into *q before it
tries the DMA allocation:
af/common.h:qmem_alloc() {
*q = devm_kzalloc(dev, sizeof(*qmem), GFP_KERNEL);
if (!*q)
return -ENOMEM;
qmem = *q;
...
qmem->base = dma_alloc_attrs(dev, qmem->alloc_sz, &qmem->iova,
GFP_KERNEL, DMA_ATTR_FORCE_CONTIGUOUS);
if (!qmem->base)
return -ENOMEM;
}
so on dma_alloc_attrs() failure it returns -ENOMEM with a live descriptor
in the caller's pointer. Before the patch that pointer was
&pfvf->rss_ctx, so nix_ctx_free() -> qmem_free() -> devm_kfree() released
it. Now the descriptor is only in the local rss_ctx and the function
returns before nix_ctx_assign() publishes it, so nothing frees it until the
AF driver is unbound.
The same applies to the five sites in rvu_mbox_handler_nix_lf_alloc()
(rq_ctx, sq_ctx, cq_ctx, cq_ints_ctx, nix_qints_ctx): they goto free_mem,
and nix_ctx_free() only frees the pfvf->* pointers, which are still NULL.
Should these paths call qmem_free() on the local pointer, as the kcalloc()
failure paths already do?
>
> rvu_write64(rvu, blkaddr, NIX_AF_LFX_RSS_BASE(nixlf),
> - (u64)pfvf->rss_ctx->iova);
> + (u64)rss_ctx->iova);
>
[ ... ]
> @@ -974,6 +1019,8 @@ static int nixlf_rss_ctx_init(struct rvu *rvu, int blkaddr,
> for (grp = 0; grp < rss_grps; grp++)
> rvu_write64(rvu, blkaddr, NIX_AF_LFX_RSS_GRPX(nixlf, grp),
> ((ilog2(rss_sz) - 1) << 16) | (rss_sz * grp));
> +
> + nix_ctx_assign(rvu, &pfvf->rss_ctx, rss_ctx);
> return 0;
> }
>
[ ... ]
> @@ -1584,59 +1634,68 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
>
> /* Alloc NIX RQ HW context memory and config the base */
> hwctx_size = 1UL << ((ctx_cfg >> 4) & 0xF);
> - rc = qmem_alloc(rvu->dev, &pfvf->rq_ctx, req->rq_cnt, hwctx_size);
> + rc = qmem_alloc(rvu->dev, &rq_ctx, req->rq_cnt, hwctx_size);
> if (rc)
> goto free_mem;
>
> - pfvf->rq_bmap = kcalloc(req->rq_cnt, sizeof(long), GFP_KERNEL);
> - if (!pfvf->rq_bmap) {
> + rq_bmap = kcalloc(req->rq_cnt, sizeof(long), GFP_KERNEL);
> + if (!rq_bmap) {
> + qmem_free(rvu->dev, rq_ctx);
> rc = -ENOMEM;
> goto free_mem;
> }
[ ... ]
> @@ -1649,29 +1708,33 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
> cfg = rvu_read64(rvu, blkaddr, NIX_AF_CONST2);
> qints = (cfg >> 24) & 0xFFF;
> hwctx_size = 1UL << ((ctx_cfg >> 24) & 0xF);
> - rc = qmem_alloc(rvu->dev, &pfvf->cq_ints_ctx, qints, hwctx_size);
> + rc = qmem_alloc(rvu->dev, &cq_ints_ctx, qints, hwctx_size);
> if (rc)
> goto free_mem;
>
> rvu_write64(rvu, blkaddr, NIX_AF_LFX_CINTS_BASE(nixlf),
> - (u64)pfvf->cq_ints_ctx->iova);
> + (u64)cq_ints_ctx->iova);
>
> rvu_write64(rvu, blkaddr, NIX_AF_LFX_CINTS_CFG(nixlf),
> BIT_ULL(36) | req->way_mask << 20);
>
> + nix_ctx_assign(rvu, &pfvf->cq_ints_ctx, cq_ints_ctx);
> +
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
> index 60922944675b6..c115601b1212d 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc.c
> @@ -3545,6 +3545,46 @@ int rvu_mbox_handler_npc_mcam_free_entry(struct rvu *rvu,
> return rc;
> }
>
> +int rvu_mbox_handler_npc_flow_del_n_free(struct rvu *rvu,
> + struct npc_flow_del_n_free_req *mreq,
> + struct msg_rsp *rsp)
> +{
[ ... ]
> + for (i = 0; i < cnt; i++) {
> + dreq.entry = entry[i];
> + rvu_mbox_handler_npc_delete_flow(rvu, &dreq, &drsp);
> +
> + sreq.entry = entry[i];
> + ret = rvu_mbox_handler_npc_mcam_free_entry(rvu, &sreq, rsp);
> + if (ret) {
> + dev_err(rvu->dev, "free entry error for i=%d entry=%d\n",
> + i, entry[i]);
> + err = true;
> + }
> + }
[Severity: Medium]
Should this print be ratelimited? cnt comes from the request and can be up
to 256, and rvu_mbox_handler_npc_mcam_free_entry() returns an error for
every entry the requester does not own (npc_mcam_verify_entry()), so one
NPC_MCAM_FLOW_DEL_N_FREE message from any PF/VF can emit 256 dev_err()
lines, repeatable in a loop.
The cnt validation a few lines above already uses dev_err_ratelimited().
Would dev_err_ratelimited() or dev_dbg() be better here too?
> +
> + return err ? -EINVAL : 0;
> +}
> +
[ ... ]
> @@ -4444,6 +4484,81 @@ int rvu_mbox_handler_npc_mcam_entry_stats(struct rvu *rvu,
> return 0;
> }
>
> +int rvu_mbox_handler_npc_mcam_mul_stats(struct rvu *rvu,
> + struct npc_mcam_get_mul_stats_req *req,
> + struct npc_mcam_get_mul_stats_rsp *rsp)
> +{
[ ... ]
> + for (i = 0; i < req_cnt; i++) {
> + mcam_entry = npc_cn20k_vidx2idx(entry[i]);
> +
> + if (npc_mcam_verify_entry(mcam, pcifunc, mcam_entry)) {
> + mutex_unlock(&mcam->lock);
> + dev_err(rvu->dev, "%s invalid mcam index=%d\n",
> + __func__, entry[i]);
> + return -EINVAL;
> + }
[Severity: Medium]
Same question here: entry[i] is taken straight from the request, so any
PF/VF can drive this dev_err() at will by passing an index it does not own,
while the req_cnt check above uses dev_err_ratelimited(). Could this one be
ratelimited as well?
> +
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,3/8] octeontx2-pf: switch: Add pf files hierarchy
2026-08-31 13:19 ` [PATCH v9 net-next 3/8] octeontx2-pf: switch: Add pf files hierarchy Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:
Critical: 0 · High: 0 · Medium: 0 · Low: 1
- [Low] Scaffolding-only commit: the new user-visible Kconfig symbol
OCTEONTX_SWITCH (drivers/net/ethernet/marvell/octeontx2/Kconfig:31-39)…
--
octeontx2-pf: switch: Add pf files hierarchy
This adds a new user-visible CONFIG_OCTEONTX_SWITCH symbol, creates the
nic/switch/ directory, and links the new stub objects into the rvu_nicpf
module. All of the new sw_* init/deinit and notifier entry points are
empty bodies returning 0.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/Kconfig b/drivers/net/ethernet/marvell/octeontx2/Kconfig
> index 47e549c581f0d..e2fb6dd71078b 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/Kconfig
> +++ b/drivers/net/ethernet/marvell/octeontx2/Kconfig
> @@ -28,6 +28,16 @@ config NDC_DIS_DYNAMIC_CACHING
> , NPA stack pages etc in NDC. Also locks down NIX SQ/CQ/RQ/RSS and
> NPA Aura/Pool contexts.
>
> +config OCTEONTX_SWITCH
> + bool "Marvell OcteonTX2 switch driver"
> + depends on (64BIT && COMPILE_TEST) || ARM64
> + depends on OCTEONTX2_PF
> + default n
> + help
> + This driver supports Marvell's OcteonTX2 switch.
> + Marvell SWITCH HW can offload L2, L3 flow. ARM core interacts
> + with Marvell SW HW thru mbox.
> +
[Severity: Low]
This isn't a bug, but the help text promises behaviour that this commit on
its own does not provide. At this commit the option only adds two empty
object files: every function it enables is a stub, and nothing in the tree
calls sw_fdb_init(), sw_fdb_deinit(), sw_fl_init(), sw_fl_deinit(),
otx2_sw_fib_init(), otx2_sw_fib_deinit(), otx2_sw_nb_register() or
otx2_sw_nb_unregister().
A grep at this commit for those symbols matches only the newly added
switch/*.c and switch/*.h files, so turning CONFIG_OCTEONTX_SWITCH=y has no
runtime effect here. The real bodies and the call sites appear later in the
series, where otx2_sw_nb_register() registers the notifiers and calls
sw_fdb_init(), otx2_sw_fib_init() and sw_fl_init().
Would it be preferable to introduce the Kconfig symbol in the patch that
first wires up the functionality, so that no released bisection point offers
a switch option that does nothing? Alternatively, the help text could say
what is actually offloaded once the series is complete.
Two small nits in the same text: "Marvell SW HW" looks like it should be
"Marvell switch HW", "thru" should be "through", and "default n" is the
Kconfig default and can be dropped.
> config OCTEONTX2_PF
> tristate "Marvell OcteonTX2 NIC Physical Function driver"
> select OCTEONTX2_MBOX
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
> new file mode 100644
> index 0000000000000..500451e85b503
> --- /dev/null
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
> @@ -0,0 +1,19 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Marvell RVU switch driver
> + *
> + * Copyright (C) 2026 Marvell.
> + *
> + */
> +#include "sw_fdb.h"
> +
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> +int sw_fdb_init(void)
> +{
> + return 0;
> +}
> +
> +void sw_fdb_deinit(void)
> +{
> +}
> +
> +#endif
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> new file mode 100644
> index 0000000000000..426a420119301
> --- /dev/null
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> @@ -0,0 +1,21 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Marvell RVU switch driver
> + *
> + * Copyright (C) 2026 Marvell.
> + *
> + */
> +#include "sw_nb.h"
> +
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> +
> +int otx2_sw_nb_unregister(void)
> +{
> + return 0;
> +}
> +
> +int otx2_sw_nb_register(void)
> +{
> + return 0;
> +}
> +
> +#endif
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,4/8] octeontx2-af: switch: Representor for switch port
2026-08-31 13:19 ` [PATCH v9 net-next 4/8] octeontx2-af: switch: Representor for switch port Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 4 potential
issue(s) to consider:
Critical: 0 · High: 2 (2) · Medium: 1 · Low: 1
- [High] esw_cfg_req_has_switch_id()
(drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c:442-455) derives…
- [High] Lockless access to rvu->rep2pfvf_map in rvu_sw_port_id() races
with GET_REP_CNT mailbox handler updates, leading to a NULL pointer…
- [Medium] struct esw_cfg_req — an existing AF<->PF mailbox wire-format
message carried in the shared mailbox window — grows a 32-byte…
- [Low] The patch adds rvu_sw_port_id(), RVU_SWITCH_FLAG_FW_READY and the
rvu_switch.flags/pcifunc fields with no producer or consumer at this…
Pre-existing issues:
- [High] rvu_mbox_handler_get_rep_cnt() (af/rvu_rep.c:474-507, unchanged
at the end of the series) re-allocates rvu->rep2pfvf_map with…
- [High] The new switch_id ingestion and rvu_sw_port_id() build on
rvu->rep_pcifunc, rvu->rep_cnt and rvu->rep2pfvf_map, all of which any…
--
octeontx2-af: switch: Representor for switch port
This adds a devlink-derived switch id to the esw_cfg mailbox message, copies
it into rvu->rswitch on the AF side, and adds rvu_sw_port_id() plus an export
of rvu_rep_get_vlan_id(). A few questions below about the new mailbox field
and the state the new helper depends on.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> index e45e6e93ed087..8e3850f33751d 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> @@ -1841,6 +1841,7 @@ struct esw_cfg_req {
> struct mbox_msghdr hdr;
> u8 ena;
> u64 rsvd;
> + unsigned char switch_id[MAX_PHYS_ITEM_ID_LEN];
> };
[Severity: Medium]
This changes the layout of an existing AF<->PF mailbox message, growing
struct esw_cfg_req from 32 to 64 bytes, but OTX2_MBOX_VERSION in the same
header stays at 0x000a.
The existing precedent in this driver for a message layout extension is to
gate on the header version, for example in af/rvu.c:
if (attach->hdr.ver < RVU_MULTI_BLK_VER)
and rvu_process_mbox_msg() only validates req->sig, never req->ver. With
the version unchanged, an old PF and a new AF both advertise ver = 0x000a
for two incompatible layouts. Should OTX2_MBOX_VERSION be bumped and
checked here instead?
Two related points on the field itself:
The on-the-wire size of this message is now derived from
MAX_PHYS_ITEM_ID_LEN, a core netdevice constant outside this driver's
control. Is it intentional that the mailbox layout follows that constant?
No length accompanies the value. rvu_rep_devlink_set_switch_id() in
nic/rep.c sets ppid->id_len = sizeof(u64), and rvu_eswitch_config() copies
only that many bytes:
memcpy(req->switch_id, attrs.switch_id.id, attrs.switch_id.id_len);
while the AF stores and would compare all MAX_PHYS_ITEM_ID_LEN bytes. How
does the AF recover the meaningful prefix, i.e. should an id_len companion
be carried in the message and in struct rvu_switch?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
> index 2876c76ae61b6..9174b879850a2 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.h
> @@ -576,6 +576,10 @@ struct rvu_switch {
> u16 *entry2pcifunc;
> u16 mode;
> u16 start_entry;
> + unsigned char switch_id[MAX_PHYS_ITEM_ID_LEN];
> +#define RVU_SWITCH_FLAG_FW_READY BIT_ULL(0)
> + u64 flags;
> + u16 pcifunc;
> };
[Severity: Low]
This isn't a bug, but rvu_sw_port_id(), RVU_SWITCH_FLAG_FW_READY and
rvu_switch.pcifunc have no producer or consumer at this commit; they do gain
users in the later patches of the series, which reads like normal series
decomposition.
The one field that stays unused through the end of the series is
rvu_switch.switch_id: it is written by rvu_mbox_handler_esw_cfg() and never
read anywhere. Would it be clearer to add that store together with the code
that consumes it?
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
> index a2781e0f504e3..672d54847c7b1 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c
[ ... ]
> @@ -189,7 +190,7 @@ int rvu_mbox_handler_nix_lf_stats(struct rvu *rvu,
> return 0;
> }
>
> -static u16 rvu_rep_get_vlan_id(struct rvu *rvu, u16 pcifunc)
> +u16 rvu_rep_get_vlan_id(struct rvu *rvu, u16 pcifunc)
> {
> int id;
>
> @@ -429,6 +430,30 @@ int rvu_rep_pf_init(struct rvu *rvu)
> return 0;
> }
>
> +/* ESW_CFG is always the sole message in a mailbox transaction.
> + *
> + * The otx2 mailbox API does not batch multiple messages per sync: the
> + * representor driver allocates only ESW_CFG before calling
> + * otx2_sync_mbox_msg() (see rvu_eswitch_config()), and the AF processes
> + * one message per dispatch. next_msgoff is therefore the end offset of this
> + * message, not a cumulative offset across batched messages, so the length
> + * check below is safe. Batching is not supported; do not flag this path.
> + */
> +static bool esw_cfg_req_has_switch_id(const struct esw_cfg_req *req)
> +{
> + u16 hdr_len = ALIGN(sizeof(struct mbox_hdr), MBOX_MSG_ALIGN);
> + u16 next_off = req->hdr.next_msgoff;
> + u16 msg_len;
> +
> + if (next_off < hdr_len)
> + return false;
> +
> + msg_len = next_off - hdr_len;
> +
> + return msg_len >= offsetof(struct esw_cfg_req, switch_id) +
> + MAX_PHYS_ITEM_ID_LEN;
> +}
> +
> int rvu_mbox_handler_esw_cfg(struct rvu *rvu, struct esw_cfg_req *req,
> struct msg_rsp *rsp)
> {
> @@ -436,6 +461,9 @@ int rvu_mbox_handler_esw_cfg(struct rvu *rvu, struct esw_cfg_req *req,
> return 0;
>
> rvu->rep_mode = req->ena;
> + if (esw_cfg_req_has_switch_id(req))
> + memcpy(rvu->rswitch.switch_id, req->switch_id,
> + MAX_PHYS_ITEM_ID_LEN);
[Severity: High]
Is the premise of the comment above esw_cfg_req_has_switch_id() correct?
otx2_mbox_alloc_msg_rsp() in af/mbox.c derives next_msgoff from the running
total of the whole mailbox region:
mdev->msg_size += size;
mdev->rsp_size += size_rsp;
msghdr->next_msgoff = mdev->msg_size + msgs_offset;
and __rvu_mbox_handler() in af/rvu.c walks a batch of messages per doorbell
using it:
for (id = 0; id < mw->mbox_wrk[devid].num_msgs; id++) {
msg = mdev->mbase + offset;
...
offset = mbox->rx_start + msg->next_msgoff;
}
So if ESW_CFG is not the first message in a transaction, wouldn't
msg_len = next_msgoff - hdr_len exceed this message's real size and let the
check pass for an old 32-byte esw_cfg_req?
Beyond batching, next_msgoff lives in the shared mailbox window and is
written by the peer. rvu_process_mbox_msg() checks only req->sig, and
otx2_pfvf_mbox_handler() in nic/otx2_pf.c forwards a VF's headers after
rewriting just pcifunc:
if (msg->sig != OTX2_MBOX_REQ_SIG)
goto inval_msg;
msg->pcifunc &= ~RVU_PFVF_FUNC_MASK;
msg->pcifunc |= (vf_idx + 1) & RVU_PFVF_FUNC_MASK;
offset = msg->next_msgoff;
with otx2_forward_vf_mbox_msgs() handing the VF's own mailbox memory to the
AF verbatim (dst_mdev->mbase = src_mdev->mbase).
Can a VF that writes next_msgoff >= 80 and sends a 32-byte ESW_CFG make the
memcpy above read 32 bytes past the end of its message, picking up the
following message's header and payload, or bytes past the last message and
potentially past the per-device mailbox window, and persist them as the AF's
switch identity?
Since hdr.ver / OTX2_MBOX_VERSION exists to signal exactly this kind of
layout skew, would a version check be the safer bound here rather than a
peer-supplied offset?
One more note on the comment: the closing sentence "Batching is not
supported; do not flag this path" instructs readers not to inspect this
code. Could that be dropped?
>
> if (!rvu->rep_mode)
> rvu_npc_free_mcam_entries(rvu, req->hdr.pcifunc, -1);
> @@ -449,6 +477,9 @@ int rvu_mbox_handler_get_rep_cnt(struct rvu *rvu, struct msg_req *req,
> int pf, vf, numvfs, hwvf, rep = 0;
> u16 pcifunc;
>
> + /* Called once from representor driver probe during devlink eswitch
> + * SWITCHDEV bring-up; not re-run during switch device operation.
> + */
> rvu->rep_pcifunc = req->hdr.pcifunc;
> rsp->rep_cnt = rvu->cgx_mapped_pfs + rvu->cgx_mapped_vfs;
> rvu->rep_cnt = rsp->rep_cnt;
[Severity: High]
This isn't a bug introduced by this patch, but the new comment states this
handler is called once, and the new rvu_sw_port_id() is documented as relying
on that. Is the invariant enforced anywhere?
rvu_mbox_handler_get_rep_cnt() re-runs on every MBOX_MSG_GET_REP_CNT:
rvu->rep_cnt = rsp->rep_cnt;
rvu->rep2pfvf_map = devm_kzalloc(rvu->dev, rvu->rep_cnt *
sizeof(u16), GFP_KERNEL);
if (!rvu->rep2pfvf_map)
return -ENOMEM;
Each repetition drops the previous pointer without devm_kfree(), and the old
buffer is devres-managed against the AF device, so it is only released when
the AF itself is removed. Does a representor PF unbind/rebind or module
reload, or a VF repeating the message, grow the devres list without bound?
On the -ENOMEM path, rvu->rep_cnt is already published while
rvu->rep2pfvf_map is NULL, and rvu_rep_get_vlan_id() dereferences the map
with no NULL check:
for (id = 0; id < rvu->rep_cnt; id++)
if (rvu->rep2pfvf_map[id] == pcifunc)
return id;
Can that NULL-dereference in the pre-existing AF callers of
rvu_rep_get_vlan_id() after a failed re-initialization? Note that
rvu_sw_port_id() checks rvu->rep2pfvf_map itself, but its callee does not.
[Severity: High]
This is a pre-existing issue, but the new switch_id ingestion and
rvu_sw_port_id() build on rvu->rep_pcifunc, rvu->rep_cnt and
rvu->rep2pfvf_map, all of which this handler sets with no capability or
identity check:
rvu->rep_pcifunc = req->hdr.pcifunc;
rvu_process_mbox_msg() applies no per-message privilege filter, and
otx2_pfvf_mbox_handler() forwards raw VF message IDs after checking only
sig. Can a guest-owned VF send MBOX_MSG_GET_REP_CNT to become
rvu->rep_pcifunc, and then pass the gate in rvu_mbox_handler_esw_cfg():
if (req->hdr.pcifunc != rvu->rep_pcifunc)
return 0;
rvu->rep_mode = req->ena;
thereby setting the AF-global rvu->rep_mode, driving
rvu_npc_free_mcam_entries(), and reaching the new switch_id memcpy?
Also, rvu_afvf_mbox_handler() does not take rvu->mbox_lock while the AFPF
worker does. Can the AFVF path replace rvu->rep2pfvf_map concurrently with
the new lockless readers?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> index fe143ad3f9449..2451eb57ec4c8 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> @@ -5,7 +5,33 @@
> *
> */
>
> +#include <linux/bitfield.h>
> +
> #include "rvu.h"
> +#include "rvu_sw.h"
> +
> +/*
> + * rep_cnt and rep2pfvf_map are populated once when the representor driver
> + * probes via GET_REP_CNT (see rvu_get_rep_cnt() in rep.c), as part of
> + * devlink eswitch SWITCHDEV bring-up. They are not updated during switch
> + * device mailbox handling, so this lockless lookup cannot race with a
> + * concurrent rep2pfvf_map resize.
> + */
> +u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc)
> +{
> + u16 rep_id;
> +
> + if (!rvu->rep2pfvf_map || !rvu->rep_cnt)
> + return RVU_SW_INVALID_PORT_ID;
> +
> + rep_id = rvu_rep_get_vlan_id(rvu, pcifunc);
> + if (rep_id >= rvu->rep_cnt ||
> + rvu->rep2pfvf_map[rep_id] != pcifunc)
> + return RVU_SW_INVALID_PORT_ID;
> +
> + return FIELD_PREP(GENMASK_ULL(31, 16), rep_id) |
> + FIELD_PREP(GENMASK_ULL(15, 0), pcifunc);
> +}
[Severity: High]
The comment above this helper asserts that rep_cnt and rep2pfvf_map
cannot change under a reader, but rvu_mbox_handler_get_rep_cnt() runs
for every MBOX_MSG_GET_REP_CNT, and it publishes the new count before
the matching array exists:
rvu->rep_cnt = rsp->rep_cnt;
rvu->rep2pfvf_map = devm_kzalloc(rvu->dev, rvu->rep_cnt *
sizeof(u16), GFP_KERNEL);
if (!rvu->rep2pfvf_map)
return -ENOMEM;
So there is a window in which rvu->rep_cnt describes the new topology
while rvu->rep2pfvf_map still points at the previous, smaller buffer.
A reader that passes the entry test here then walks id < rvu->rep_cnt
in rvu_rep_get_vlan_id() and indexes rvu->rep2pfvf_map[rep_id] past the
end of the old allocation. The rep_id >= rvu->rep_cnt bound added in
this helper does not help, since rep_cnt is exactly the value that has
already been advanced.
The allocation failure path looks worse: rvu->rep2pfvf_map is
overwritten with NULL while rvu->rep_cnt stays non-zero, and nothing
re-checks the pointer after this function's own test - rvu_sw_port_id()
validates rvu->rep2pfvf_map on entry, but rvu_rep_get_vlan_id()
dereferences it unconditionally, so a store that lands between the two
turns the lookup into a NULL dereference.
Neither side takes a lock: rvu_afvf_mbox_handler() does not hold
rvu->mbox_lock, so a VF-originated GET_REP_CNT can execute the
reallocation in parallel with these reads on another CPU.
What is the intended serialization here? If GET_REP_CNT really can only
arrive once, could that be enforced (reject the message when
rvu->rep2pfvf_map is already set) rather than documented in a comment?
Otherwise the map and its count need to be published together under a
lock or as an RCU-swapped object, and rvu->rep_cnt should not be updated
before the allocation succeeds.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control
2026-08-31 13:19 ` [PATCH v9 net-next 5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 7 potential
issue(s) to consider:
Critical: 1 · High: 1 · Medium: 2 (1) · Low: 3
- [Critical] Missing scheduler-level validation on the new
TXSCHQ_FREE_PAN_TL1 path in nix_txschq_free_one()…
- [High] Partial-allocation leak in rvu_mbox_handler_nix_txsch_alloc()
(drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c):…
- [Medium] The commit message justifies the new fixed-size SQ bitmap with
"the same span used by bitmap_weight(..., BITS_PER_LONG * 16) in…
- [Medium] PAN TL1 scheduler nodes are returned to the resource pool
while still armed.
- [Low] Claim that rvu->rswitch.pcifunc has no writer, making
rvu_is_switch_pcifunc() permanently false and all new PAN/set_chanmask…
- [Low] The new mailbox ABI comments in mbox.h describe the PAN scheduler
queues as TL2 ("requests two aggregate-level TL2 queues on the PAN…
- [Low] The commit message states "without it, chan_mask was always
forced to 0xFFF for non-AF functions" and describes set_chanmask as…
Pre-existing issues:
- [Medium] In rvu_nix_blk_aq_enq_inst() the SQ index bounds computation
(the pfvf->sq_ctx NULL test, the sq_ctx->qsize read and the new…
--
octeontx2-af: switch: TL1 scheduling and NPC channel control
This adds a NIX_TXSCH_ALLOC_FLAG_PAN request flag so the switchdev PF can
get more than one aggregate-level scheduler queue on the PAN link, a
TXSCHQ_FREE_PAN_TL1 flag so those TL1 entries can be freed again, and a
set_chanmask field so the switchdev PF can keep its own NPC chan_mask.
It also changes the NIX LF SQ bitmap sizing and extends struct sg_list.
A few questions below.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> index 8e3850f33751d..2aa1aa6599a5a 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> @@ -1162,6 +1162,13 @@ struct nix_txsch_alloc_req {
> /* Scheduler queue count request at each level */
> u16 schq_contig[NIX_TXSCH_LVL_CNT]; /* No of contiguous queues */
> u16 schq[NIX_TXSCH_LVL_CNT]; /* No of non-contiguous queues */
> + /* Set only by the single switchdev PF (rvu->rswitch.pcifunc). This is
> + * not the eswitch representor (rvu->rep_pcifunc). That PF requests two
> + * aggregate-level TL2 queues on the PAN link, one for CGX and one for
> + * SDP steering. No other PF or VF sets this flag.
> + */
> +#define NIX_TXSCH_ALLOC_FLAG_PAN BIT(0)
> + u32 flags;
> };
[Severity: Low]
The comment says the flag requests "aggregate-level TL2 queues", but the
aggregation level is always TL1 here.
af/rvu.c:rvu_setup_hw_capabilities() has the only assignment:
hw->cap.nix_tx_aggr_lvl = NIX_TXSCH_LVL_TL1;
and every implementation site in this patch indexes
nix_hw->txsch[NIX_TXSCH_LVL_TL1]. The companion macro is even named
TXSCHQ_FREE_PAN_TL1.
Since mbox.h is the only contract a request producer has when choosing
nix_txsch_free_req.schq_lvl, and nix_txschq_free_one() never cross-checks
schq_lvl against the flag, would a producer following the TL2 prose end up
operating on a different scheduler level than one following the TL1 macro
name?
Could the comments say TL1, and could the "PAN link" also be defined
somewhere, along with why nix_get_pan_tx_link() is cgx_links + lbk_links + 1?
[Severity: Low]
This isn't a bug, but nothing in the tree sets NIX_TXSCH_ALLOC_FLAG_PAN,
TXSCHQ_FREE_PAN_TL1 or npc_install_flow_req.set_chanmask, even at the end of
this series, so the new mailbox fields have consumers in the AF but no
in-tree emitters. Are the producers coming in a follow-up series?
(For the record, rvu->rswitch.pcifunc itself does get a writer later in the
series in rvu_sw_l2_ctrl_work_handler(), so rvu_is_switch_pcifunc() is not
permanently false.)
> @@ -1180,6 +1187,10 @@ struct nix_txsch_alloc_rsp {
> struct nix_txsch_free_req {
> struct mbox_msghdr hdr;
> #define TXSCHQ_FREE_ALL BIT_ULL(0)
> + /* Frees PAN TL2 queues allocated with NIX_TXSCH_ALLOC_FLAG_PAN. Used
> + * only by the switchdev PF (rvu->rswitch.pcifunc), not by other PFs/VFs.
> + */
> +#define TXSCHQ_FREE_PAN_TL1 BIT_ULL(1)
> u16 flags;
> /* Scheduler queue level to be freed */
> u16 schq_lvl;
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> index 1402beccf6618..e4d13adc2896c 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu.c
> @@ -2006,9 +2007,18 @@ static void rvu_iface_get_qcnts(struct rvu *rvu, struct rvu_pfvf *pfvf,
>
> spin_lock_irqsave(&aq->lock, flags);
>
> - /* Use each LF queue context size; bitmaps are sized to qsize longs. */
> - if (pfvf->sq_ctx && pfvf->sq_bmap)
> - info->sq_cnt = bitmap_weight(pfvf->sq_bmap, pfvf->sq_ctx->qsize);
> + if (pfvf->sq_bmap) {
> + /* Match switchdev sq_bmap allocation size in nix_lf_alloc(). */
> + if (rvu_is_switch_pcifunc(rvu, pcifunc))
> + sq_bmap_bits = NIX_SQ_BMAP_BITS;
> + else if (pfvf->sq_ctx)
> + sq_bmap_bits = pfvf->sq_ctx->qsize;
> + else
> + sq_bmap_bits = 0;
> +
> + if (sq_bmap_bits)
> + info->sq_cnt = bitmap_weight(pfvf->sq_bmap, sq_bmap_bits);
> + }
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> index b8f4ad160afcf..9ee6531afbf94 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_nix.c
> @@ -1138,10 +1139,25 @@ static int rvu_nix_blk_aq_enq_inst(struct rvu *rvu, struct nix_hw *nix_hw,
> if (!pfvf->rq_ctx || req->qidx >= pfvf->rq_ctx->qsize)
> rc = NIX_AF_ERR_AQ_ENQUEUE;
> break;
> - case NIX_AQ_CTYPE_SQ:
> - if (!pfvf->sq_ctx || req->qidx >= pfvf->sq_ctx->qsize)
> + case NIX_AQ_CTYPE_SQ: {
> + if (!pfvf->sq_ctx) {
> + rc = NIX_AF_ERR_AQ_ENQUEUE;
> + break;
> + }
> +
> + /* Switchdev PF uses a fixed sq_bmap (NIX_SQ_BMAP_BITS); cap qidx
> + * to that span so __set_bit() cannot run past the allocation.
> + * nix_lf_alloc() also rejects sq_cnt above NIX_SQ_BMAP_BITS.
> + */
> + sq_bmap_bits = rvu_is_switch_pcifunc(rvu, pcifunc) ?
> + NIX_SQ_BMAP_BITS :
> + (u64)pfvf->sq_ctx->qsize * BITS_PER_LONG;
> + max_q = min_t(u64, pfvf->sq_ctx->qsize, sq_bmap_bits);
> +
> + if ((u64)req->qidx >= max_q)
> rc = NIX_AF_ERR_AQ_ENQUEUE;
> break;
> + }
[Severity: Medium]
This isn't a bug introduced by this patch, but the comment claims a property
the locking does not provide.
The bounds computation above (the pfvf->sq_ctx test, the qsize read and
max_q) runs with no lock held, while the mutation it is supposed to bound
runs later in the same function under aq->lock, with no re-validation and no
NULL check:
spin_lock(&aq->lock);
...
if (req->ctype == NIX_AQ_CTYPE_SQ && req->sq.ena)
__set_bit(req->qidx, pfvf->sq_bmap);
Those are exactly the fields nix_ctx_free() clears under the same lock and
frees immediately after releasing it:
spin_lock_irqsave(&aq->lock, flags);
...
pfvf->sq_bmap = NULL;
pfvf->sq_ctx = NULL;
...
spin_unlock_irqrestore(&aq->lock, flags);
kfree(sq_bmap);
...
if (sq_ctx)
qmem_free(rvu->dev, sq_ctx);
The unlocked-validate/locked-mutate split predates this patch (the baseline
read was "if (!pfvf->sq_ctx || req->qidx >= pfvf->sq_ctx->qsize)"), so this
is only extending it. I could not prove that AF mailbox AQ processing for a
pcifunc can actually run concurrently with that pcifunc's teardown, so this
may be unreachable. If it is unreachable, could the comment avoid asserting
that __set_bit() "cannot run past the allocation", since the check and the
write are not in the same critical section?
> @@ -1566,18 +1582,28 @@ int rvu_mbox_handler_nix_lf_alloc(struct rvu *rvu,
> if (!req->rq_cnt || !req->sq_cnt || !req->cq_cnt)
> return NIX_AF_ERR_PARAM;
>
> + /* Switchdev PF sq_bmap is fixed at NIX_SQ_BMAP_BITS; reject larger
> + * sq_cnt before allocating context memory or the bitmap.
> + */
> + sq_bmap_bits = rvu_is_switch_pcifunc(rvu, pcifunc) ?
> + NIX_SQ_BMAP_BITS :
> + (u64)req->sq_cnt * BITS_PER_LONG;
> +
> + if ((u64)req->sq_cnt > sq_bmap_bits)
> + return NIX_AF_ERR_PARAM;
> +
[Severity: Medium]
The commit message says:
Allocate the NIX LF SQ bitmap with the same span used by
bitmap_weight(..., BITS_PER_LONG * 16) in rvu_get_hwinfo().
There is no function named rvu_get_hwinfo() in the driver (git grep at this
commit returns no matches), and there is no pre-existing bitmap_weight()
call with a BITS_PER_LONG * 16 span either - NIX_SQ_BMAP_BITS is added by
this patch. The site actually changed is rvu_iface_get_qcnts(), the
IFACE_GET_INFO handler. Could the message name the real function?
For the check above, the non-switch branch reduces to
req->sq_cnt > (u64)req->sq_cnt * BITS_PER_LONG
which can never be true, so the "reject larger sq_cnt" validation is inert
for every non-switch caller. Is that intentional, and could the new
NIX_AF_ERR_PARAM path and the new qidx cap in the SQ case of
rvu_nix_blk_aq_enq_inst() be mentioned in the changelog?
[ ... ]
> @@ -2209,6 +2240,25 @@ static void nix_get_txschq_range(struct rvu *rvu, u16 pcifunc,
> }
> }
>
> +static int nix_get_pan_tx_link(struct rvu *rvu)
> +{
> + struct rvu_hwinfo *hw = rvu->hw;
> +
> + return hw->cgx_links + hw->lbk_links + 1;
> +}
> +
> +static bool nix_txsch_is_pan_schq(struct rvu *rvu, int schq)
> +{
> + int pan_link = nix_get_pan_tx_link(rvu);
> +
> + return schq >= pan_link && schq <= pan_link + 1;
> +}
> +
> +static bool nix_txsch_pan_allowed(struct rvu *rvu, u16 pcifunc)
> +{
> + return rvu_is_switch_pcifunc(rvu, pcifunc);
> +}
> +
> static int nix_check_txschq_alloc_req(struct rvu *rvu, int lvl, u16 pcifunc,
> struct nix_hw *nix_hw,
> struct nix_txsch_alloc_req *req)
> @@ -2224,12 +2274,27 @@ static int nix_check_txschq_alloc_req(struct rvu *rvu, int lvl, u16 pcifunc,
> /* For traffic aggregating scheduler level, one queue is enough */
> if (lvl >= hw->cap.nix_tx_aggr_lvl) {
> - if (req_schq != 1)
> + if (req_schq != 1 && !(req->flags & NIX_TXSCH_ALLOC_FLAG_PAN))
> return NIX_AF_ERR_TLX_ALLOC_FAIL;
> + if (req->schq[lvl] > MAX_TXSCHQ_PER_FUNC ||
> + req->schq_contig[lvl] > MAX_TXSCHQ_PER_FUNC)
> + return NIX_AF_ERR_TLX_ALLOC_FAIL;
> + if (req->flags & NIX_TXSCH_ALLOC_FLAG_PAN) {
> + if (link >= txsch->schq.max || link + 1 >= txsch->schq.max)
> + return NIX_AF_ERR_TLX_ALLOC_FAIL;
> + if (req_schq > 2)
> + return NIX_AF_ERR_TLX_ALLOC_FAIL;
> + }
> return 0;
> }
[Severity: High]
This PAN block never test_bit()s whether the two PAN indices are actually
free, it only bounds them against txsch->schq.max and caps req_schq at 2.
So validation can pass while the two indices are already taken, and the
failure is then deferred into nix_txsch_alloc(), which is now fallible.
Can that leak the scheduler queues committed for the earlier levels? See
the two hunks below.
> @@ -2270,6 +2335,46 @@ static void nix_txsch_alloc(struct rvu *rvu, struct nix_txsch *txsch,
> * on transmit link to which PF_FUNC is mapped to.
> */
> if (lvl >= hw->cap.nix_tx_aggr_lvl) {
> + if (start != end) {
> + int want_contig = rsp->schq_contig[lvl];
> + int got_contig = 0, got = 0;
> + int want = rsp->schq[lvl];
> +
> + for (schq = start; schq <= end; schq++) {
> + if (test_bit(schq, txsch->schq.bmap))
> + continue;
> +
> + if (got_contig < want_contig) {
> + set_bit(schq, txsch->schq.bmap);
> + rsp->schq_contig_list[lvl][got_contig++] = schq;
> + continue;
> + }
> +
> + if (got < want) {
> + set_bit(schq, txsch->schq.bmap);
> + rsp->schq_list[lvl][got++] = schq;
> + }
> + }
> +
> + rsp->schq_contig[lvl] = got_contig;
> + rsp->schq[lvl] = got;
> +
> + if (got_contig < want_contig || got < want) {
> + for (idx = 0; idx < got_contig; idx++)
> + clear_bit(rsp->schq_contig_list[lvl][idx],
> + txsch->schq.bmap);
> + for (idx = 0; idx < got; idx++)
> + clear_bit(rsp->schq_list[lvl][idx],
> + txsch->schq.bmap);
> + rsp->schq_contig[lvl] = 0;
> + rsp->schq[lvl] = 0;
> + dev_err(rvu->dev,
> + "Could not allocate schq at lvl=%u start=%u end=%u\n",
> + lvl, start, end);
> + return -ENOMEM;
> + }
> + return 0;
> + }
[ ... ]
> @@ -2398,10 +2512,11 @@ int rvu_mbox_handler_nix_txsch_alloc(struct rvu *rvu,
> end = txsch->schq.max;
> }
>
> - nix_txsch_alloc(rvu, txsch, rsp, lvl, start, end);
> + if (nix_txsch_alloc(rvu, txsch, rsp, lvl, start, end))
> + goto err;
[Severity: High]
Does this goto leak the scheduler queues allocated for the previous levels?
This goto is inside the per-level loop, but the error path only sets the
return code and unlocks:
err:
rc = NIX_AF_ERR_TLX_ALLOC_FAIL;
exit:
mutex_unlock(&rvu->rsrc_lock);
return rc;
There is no clear_bit()/rvu_free_rsrc() for the bits already set in
txsch->schq.bmap, and no reset of the pfvf_map[schq] = TXSCH_MAP(pcifunc, 0)
stamps applied for SMQ/TL4/TL3/TL2 in earlier iterations. The only failing
return is the new start != end branch at the aggregate level, which is the
last iteration, so the lower levels are already committed when it fires.
The requester gets an error and no schq lists back, so it cannot free those
indices individually - they stay used until TXSCHQ_FREE_ALL or LF teardown,
and each retry commits a fresh set. Since the two PAN indices stay occupied
across retries, can a retry loop exhaust the shared SMQ/TL4/TL3/TL2 pool for
the other PFs/VFs on that NIX block?
Before this patch nix_txsch_alloc() was void and could not fail mid-loop, so
there was nothing to unwind.
> @@ -2679,6 +2794,20 @@ static int nix_txschq_free(struct rvu *rvu, u16 pcifunc)
> }
> nix_clear_tx_xoff(rvu, blkaddr, NIX_TXSCH_LVL_TL1,
> nix_get_tx_link(rvu, pcifunc));
> + /* TL1 is at nix_tx_aggr_lvl so the loop above skips it; also clear
> + * PAN TL1 XOFF on switch-owned links before flushing SMQs.
> + */
> + if (nix_txsch_pan_allowed(rvu, pcifunc)) {
> + txsch = &nix_hw->txsch[NIX_TXSCH_LVL_TL1];
> +
> + for (schq = nix_get_pan_tx_link(rvu);
> + schq < txsch->schq.max &&
> + nix_txsch_is_pan_schq(rvu, schq); schq++) {
> + if (TXSCH_MAP_FUNC(txsch->pfvf_map[schq]) != pcifunc)
> + continue;
> + nix_clear_tx_xoff(rvu, blkaddr, NIX_TXSCH_LVL_TL1, schq);
> + }
> + }
>
> /* On PF cleanup, clear cfg done flag as
> * PF would have changed default config.
> @@ -2706,11 +2835,11 @@ static int nix_txschq_free(struct rvu *rvu, u16 pcifunc)
> /* TLs above aggregation level are shared across all PF
> * and it's VFs, hence skip freeing them.
> */
> - if (lvl >= hw->cap.nix_tx_aggr_lvl)
> - continue;
> -
> txsch = &nix_hw->txsch[lvl];
> for (schq = 0; schq < txsch->schq.max; schq++) {
> + if (lvl >= hw->cap.nix_tx_aggr_lvl &&
> + !nix_txsch_is_pan_schq(rvu, schq))
> + continue;
> if (TXSCH_MAP_FUNC(txsch->pfvf_map[schq]) != pcifunc)
> continue;
> nix_reset_tx_schedule(rvu, blkaddr, lvl, schq);
> rvu_free_rsrc(&txsch->schq, schq);
> txsch->pfvf_map[schq] = TXSCH_MAP(0, NIX_TXSCHQ_FREE);
[Severity: Medium]
Are the PAN TL1 nodes returned to the free pool while still armed?
The block above writes 0 to NIX_AF_TL1X_SW_XOFF (leaving the node
scheduling), and the loop here then calls nix_reset_tx_schedule() followed
by rvu_free_rsrc(). But nix_reset_tx_schedule() has no TL1 case:
switch (lvl) {
case NIX_TXSCH_LVL_TL2:
...
default:
return;
}
TL1X_CIR is not reset in this path either, because the first loop in
nix_txschq_free() still does
if (lvl >= hw->cap.nix_tx_aggr_lvl)
continue;
so nix_reset_tx_shaping() never runs for TL1. The TL1 therefore goes back
to the pool enabled and still anchored through TL1X_TOPOLOGY.PRIO_ANCHOR to
TL2 children that the same loop frees and that can be handed to another PF.
The reverse direction looks asymmetric too: nothing in the allocation path
normalizes TL1X_SW_XOFF (nix_reset_tx_linkcfg() returns early for the
aggregation level, nix_reset_tx_schedule() is a no-op for TL1, and
nix_reset_tx_shaping() only touches TL1X_CIR). If a PAN TL1 is freed while
XOFF is asserted - for instance rvu->rswitch.pcifunc has already been
cleared, so the new XOFF-clearing block is skipped while this loop still
releases the index - does the next owner get a TL1 that never transmits?
nix_txschq_free_one() on the TXSCHQ_FREE_PAN_TL1 path does call
nix_reset_tx_shaping() but likewise leaves TOPOLOGY/SCHEDULE programmed, so
the two free paths leave different hardware state behind. Before this patch
TL1 was never freed, so leaving it armed did not matter.
> @@ -2754,7 +2883,16 @@ static int nix_txschq_free_one(struct rvu *rvu,
> schq = req->schq;
> txsch = &nix_hw->txsch[lvl];
>
> - if (lvl >= hw->cap.nix_tx_aggr_lvl || schq >= txsch->schq.max)
> + if (req->flags & TXSCHQ_FREE_PAN_TL1) {
> + if (!nix_txsch_pan_allowed(rvu, pcifunc))
> + return NIX_AF_ERR_TLX_INVALID;
> + if (!nix_txsch_is_pan_schq(rvu, schq))
> + return NIX_AF_ERR_TLX_INVALID;
> + } else if (lvl >= hw->cap.nix_tx_aggr_lvl) {
> + return 0;
> + }
> +
> + if (schq >= txsch->schq.max)
> return 0;
[Severity: Critical]
Can this read past nix_hw->txsch[]?
lvl comes straight from the mailbox and is never bounded:
lvl = req->schq_lvl;
schq = req->schq;
txsch = &nix_hw->txsch[lvl];
rvu_mbox_handler_nix_txsch_free() forwards req unvalidated:
if (req->flags & TXSCHQ_FREE_ALL)
return nix_txschq_free(rvu, req->hdr.pcifunc);
else
return nix_txschq_free_one(rvu, req);
and nix_hw->txsch[] has only NIX_TXSCH_LVL_CNT (5) entries. Before this
patch the combined test short-circuited for every lvl >= 4 and returned 0
before txsch was dereferenced. The new PAN branch skips that early return
and checks only the requester (nix_txsch_pan_allowed()) and the numeric
index (nix_txsch_is_pan_schq(), which is level-agnostic), so with
schq_lvl = 65535 execution reaches
if (schq >= txsch->schq.max)
reading roughly 65535 * sizeof(struct nix_txsch) past nix_hw. If that
garbage max happens to exceed schq, the code continues into
TXSCH_MAP_FUNC(pfvf_map[schq]) through a garbage pointer, then
rvu_free_rsrc(&txsch->schq, schq) and a pfvf_map[schq] write, all under
rvu->rsrc_lock.
Is the requester check enough to make this unreachable? It looks like any
PF/VF can become rvu->rswitch.pcifunc, since rvu_sw_swdev2af_sender_check()
in af/switch/rvu_sw.c returns early without checking the sender:
if (msg_type == SWDEV2AF_MSG_TYPE_FW_STATUS && req->fw_up)
return 0;
after which rvu_sw_l2_ctrl_work_handler() stores that pcifunc into
rvu->rswitch.pcifunc.
Also, even for in-range levels there is no lvl == NIX_TXSCH_LVL_TL1 check,
so TXSCHQ_FREE_PAN_TL1 with schq_lvl = NIX_TXSCH_LVL_SMQ would tear down an
SMQ whose index happens to fall in the PAN range. Could this path reject
lvl >= NIX_TXSCH_LVL_CNT before indexing, and require
lvl == NIX_TXSCH_LVL_TL1 when the PAN flag is set, the way
rvu_mbox_handler_nix_txschq_cfg() already validates
req->lvl >= NIX_TXSCH_LVL_CNT?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
> index e36c68ee5d847..40d49a3238146 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c
> @@ -1833,9 +1833,23 @@ int rvu_mbox_handler_npc_install_flow(struct rvu *rvu,
> - /* ignore chan_mask in case pf func is not AF, revisit later */
> - if (!is_pffunc_af(req->hdr.pcifunc))
> - req->chan_mask = rvu_get_cpt_chan_mask(rvu);
> + /* Non-AF callers get the CPT default chan_mask unless the authorized
> + * switchdev PF sets set_chanmask to preserve a caller-supplied mask.
> + * VFs and other PFs must not use set_chanmask; that would bypass
> + * channel isolation.
> + */
> + if (!is_pffunc_af(req->hdr.pcifunc)) {
> + if (req->set_chanmask &&
> + !rvu_is_switch_pcifunc(rvu, req->hdr.pcifunc)) {
> + rvu_npc_free_entry_for_flow_install(rvu,
> + req->hdr.pcifunc,
> + allocated,
> + req->entry);
> + return NPC_FLOW_VF_PERM_DENIED;
> + }
> + if (!req->set_chanmask)
> + req->chan_mask = rvu_get_cpt_chan_mask(rvu);
> + }
[Severity: Low]
The commit message says:
For NPC install flow, add set_chanmask so callers can keep a non-default
chan_mask when the requester is not the AF; without it, chan_mask was
always forced to 0xFFF for non-AF functions.
Both halves look inaccurate for this code. The replaced line called
rvu_get_cpt_chan_mask(), which is platform dependent:
if (!is_rvu_otx2(rvu))
return NIX_CHAN_CPT_X2P_MASK;
else
return 0xFFFu;
so the previous default was 0xFFF only on OTX2. And the new behaviour is
not available to non-AF callers in general - the hunk above returns
NPC_FLOW_VF_PERM_DENIED unless the requester is exactly
rvu->rswitch.pcifunc.
Could the changelog say that set_chanmask is honored only for the switchdev
PF, and drop the claim about 0xFFF?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,6/8] octeontx2-pf: switch: Register notifiers for switch offload
2026-08-31 13:19 ` [PATCH v9 net-next 6/8] octeontx2-pf: switch: Register notifiers for switch offload Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 10 potential
issue(s) to consider:
Critical: 0 · High: 3 · Medium: 3 · Low: 4
- [High] rvu_eswitch_config()
(drivers/net/ethernet/marvell/octeontx2/nic/rep.c:401) now returns…
- [High] sw_nb_net_event()
(drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c) casts the…
- [High] Global notifiers blindly cast `netdev_priv()` to `struct
otx2_nic *` for any Cavium PCI device, leading to type confusion and…
- [Medium] IPv6 interface-address entries are marked as host entries
(`entry->host = 1`) but carry the interface's subnet prefix length…
- [Medium] rvu_eswitch_config() now reads `priv->netdev` and passes it to
otx2_sw_nb_register()/otx2_sw_nb_unregister(), which stores it in the…
- [Medium] otx2_sw_nb_registered and sw_nb_pf_netdev are plain globals
with no synchronisation.
- [Low] At this commit every new notifier handler builds a struct
fib_entry, logs it with netdev_dbg() and immediately kfree()s it, and…
- [Low] sw_nb_netdev_event() in
drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c declares…
- [Low] Notifier registration is a process-global singleton:
otx2_sw_nb_register() returns -EBUSY if otx2_sw_nb_registered is…
- [Low] sw_nb.h declares `int
otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf, struct…
--
octeontx2-pf: switch: Register notifiers for switch offload
This registers and unregisters the switch notifier blocks (FIB, netevent,
inetaddr/inet6addr, netdevice and switchdev FDB) when the representor
enables or disables switch mode via devlink. It also adds sw_nb_v4.c and
sw_nb_v6.c for the address-family specific handling, and links sw_nb_v6.o
only when the IPv6 symbols are reachable.
A few questions below about the notifier registration path and the new
event handlers.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
> index 257a2ae6a53e5..96ec58c508432 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/rep.c
[ ... ]
> @@ -399,22 +400,62 @@ static void rvu_rep_get_stats64(struct net_device *dev,
>
> static int rvu_eswitch_config(struct otx2_nic *priv, u8 ena)
> {
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> + struct net_device *netdev = priv->netdev;
> +#endif
[Severity: Medium]
Is priv->netdev ever set for the rvu_rep PF? Grepping the octeontx2 tree
shows only two references: this new read, and the assignment inside
rvu_rep_setup_tc_cb():
priv->netdev = rep->netdev;
rvu_rep_probe() does not initialise it, so on the first eswitch enable
netdev is NULL. otx2_sw_nb_register() then stores NULL into
sw_nb_pf_netdev (making the FIB logging fallback dead), and any
register_*_notifier() failure runs netdev_err(NULL, ...).
There is a second concern with the same pointer. Once a TC flower rule has
been offloaded on a representor, priv->netdev points at rep->netdev, and
rvu_rep_destroy() does:
unregister_netdev(rep->netdev);
rvu_rep_devlink_port_unregister(rep);
free_netdev(rep->netdev);
without clearing priv->netdev. Can a later switchdev enable then publish
that freed netdev into sw_nb_pf_netdev, where sw_nb_fib_event() logs
through it? Would taking a netdev_hold(), or simply using priv->dev for
logging, be safer here?
> struct devlink_port_attrs attrs = {};
> struct esw_cfg_req *req;
> + int mbox_err;
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> + int err;
> +#endif
>
> rvu_rep_devlink_set_switch_id(priv, &attrs.switch_id);
>
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> + /* Disable unregisters PF notifiers before ESW_CFG clears rep_mode on
> + * the AF. unregister_*_notifier() removes each block synchronously,
> + * so there is no window where the AF considers the eswitch off while
> + * sw_nb_* handlers remain registered and could still send mailbox
> + * traffic (that race existed only when disable ran after the mailbox).
> + */
> + if (ena) {
> + err = otx2_sw_nb_register(netdev);
> + if (err)
> + return err;
> + } else {
> + /* TODO: On disable, notifiers are unregistered before ESW_CFG. If
> + * mailbox allocation fails below, restore otx2_sw_nb_register()
> + * so software notifiers are not abandoned while hardware remains
> + * in eswitch mode.
> + */
> + err = otx2_sw_nb_unregister(netdev);
> + if (err)
> + return err;
> + }
> +#endif
[Severity: High]
These early returns skip the ESW_CFG mailbox entirely, and both callers
discard the return value:
rvu_rep_create()
rvu_eswitch_config(priv, true);
return 0;
rvu_rep_destroy()
rvu_eswitch_config(priv, false);
priv->flags |= OTX2_FLAG_INTF_DOWN;
rvu_rep_free_cq_rsrc(priv);
Can AF state and driver state diverge in both directions here?
On enable, otx2_sw_nb_register() can fail with -EBUSY (the global
single-registration guard) or with errors from sw_fdb_init(),
otx2_sw_fib_init() or any register_*_notifier(). ESW_CFG is never sent,
so rvu_mbox_handler_esw_cfg() never runs and rvu->rep_mode stays 0, but
otx2_devlink_eswitch_mode_set() still sees success:
if (!ret)
pfvf->esw_mode = mode;
so devlink reports switchdev mode with no representor steering installed.
On disable, otx2_sw_nb_unregister() has already unregistered all six
notifier blocks and run sw_fl_deinit()/otx2_sw_fib_deinit()/sw_fdb_deinit()
and cleared otx2_sw_nb_registered before it returns the accumulated error.
rvu_eswitch_config() then returns without sending ESW_CFG(ena = 0), so the
AF keeps rep_mode = 1 and the rep MCAM entries installed, while
rvu_rep_destroy() continues on to rvu_rep_free_cq_rsrc(),
unregister_netdev(), free_netdev() and rvu_rep_rsrc_free(). Does that
leave hardware steering traffic to representor pcifuncs whose NIX/CQ
resources have been freed?
A retry does not repair it either, since otx2_sw_nb_unregister() now
short-circuits on !otx2_sw_nb_registered.
Would it make more sense to send ESW_CFG regardless and propagate the
result to rvu_rep_create()/rvu_rep_destroy()?
> +
> mutex_lock(&priv->mbox.lock);
> req = otx2_mbox_alloc_msg_esw_cfg(&priv->mbox);
> if (!req) {
> mutex_unlock(&priv->mbox.lock);
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> + if (ena)
> + otx2_sw_nb_unregister(netdev);
> +#endif
> return -ENOMEM;
> }
> req->ena = ena;
> memcpy(req->switch_id, attrs.switch_id.id, attrs.switch_id.id_len);
> - otx2_sync_mbox_msg(&priv->mbox);
> + mbox_err = otx2_sync_mbox_msg(&priv->mbox);
> mutex_unlock(&priv->mbox.lock);
> - return 0;
> +
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> + if (ena && mbox_err)
> + otx2_sw_nb_unregister(netdev);
> +#endif
> +
> + return mbox_err;
> }
>
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> index 426a420119301..b51d8d2d01b8c 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> @@ -4,18 +4,555 @@
[ ... ]
> +const char *sw_nb_get_cmd2str(int cmd)
> +{
> + return sw_nb_cmd2str[cmd];
> +}
> +EXPORT_SYMBOL(sw_nb_get_cmd2str);
[Severity: Low]
This helper has no in-tree caller, even at the end of the series, and it
indexes sw_nb_cmd2str[] without validating cmd: index 0 returns NULL and
cmd >= OTX2_CMD_MAX reads past the array. Is the EXPORT_SYMBOL() needed,
and could a bounds check be added?
Related: sw_nb_fib_event_dump() and SWITCH_NB_FIB_EVENT_DUMP() are added as
__maybe_unused with no user and stay that way through the series.
Also, at this commit each new handler builds a struct fib_entry, logs it
with netdev_dbg() and immediately kfree()s it, and sw_nb_fdb_event() is a
no-op:
case SWITCHDEV_FDB_ADD_TO_DEVICE:
if (fdb_info->is_local)
break;
break;
The later patches in the series replace those kfree() calls with
sw_fib_add_to_list()/sw_fdb_add_to_list(), so this is only noted for the
record.
[ ... ]
> +static int sw_nb_net_event(struct notifier_block *nb,
> + unsigned long event, void *ptr)
> +{
> + struct neighbour *n = ptr;
> +
> + if (!sw_nb_is_valid_dev(n->dev))
> + return NOTIFY_DONE;
> +
> + if (event != NETEVENT_NEIGH_UPDATE)
> + return NOTIFY_DONE;
[Severity: High]
Should the event check come before ptr is treated as a struct neighbour?
The netevent chain carries a different payload per event, per
include/net/netevent.h:
NETEVENT_NEIGH_UPDATE = 1, /* arg is struct neighbour ptr */
NETEVENT_REDIRECT, /* arg is struct netevent_redirect ptr */
NETEVENT_DELAY_PROBE_TIME_UPDATE, /* arg is struct neigh_parms ptr */
NETEVENT_IPV4_MPATH_HASH_UPDATE, /* arg is struct net ptr */
net/core/neighbour.c passes a struct neigh_parms:
if (index == NEIGH_VAR_DELAY_PROBE_TIME)
call_netevent_notifiers(NETEVENT_DELAY_PROBE_TIME_UPDATE, p);
and net/ipv4/sysctl_net_ipv4.c passes a struct net:
if (write && ret == 0)
call_netevent_notifiers(NETEVENT_IPV4_MPATH_HASH_UPDATE, net);
offsetof(struct neighbour, dev) is several hundred bytes in (arp_queue,
timer, ha[] and a struct hh_cache precede dev), so reading n->dev from a
neigh_parms object or from the stack-allocated struct netevent_redirect
looks like an out-of-bounds read. The garbage value is then dereferenced
as a net_device by netif_is_bridge_master() (dev->priv_flags),
sw_nb_is_cavium_dev():
dev = netdev->dev.parent;
if (!dev || dev->bus != &pci_bus_type)
return false;
pdev = to_pci_dev(dev);
if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
and netdev_walk_all_lower_dev_rcu() (dev->adj_list). Once switchdev mode
is on, a write to /proc/sys/net/ipv4/neigh/*/delay_first_probe_time or to
/proc/sys/net/ipv4/fib_multipath_hash_policy would reach this. Can that
oops?
[ ... ]
> +static int sw_nb_netdev_event(struct notifier_block *unused,
> + unsigned long event, void *ptr)
> +{
> + struct net_device *dev = netdev_notifier_info_to_dev(ptr);
> + struct in_device *idev;
> + struct inet6_dev *i6dev;
[Severity: Low]
i6dev is declared unconditionally but its only use sits inside
#if IS_REACHABLE(CONFIG_IPV6) below. With CONFIG_OCTEONTX_SWITCH=y and
CONFIG_IPV6=n (allowed, since OCTEONTX_SWITCH only depends on
(64BIT && COMPILE_TEST) || ARM64 and OCTEONTX2_PF), or with CONFIG_IPV6=m
and a built-in driver, this is an unused variable and -Wunused-variable
becomes a build failure with the default CONFIG_WERROR=y. Should the
declaration move inside the same conditional?
> +
> + if (event != NETDEV_CHANGE &&
> + event != NETDEV_UP &&
> + event != NETDEV_DOWN) {
> + return NOTIFY_DONE;
> + }
> +
> + if (!sw_nb_is_valid_dev(dev))
> + return NOTIFY_DONE;
> +
> + idev = __in_dev_get_rtnl(dev);
> + if (idev)
> + sw_nb_v4_netdev_event(unused, event, ptr);
> +
> +#if IS_REACHABLE(CONFIG_IPV6)
> + i6dev = __in6_dev_get(dev);
> + if (i6dev)
> + sw_nb_v6_netdev_event(unused, event, ptr);
> +#endif
> +
> + return NOTIFY_DONE;
> +}
[ ... ]
> +int otx2_sw_nb_unregister(struct net_device *netdev)
> +{
> + int err, ret = 0;
> +
> + if (!otx2_sw_nb_registered)
> + return 0;
[ ... ]
> + sw_fl_deinit();
> + otx2_sw_fib_deinit();
> + sw_fdb_deinit();
> +
> + sw_nb_pf_netdev = NULL;
> + otx2_sw_nb_registered = false;
> +
> + return ret;
> +}
> +EXPORT_SYMBOL(otx2_sw_nb_unregister);
> +
> +/* Concurrent registration from multiple devlink instances cannot occur on a
> + * given RVU: only the RVU_REP netdev devlink reaches this function (see
> + * comment above). The AF and PF/VF devlinks do not call otx2_sw_nb_register(),
> + * and their eswitch_mode_set handlers return -EOPNOTSUPP. devlink core
> + * holds devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler,
> + * so two threads cannot enter here concurrently on that single rep devlink.
> + * A second call after successful registration returns -EBUSY before any
> + * notifier or workqueue state is modified.
> + */
> +int otx2_sw_nb_register(struct net_device *netdev)
> {
> + int err;
> +
> + /* Notifier blocks are global and only one RVU_REP may register at a
> + * time (switch offload is init_net-wide; see comment at file top).
> + * A second RVU card gets -EBUSY here by design. Concurrent calls on
> + * the same RVU_REP cannot happen: only that netdev's devlink reaches
> + * this function (otx2_rep_dev()), and devlink core holds
> + * devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler.
> + * No extra lock is needed to protect the notifier chains.
> + */
> + if (otx2_sw_nb_registered)
> + return -EBUSY;
> +
> + sw_nb_pf_netdev = netdev;
[Severity: Medium]
devlink->lock is per devlink instance. With two RVU_REP functions there
are two devlinks, so can two threads run this unlocked check-then-set
concurrently?
if (otx2_sw_nb_registered)
return -EBUSY;
...
otx2_sw_nb_registered = true;
Both would then run sw_fdb_init()/otx2_sw_fib_init()/sw_fl_init(), which
re-initialise global workqueue pointers (leaking the loser's workqueue),
both would call register_switchdev_notifier() and friends with the same
static notifier_block objects, and sw_nb_pf_netdev would keep whichever
store landed last.
The flag also records no owner. Since rvu_rep_create() ignores the
-EBUSY, device B enters switchdev mode without registering, and a later
rvu_rep_destroy(B) -> otx2_sw_nb_unregister() sees the global flag set and
tears down device A's notifiers and helpers. Is that intended?
Finally, is the comment's claim that DEVLINK_CMD_ESWITCH_MODE_SET is the
only entry point accurate? rvu_rep_remove() reaches the same code with no
devlink lock held:
otx2_unregister_dl(priv);
if (!(priv->flags & OTX2_FLAG_INTF_DOWN))
rvu_rep_destroy(priv);
[Severity: Low]
The commit message describes switch-mode support generally but does not
mention that this registration is a process-global singleton, so switch
offload can be enabled on only one RVU device per host ("A second RVU card
gets -EBUSY here by design"). Could the changelog state that, along with
the other restrictions the comments encode: registration is init_net-wide
with no netns scoping, and register_fib_notifier() is called with NULL
dump/replay callbacks so pre-existing routes are never replayed?
> +
> + err = sw_fdb_init();
> + if (err)
> + goto err_clear;
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
> index 0ba29f76fd41f..39435f23427cf 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.h
> @@ -9,12 +9,41 @@
[ ... ]
> +int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
> + struct af2pf_fdb_refresh_req *req,
> + struct msg_rsp *rsp);
[Severity: Low]
At this commit this prototype has no definition, and
otx2_process_mbox_msg_up() does not expand
MBOX_UP_AF2PF_FDB_REFRESH_MESSAGES, so MBOX_MSG_AF2PF_FDB_REFRESH is not
routed. Both gaps are closed by later patches in the same series
(sw_fdb.c defines the handler and otx2_pf.c expands the macro), so this is
only noted for the record - would it be tidier to add the declaration in
the patch that adds the definition?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
> new file mode 100644
> index 0000000000000..3497e60aedbec
> --- /dev/null
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v6.c
> @@ -0,0 +1,301 @@
[ ... ]
> + entry->cmd = sw_nb_inetaddr_event_to_otx2_event(event, dev);
> + memcpy(entry->dst6, &addr, sizeof(entry->dst6));
> + entry->dst6_plen = prefix_len;
> + entry->host = 1;
> + entry->ipv6 = 1;
[Severity: Medium]
Should host entries carry a full 128-bit prefix here? This path in
sw_nb_v6_netdev_event(), and the same assignment in
sw_nb_v6_inetaddr_event():
entry->dst6_plen = ifa6->prefix_len;
entry->mac_valid = 1;
entry->host = 1;
use the interface subnet prefix while setting host = 1. Every other
producer of the same descriptor uses an exact-host length -
sw_nb_v4_inetaddr_event() and sw_nb_v4_netdev_event() use:
entry->dst_len = 32;
entry->mac_valid = 1;
entry->host = 1;
and sw_nb_net_v6_neigh_update() uses the full key length:
entry->dst6_plen = n->tbl->key_len * 8;
If the consumer honours dst6_plen for host rules, would a typical /64
interface prefix be installed as the local-host entry, claiming the whole
subnet as local?
[ ... ]
> @@ -4,18 +4,555 @@
> * Copyright (C) 2026 Marvell.
> *
> */
> +#include <linux/kernel.h>
> +#include <linux/netdevice.h>
> +#include <linux/etherdevice.h>
> +#include <net/switchdev.h>
> +#include <net/netevent.h>
> +#include <net/arp.h>
> +#include <net/route.h>
> +#include <linux/inetdevice.h>
> +#include <net/addrconf.h>
> +
> +#include "../otx2_reg.h"
> +#include "../otx2_common.h"
> +#include "../otx2_struct.h"
> +#include "../cn10k.h"
> #include "sw_nb.h"
> +#include "sw_fdb.h"
> +#include "sw_fib.h"
> +#include "sw_fl.h"
> +#include "sw_nb_v4.h"
> +#include "sw_nb_v6.h"
>
> #if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
>
> -int otx2_sw_nb_unregister(void)
> +/* PF netdev for netdev_* logging when notifier info has no device */
> +static struct net_device *sw_nb_pf_netdev;
> +/* Notifier registration is toggled only from rvu_eswitch_config(), which is
> + * reached exclusively when switchdev mode is enabled on the RVU eswitch
> + * representor PF (PCI_DEVID_RVU_REP). The sole call path is:
> + *
> + * DEVLINK_CMD_ESWITCH_MODE_SET
> + * -> otx2_devlink_eswitch_mode_set() [otx2_rep_dev() only]
> + * -> rvu_rep_create() / rvu_rep_destroy()
> + * -> rvu_eswitch_config(ena = 1) -> otx2_sw_nb_register()
> + * -> rvu_eswitch_config(ena = 0) -> otx2_sw_nb_unregister()
> + *
> + * On disable, otx2_sw_nb_unregister() runs before the ESW_CFG mailbox so flush
> + * paths in sw_fdb/fib/fl_deinit() can still reach hardware.
> + *
> + * Other OcteonTX2 netdev PFs/VFs also have a devlink, but their
> + * eswitch_mode_set handler returns -EOPNOTSUPP. The AF rvu_devlink
> + * eswitch_mode_set does not register these notifiers. There is exactly
> + * one RVU_REP PCI function (and netdev devlink) per RVU, and devlink
> + * core holds devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET
> + * handler, so this path cannot run concurrently on the same device.
> + * otx2_sw_nb_registered further ensures at most one active registration.
> + */
> +static bool otx2_sw_nb_registered;
> +
> +static const char *sw_nb_cmd2str[OTX2_CMD_MAX] = {
> + [OTX2_DEV_UP] = "OTX2_DEV_UP",
> + [OTX2_DEV_DOWN] = "OTX2_DEV_DOWN",
> + [OTX2_DEV_CHANGE] = "OTX2_DEV_CHANGE",
> + [OTX2_NEIGH_UPDATE] = "OTX2_NEIGH_UPDATE",
> + [OTX2_FIB_ENTRY_REPLACE] = "OTX2_FIB_ENTRY_REPLACE",
> + [OTX2_FIB_ENTRY_ADD] = "OTX2_FIB_ENTRY_ADD",
> + [OTX2_FIB_ENTRY_DEL] = "OTX2_FIB_ENTRY_DEL",
> + [OTX2_FIB_ENTRY_APPEND] = "OTX2_FIB_ENTRY_APPEND",
> +};
> +
> +const char *sw_nb_get_cmd2str(int cmd)
> +{
> + return sw_nb_cmd2str[cmd];
> +}
> +EXPORT_SYMBOL(sw_nb_get_cmd2str);
> +
> +bool sw_nb_is_cavium_dev(struct net_device *netdev)
> +{
> + struct pci_dev *pdev;
> + struct device *dev;
> +
> + dev = netdev->dev.parent;
> + if (!dev || dev->bus != &pci_bus_type)
> + return false;
> +
> + pdev = to_pci_dev(dev);
> + if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
> + return false;
> +
> + return true;
> +}
> +
> +/* Resolve the Cavium PF netdev used to reach the switch AF for offload.
> + *
> + * For a bridge master netdev, any Cavium netdev enslaved to the bridge is
> + * sufficient: callers only need a PF netdev to obtain the switch AF mailbox
> + * context (pcifunc). Bridge-specific information is tagged separately in
> + * the offload entry (entry->bridge), so walking every lower netdev is not
> + * required here.
> + *
> + * Only a single level of netdev nesting is resolved (bridge lower dev or
> + * VLAN real dev). Nested topologies such as VLAN-over-bridge are not
> + * supported; offload will not work for those configurations.
> + */
> +struct net_device *sw_nb_resolve_pf_dev(struct net_device *dev)
> +{
> + struct net_device *pf_dev = dev;
> + struct list_head *iter;
> +
> + rcu_read_lock();
> +
> + if (netif_is_bridge_master(dev)) {
> + iter = &dev->adj_list.lower;
> + pf_dev = netdev_next_lower_dev_rcu(dev, &iter);
> + if (!pf_dev)
> + pf_dev = dev;
> + } else if (is_vlan_dev(dev)) {
> + pf_dev = vlan_dev_real_dev(dev);
> + }
> +
> + rcu_read_unlock();
> +
> + if (!sw_nb_is_cavium_dev(pf_dev))
> + return NULL;
> +
> + return pf_dev;
> +}
> +
> +static int sw_nb_check_slaves(struct net_device *dev,
> + struct netdev_nested_priv *priv)
> {
> + int *cnt;
> +
> + if (!priv->flags)
> + return 0;
> +
> + priv->flags &= sw_nb_is_cavium_dev(dev);
> + if (priv->flags) {
> + cnt = priv->data;
> + (*cnt)++;
> + }
> +
> return 0;
> }
>
> -int otx2_sw_nb_register(void)
> +/* Switch offload has no network namespace support. The global notifiers
> + * registered below are not scoped to a netns, and sw_nb_is_cavium_dev()
> + * matches any Cavium PCI netdev without checking dev_net(). All netdevs
> + * involved in offload (PF/VF ports, bridge members, VLANs, neighbours,
> + * and routes) must therefore reside in &init_net for offload to work.
> + */
> +bool sw_nb_is_valid_dev(struct net_device *netdev)
> +{
> + struct netdev_nested_priv priv;
> + struct net_device *br;
> + int cnt = 0;
> + bool valid;
> +
> + priv.flags = true;
> + priv.data = &cnt;
> +
> + rcu_read_lock();
> +
> + if (netif_is_bridge_master(netdev) || is_vlan_dev(netdev)) {
> + netdev_walk_all_lower_dev_rcu(netdev, sw_nb_check_slaves, &priv);
> + valid = priv.flags && cnt;
> + rcu_read_unlock();
> + return valid;
> + }
> +
> + if (netif_is_bridge_port(netdev)) {
> + br = netdev_master_upper_dev_get_rcu(netdev);
> + if (!br) {
> + rcu_read_unlock();
> + return false;
> + }
> + netdev_walk_all_lower_dev_rcu(br, sw_nb_check_slaves, &priv);
> + valid = priv.flags && cnt;
> + rcu_read_unlock();
> + return valid;
> + }
> +
> + rcu_read_unlock();
> +
> + return sw_nb_is_cavium_dev(netdev);
> +}
> +
> +static int sw_nb_fdb_event(struct notifier_block *unused,
> + unsigned long event, void *ptr)
> +{
> + struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
> + struct switchdev_notifier_fdb_info *fdb_info = ptr;
> +
> + if (!sw_nb_is_valid_dev(dev))
> + return NOTIFY_DONE;
> +
> + switch (event) {
> + case SWITCHDEV_FDB_ADD_TO_DEVICE:
> + if (fdb_info->is_local)
> + break;
> + break;
> +
> + case SWITCHDEV_FDB_DEL_TO_DEVICE:
> + if (fdb_info->is_local)
> + break;
> + break;
> +
> + default:
> + return NOTIFY_DONE;
> + }
> +
> + return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block sw_nb_fdb = {
> + .notifier_call = sw_nb_fdb_event,
> +};
> +
> +static void __maybe_unused
> +sw_nb_fib_event_dump(unsigned long event, void *ptr)
> +{
> + struct fib_entry_notifier_info *fen_info = ptr;
> + struct net_device *log_dev;
> + struct fib_nh *fib_nh;
> + struct fib_info *fi;
> + int i;
> +
> + fi = fen_info->fi;
> + log_dev = (fi && fi->fib_nhs) ? fi->fib_nh->fib_nh_dev : sw_nb_pf_netdev;
> + if (log_dev)
> + netdev_info(log_dev, "%s: FIB event=%lu dst=%pI4h dstlen=%d type=%u\n",
> + __func__, event, &fen_info->dst, fen_info->dst_len,
> + fen_info->type);
> +
> + if (!fi)
> + return;
> +
> + fib_nh = fi->fib_nh;
> + for (i = 0; i < fi->fib_nhs; i++, fib_nh++) {
> + if (!fib_nh->fib_nh_dev)
> + continue;
> + netdev_info(fib_nh->fib_nh_dev,
> + "%s: dev=%s saddr=%pI4n gw=%pI4n\n",
> + __func__, fib_nh->fib_nh_dev->name,
> + &fib_nh->nh_saddr, &fib_nh->fib_nh_gw4);
> + }
> +}
> +
> +#define SWITCH_NB_FIB_EVENT_DUMP(...) \
> + sw_nb_fib_event_dump(__VA_ARGS__)
> +
> +int sw_nb_fib_event_to_otx2_event(int event, struct net_device *netdev)
> +{
> + switch (event) {
> + case FIB_EVENT_ENTRY_REPLACE:
> + return OTX2_FIB_ENTRY_REPLACE;
> + case FIB_EVENT_ENTRY_ADD:
> + return OTX2_FIB_ENTRY_ADD;
> + case FIB_EVENT_ENTRY_DEL:
> + return OTX2_FIB_ENTRY_DEL;
> + default:
> + break;
> + }
> +
> + netdev_err(netdev, "Wrong FIB event %d\n", event);
> + return -1;
> +}
> +
> +static int sw_nb_fib_event(struct notifier_block *nb,
> + unsigned long event, void *ptr)
> +{
> + struct fib_notifier_info *info = ptr;
> +
> + switch (event) {
> + case FIB_EVENT_ENTRY_REPLACE:
> + case FIB_EVENT_ENTRY_ADD:
> + case FIB_EVENT_ENTRY_DEL:
> + break;
> + default:
> + if (sw_nb_pf_netdev)
> + netdev_dbg(sw_nb_pf_netdev,
> + "%s: Won't process FIB event %lu\n",
> + __func__, event);
> + return NOTIFY_DONE;
> + }
> +
> + switch (info->family) {
> + case AF_INET:
> + return sw_nb_v4_fib_event(nb, event, ptr);
> +#if IS_REACHABLE(CONFIG_IPV6)
> + case AF_INET6:
> + return sw_nb_v6_fib_event(nb, event, ptr);
> +#endif
> + default:
> + break;
> + }
> + return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block sw_nb_fib = {
> + .notifier_call = sw_nb_fib_event,
> +};
> +
> +static int sw_nb_net_event(struct notifier_block *nb,
> + unsigned long event, void *ptr)
> +{
> + struct neighbour *n = ptr;
> +
> + if (!sw_nb_is_valid_dev(n->dev))
> + return NOTIFY_DONE;
> +
> + if (event != NETEVENT_NEIGH_UPDATE)
> + return NOTIFY_DONE;
> +
> + switch (n->tbl->family) {
> + case AF_INET:
> + return sw_nb_net_v4_neigh_update(nb, event, ptr);
> +#if IS_REACHABLE(CONFIG_IPV6)
> + case AF_INET6:
> + return sw_nb_net_v6_neigh_update(nb, event, ptr);
> +#endif
> + default:
> + break;
> + }
> + return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block sw_nb_netevent = {
> + .notifier_call = sw_nb_net_event,
> +
> +};
> +
> +int sw_nb_inetaddr_event_to_otx2_event(int event, struct net_device *netdev)
> +{
> + switch (event) {
> + case NETDEV_CHANGE:
> + return OTX2_DEV_CHANGE;
> + case NETDEV_UP:
> + return OTX2_DEV_UP;
> + case NETDEV_DOWN:
> + return OTX2_DEV_DOWN;
> + default:
> + break;
> + }
> + netdev_dbg(netdev, "%s: Wrong interaddr event %d\n",
> + __func__, event);
> + return -1;
> +}
> +
> +static struct notifier_block sw_nb_v4_inetaddr = {
> + .notifier_call = sw_nb_v4_inetaddr_event,
> +};
> +
> +#if IS_REACHABLE(CONFIG_IPV6)
> +static struct notifier_block sw_nb_v6_inetaddr = {
> + .notifier_call = sw_nb_v6_inetaddr_event,
> +};
> +#endif
> +
> +static int sw_nb_netdev_event(struct notifier_block *unused,
> + unsigned long event, void *ptr)
> +{
> + struct net_device *dev = netdev_notifier_info_to_dev(ptr);
> + struct in_device *idev;
> + struct inet6_dev *i6dev;
> +
> + if (event != NETDEV_CHANGE &&
> + event != NETDEV_UP &&
> + event != NETDEV_DOWN) {
> + return NOTIFY_DONE;
> + }
> +
> + if (!sw_nb_is_valid_dev(dev))
> + return NOTIFY_DONE;
> +
> + idev = __in_dev_get_rtnl(dev);
> + if (idev)
> + sw_nb_v4_netdev_event(unused, event, ptr);
> +
> +#if IS_REACHABLE(CONFIG_IPV6)
> + i6dev = __in6_dev_get(dev);
> + if (i6dev)
> + sw_nb_v6_netdev_event(unused, event, ptr);
> +#endif
> +
> + return NOTIFY_DONE;
> +}
> +
> +static struct notifier_block sw_nb_netdev = {
> + .notifier_call = sw_nb_netdev_event,
> +};
> +
> +int otx2_sw_nb_unregister(struct net_device *netdev)
> +{
> + int err, ret = 0;
> +
> + if (!otx2_sw_nb_registered)
> + return 0;
> +
> + err = unregister_switchdev_notifier(&sw_nb_fdb);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister switchdev nb\n");
> + ret = err;
> + }
> +
> + err = unregister_fib_notifier(&init_net, &sw_nb_fib);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister fib nb\n");
> + if (!ret)
> + ret = err;
> + }
> +
> + err = unregister_netevent_notifier(&sw_nb_netevent);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister netevent\n");
> + if (!ret)
> + ret = err;
> + }
> +
> + err = unregister_inetaddr_notifier(&sw_nb_v4_inetaddr);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister addr event\n");
> + if (!ret)
> + ret = err;
> + }
> +
> +#if IS_REACHABLE(CONFIG_IPV6)
> + err = unregister_inet6addr_notifier(&sw_nb_v6_inetaddr);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister addr event\n");
> + if (!ret)
> + ret = err;
> + }
> +#endif
> +
> + err = unregister_netdevice_notifier(&sw_nb_netdev);
> + if (err) {
> + netdev_err(netdev, "Failed to unregister netdev notifier\n");
> + if (!ret)
> + ret = err;
> + }
> +
> + sw_fl_deinit();
> + otx2_sw_fib_deinit();
> + sw_fdb_deinit();
> +
> + sw_nb_pf_netdev = NULL;
> + otx2_sw_nb_registered = false;
> +
> + return ret;
> +}
> +EXPORT_SYMBOL(otx2_sw_nb_unregister);
> +
> +/* Concurrent registration from multiple devlink instances cannot occur on a
> + * given RVU: only the RVU_REP netdev devlink reaches this function (see
> + * comment above). The AF and PF/VF devlinks do not call otx2_sw_nb_register(),
> + * and their eswitch_mode_set handlers return -EOPNOTSUPP. devlink core
> + * holds devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler,
> + * so two threads cannot enter here concurrently on that single rep devlink.
> + * A second call after successful registration returns -EBUSY before any
> + * notifier or workqueue state is modified.
> + */
> +int otx2_sw_nb_register(struct net_device *netdev)
> {
> + int err;
> +
> + /* Notifier blocks are global and only one RVU_REP may register at a
> + * time (switch offload is init_net-wide; see comment at file top).
> + * A second RVU card gets -EBUSY here by design. Concurrent calls on
> + * the same RVU_REP cannot happen: only that netdev's devlink reaches
> + * this function (otx2_rep_dev()), and devlink core holds
> + * devlink->lock for the full DEVLINK_CMD_ESWITCH_MODE_SET handler.
> + * No extra lock is needed to protect the notifier chains.
> + */
> + if (otx2_sw_nb_registered)
> + return -EBUSY;
> +
> + sw_nb_pf_netdev = netdev;
> +
> + err = sw_fdb_init();
> + if (err)
> + goto err_clear;
> +
> + err = otx2_sw_fib_init();
> + if (err)
> + goto err_fdb;
> +
> + err = sw_fl_init();
> + if (err)
> + goto err_fib;
> +
> + err = register_switchdev_notifier(&sw_nb_fdb);
> + if (err) {
> + netdev_err(netdev, "Failed to register switchdev nb\n");
> + goto err_helpers;
> + }
> +
> + err = register_fib_notifier(&init_net, &sw_nb_fib, NULL, NULL);
> + if (err) {
> + netdev_err(netdev, "Failed to register fb notifier block\n");
> + goto err1;
> + }
> +
> + err = register_netevent_notifier(&sw_nb_netevent);
> + if (err) {
> + netdev_err(netdev, "Failed to register netevent\n");
> + goto err2;
> + }
> +
> +#if IS_REACHABLE(CONFIG_IPV6)
> + err = register_inet6addr_notifier(&sw_nb_v6_inetaddr);
> + if (err) {
> + netdev_err(netdev, "Failed to register addr event\n");
> + goto err3;
> + }
> +#endif
> +
> + err = register_inetaddr_notifier(&sw_nb_v4_inetaddr);
> + if (err) {
> + netdev_err(netdev, "Failed to register addr event\n");
> + goto err4;
> + }
> +
> + err = register_netdevice_notifier(&sw_nb_netdev);
> + if (err) {
> + netdev_err(netdev, "Failed to register netdevice nb\n");
> + goto err5;
> + }
> +
> + otx2_sw_nb_registered = true;
> +
> return 0;
> +
> +err5:
> + unregister_inetaddr_notifier(&sw_nb_v4_inetaddr);
> +
> +err4:
> +#if IS_REACHABLE(CONFIG_IPV6)
> + unregister_inet6addr_notifier(&sw_nb_v6_inetaddr);
> +
> +err3:
> +#endif
> + unregister_netevent_notifier(&sw_nb_netevent);
> +
> +err2:
> + unregister_fib_notifier(&init_net, &sw_nb_fib);
> +
> +err1:
> + unregister_switchdev_notifier(&sw_nb_fdb);
> +
> +err_helpers:
> + sw_fl_deinit();
> +err_fib:
> + otx2_sw_fib_deinit();
> +err_fdb:
> + sw_fdb_deinit();
> +err_clear:
> + sw_nb_pf_netdev = NULL;
> + return err;
> }
> +EXPORT_SYMBOL(otx2_sw_nb_register);
>
> #endif
[Severity: High]
Is a PCI vendor ID match a strong enough filter here? All six
notifier blocks are global (netdev, netevent, fib, inetaddr,
inet6addr, switchdev), so every netdev in init_net reaches
sw_nb_is_valid_dev() -> sw_nb_is_cavium_dev(), and the only test
performed is:
dev = netdev->dev.parent;
if (!dev || dev->bus != &pci_bus_type)
return false;
pdev = to_pci_dev(dev);
if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
return false;
PCI_VENDOR_ID_CAVIUM (0x177d) is not specific to RVU: liquidio,
thunder nicvf/nicpf and octeon_ep all sit behind the same vendor ID.
If any of those NICs is present in the same host, its netdev passes
the filter, sw_nb_resolve_pf_dev() happily returns it, and the new
handlers then do:
pf = netdev_priv(pf_dev);
entry->port_id = pf->pcifunc;
That reads struct otx2_nic fields out of an unrelated driver's
private area - pcifunc lives several hundred bytes into otx2_nic,
well past the end of, say, struct lio or struct nicvf, so this is a
plain out-of-bounds read of whatever follows that allocation. The
same cast appears in sw_nb_v4_netdev_event(),
sw_nb_v4_inetaddr_event(), sw_nb_v4_fib_event(),
sw_nb_net_v4_neigh_update() and all four v6 counterparts. It gets
worse later in the series, where these entries are handed to
sw_fib_add_to_list()/sw_fdb_add_to_list(), which take pf->pdev and
pf->mbox from the same bogus pointer and eventually lock
pf->mbox.lock.
sw_nb_check_slaves() inherits the same weakness for bridge and VLAN
lower devs, so a bridge containing a liquidio port would be treated
as a valid offload target too.
What is the intended way to recognise an RVU PF/VF netdev here?
Matching pdev->device against the RVU PF/VF/REP device IDs would be
a minimal fix, but comparing netdev->netdev_ops (or ethtool_ops)
against this driver's own ops table, or looking the netdev up in a
list the driver itself maintains, would make the ownership check
unambiguous. Could you pick one that cannot alias other Cavium
drivers?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev
2026-08-31 13:19 ` [PATCH v9 net-next 7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
2026-09-04 3:15 ` Ratheesh Kannoth
0 siblings, 1 reply; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 19 potential
issue(s) to consider:
Critical: 3 · High: 6 · Medium: 9 · Low: 1
- [Critical] Type confusion: `sw_nb_fdb_event()` forwards every non-local
SWITCHDEV_FDB_{ADD,DEL}_TO_DEVICE event to `sw_fdb_add_to_list(dev)`,…
- [Critical] `sw_fdb_wq_handler()` re-derives the device object from the
deferred entry via `pf = pci_get_drvdata(entry->pdev)` and assigns it…
- [Critical] `rvu_sw_swdev2af_sender_check()` deliberately short-circuits
authorization for SWDEV2AF_MSG_TYPE_FW_STATUS when `req->fw_up` is…
- [High] The `fdb_notify` mailbox message carries no port identity, so
the AF derives the switch port solely from `req->hdr.pcifunc`.
- [High] `call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE /
SWITCHDEV_FDB_DEL_TO_BRIDGE, ...)` is invoked from the PF and VF…
- [High] Both new AF up-message senders reuse the AF->PF / AF->VF
`mbox_up` shared region and re-ring the doorbell without waiting for…
- [High] Pending `sw_fdb` work entries reference per-PF/VF mailbox state,
but the global `sw_fdb_wq` is only torn down from…
- [High] `sw_fdb_llock` is acquired with plain `spin_lock()` from softirq
context and also with plain `spin_lock()` from process context.
- [High] `rvu_mbox_handler_fdb_notify()` programs hardware L2 forwarding
rules based purely on the sender's own port and an attacker-supplied…
- [Medium] Commit message claims the AF was taught to "treat it like the
CGX link event when acknowledging mailbox completion", but no such…
- [Medium] The FDB refresh path can never reach representor (RVU_REP)
netdevs even though the offload direction explicitly supports them.
- [Medium] The FDB offload and refresh pipeline drops the VLAN ID that is
part of the switchdev FDB key.
- [Medium] The design permanently loses FDB changes whenever firmware is
unavailable or any asynchronous stage hits a transient failure, with…
- [Medium] In `rvu_sw_l2_ctrl_work_handler()` the global pointers
`rvu_sw_l2_offl_wq` and `fdb_refresh_wq` and the global work…
- [Medium] A WQ_MEM_RECLAIM control worker destroys non-WQ_MEM_RECLAIM
workqueues.
- [Medium] `rvu->rswitch.pcifunc` is now mutable at runtime and is
written under the file-local `rvu_sw_l2_state_lock` (in…
- [Medium] `rvu_sw_l2_offl_coalesce_pending_locked()` cancels a new FDB
operation against *any* pending opposite operation for the same…
- [Medium] The AF acknowledges the switch firmware-up notification before
its FDB queues exist, and a later initialization failure can never be…
- [Low] Two comments added by this patch describe opposite behaviour for
the same message.
--
octeontx2: switch: plumb bridge FDB updates through AF and switchdev
This plumbs switchdev FDB add/delete notifications from the PF through a
new fdb_notify mailbox into the AF, which queues them and pushes L2 rules
toward the switchdev image. It also adds an AF to PF/VF upstream
MBOX_MSG_AF2PF_FDB_REFRESH message which raises
SWITCHDEV_FDB_ADD_TO_BRIDGE so the bridge FDB entry is kept alive.
Some questions below.
> Teach the AF swdev2af path to initialize L2 offload workqueues on
> firmware up/down and to accept refresh requests that enqueue FDB
> entries for AF to PF mailbox delivery. Add an AF to PF (and VF) upstream
> message for FDB refresh, handle it in the VF driver, and treat it like
> the CGX link event when acknowledging mailbox completion in the AF.
[Severity: Medium]
The changelog says the new message is treated "like the CGX link event when
acknowledging mailbox completion in the AF", but I can't find that change in
the diff. __rvu_mbox_up_handler() in af/rvu.c still only special-cases
MBOX_MSG_CGX_LINK_EVENT:
switch (msg->id) {
case MBOX_MSG_CGX_LINK_EVENT:
break;
default:
if (msg->rc)
dev_err(rvu->dev, "Mbox msg response has err %d, ID 0x%x\n",
msg->rc, msg->id);
So a response to MBOX_MSG_AF2PF_FDB_REFRESH lands in the default branch. A
peer without this patch (an older guest VF driver, which the host cannot
control) answers through otx2_reply_invalid_msg() with sig =
OTX2_MBOX_RSP_SIG and rc = -ENODEV, which passes the signature check and hits
that dev_err() for every refresh message. Should a case for the new id be
added here, or should the changelog drop that sentence?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> index 2aa1aa6599a5a..8f7b2962a2126 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/mbox.h
> @@ -2015,6 +2015,7 @@ struct af2pf_fdb_refresh_req {
> struct mbox_msghdr hdr;
> u16 pcifunc;
> u8 mac[6];
> + u64 flags;
> };
[Severity: Medium]
None of the structures used by this end-to-end path carry a VLAN id:
fdb_notify_req, af2swdev_notify_req and af2pf_fdb_refresh_req all have only
a MAC plus flags. The switchdev FDB key includes the VLAN, and
br_switchdev_fdb_populate() sets item->vid = fdb->key.vlan_id.
sw_nb_fdb_event() forwards only fdb_info->addr and never rejects a non-zero
fdb_info->vid, so the same MAC in two VLANs collapses to one hardware key.
On the way back, both refresh handlers build a zero-initialized
switchdev_notifier_fdb_info, so the bridge is asked to refresh the vid == 0
entry:
net/bridge/br.c:br_switchdev_event() {
...
err = br_fdb_external_learn_add(br, p, fdb_info->addr,
fdb_info->vid, fdb_info->locked, false);
Is a VLAN field needed in these messages, or should non-zero vid be rejected
in sw_nb_fdb_event() until it is plumbed through?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> index 2451eb57ec4c8..71f113bded5eb 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw.c
> @@ -33,9 +35,64 @@ u32 rvu_sw_port_id(struct rvu *rvu, u16 pcifunc)
> +static int rvu_sw_swdev2af_sender_check(struct rvu *rvu,
> + struct swdev2af_notify_req *req,
> + u64 msg_type)
> +{
> + u16 sender = req->hdr.pcifunc;
> +
> + if (!rvu_sw_swdev2af_msg_valid(msg_type))
> + return -EINVAL;
> +
> + if (msg_type == SWDEV2AF_MSG_TYPE_FW_STATUS && req->fw_up)
> + return 0;
> +
> + if (!rvu_is_switch_pcifunc(rvu, sender))
> + return -EPERM;
> +
> + return 0;
> +}
[Severity: Critical]
Can any mailbox client claim the switchdev role here? The early return
skips the only authorization check for FW_STATUS with fw_up set, and
rvu_sw_l2_ctrl_work_handler() then stores the sender's identity:
rswitch->pcifunc = pcifunc;
rswitch->flags |= RVU_SWITCH_FLAG_FW_READY;
SWDEV2AF_NOTIFY is an ordinary downstream message, and
__rvu_mbox_handler()/rvu_process_mbox_msg() apply no per-message sender ACL
(they only normalize hdr.pcifunc from the mailbox devid), including for the
AF-VF mailbox:
case TYPE_AFVF:
msg->pcifunc &= ~(RVU_PFVF_FUNC_MASK << RVU_PFVF_FUNC_SHIFT);
msg->pcifunc |= (devid << RVU_PFVF_FUNC_SHIFT) + 1;
...
err = rvu_process_mbox_msg(mbox, devid, msg);
rswitch.pcifunc is used as a privilege token elsewhere in the AF:
drivers/net/ethernet/marvell/octeontx2/af/rvu_npc_fs.c:npc_install_flow() {
if (req->set_chanmask && !rvu_is_switch_pcifunc(rvu, req->hdr.pcifunc)) {
...
return NPC_FLOW_VF_PERM_DENIED;
}
nix_txsch_pan_allowed() and nix_lf_alloc() also key off it. Before this
patch nothing ever assigned rswitch.pcifunc, so rvu_is_switch_pcifunc() was
always false. With this change a guest VF sending FW_STATUS/fw_up=true
becomes the switch pcifunc, then passes the sender check for REFRESH_FDB with
an arbitrary pcifunc and MAC, and all af2swdev L2 rule pushes are redirected
to it. Should the FW_STATUS path also verify the sender (device id /
rep_pcifunc), and reject a takeover while a switch pcifunc is already
registered?
[Severity: Medium]
rvu->rswitch.pcifunc becomes mutable at runtime with this patch, written
under the file-local rvu_sw_l2_state_lock, but read without it by
rvu_sw_l2_offl_rule_push():
swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
and by the inline rvu_is_switch_pcifunc() used from rvu_npc_fs.c,
nix_txsch_pan_allowed() and nix_lf_alloc(). A repeated FW_STATUS(fw_up=true)
with a different hdr.pcifunc can change the value while queued offload work
is running, so messages may go to the old or new switch PF. Is a shared
accessor (or storing it under a lock visible to those readers) needed?
> +int rvu_mbox_handler_swdev2af_notify(struct rvu *rvu,
> struct swdev2af_notify_req *req,
> struct msg_rsp *rsp)
> {
> - return 0;
> + int rc;
> +
> + rc = rvu_sw_swdev2af_sender_check(rvu, req, req->msg_type);
> + if (rc)
> + return rc;
> +
> + switch (req->msg_type) {
> + case SWDEV2AF_MSG_TYPE_FW_STATUS:
> + rc = rvu_sw_l2_init_offl_wq(rvu, req->hdr.pcifunc, req->fw_up);
> + break;
[Severity: Medium]
This returns the result of merely queueing work. rvu_sw_l2_init_offl_wq()
does queue_work(wq, &ctrl->work) and returns, while the offload/refresh
queues and RVU_SWITCH_FLAG_FW_READY are set later in
rvu_sw_l2_ctrl_work_handler().
Firmware that sees rc == 0 can immediately send REFRESH_FDB, which
rvu_sw_l2_fdb_list_entry_add() rejects with -EINVAL while fdb_refresh_wq is
still NULL. PF FDB updates arriving in the same window are acknowledged and
dropped by rvu_mbox_handler_fdb_notify():
if (!(rvu->rswitch.flags & RVU_SWITCH_FLAG_FW_READY)) {
spin_unlock_bh(&rvu_sw_l2_state_lock);
return 0;
}
And if either alloc_workqueue() fails, the handler only does dev_err() with
no way to correct the success already reported to firmware. Would it be
better to allocate the queues synchronously in the handler so the return
code reflects readiness?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
> index 5f805bfa81edb..448a442a6ffb2 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l2.c
> @@ -4,11 +4,619 @@
[ ... ]
> +static void rvu_sw_l2_destroy_wqs(struct rvu *rvu)
> +{
> + struct workqueue_struct *offl_wq, *refresh_wq;
> + struct l2_entry *entry;
> +
> + spin_lock_bh(&rvu_sw_l2_state_lock);
> + rvu->rswitch.flags &= ~RVU_SWITCH_FLAG_FW_READY;
> + fw_is_up = false;
> + spin_unlock_bh(&rvu_sw_l2_state_lock);
> +
> + mutex_lock(&fdb_refresh_list_lock);
> + refresh_wq = fdb_refresh_wq;
> + fdb_refresh_wq = NULL;
> + mutex_unlock(&fdb_refresh_list_lock);
> +
> + if (refresh_wq) {
> + cancel_work_sync(&fdb_refresh_work.work);
> + destroy_workqueue(refresh_wq);
[Severity: Medium]
This runs from the rvu_sw_l2_ctrl_wq worker, which is created with
WQ_MEM_RECLAIM:
rvu_sw_l2_ctrl_wq = alloc_ordered_workqueue("rvu_sw_l2_ctrl",
WQ_MEM_RECLAIM);
while refresh_wq and rvu_sw_l2_offl_wq are allocated with flags 0.
destroy_workqueue() always drains, and drain_workqueue() ->
__flush_workqueue() -> check_flush_dependency() warns on exactly this
combination:
kernel/workqueue.c:check_flush_dependency() {
WARN_ONCE(worker && ((worker->current_pwq->wq->flags &
(WQ_MEM_RECLAIM | __WQ_LEGACY)) == WQ_MEM_RECLAIM),
"workqueue: WQ_MEM_RECLAIM %s:%ps is flushing !WQ_MEM_RECLAIM %s:%ps", ...);
The preceding cancel_work_sync() doesn't avoid that drain. Should the
offload/refresh queues also carry WQ_MEM_RECLAIM, or the control queue drop
it?
[ ... ]
> +/* High-frequency link state transitions or aggressive FDB
> + * aging intervals can induce rapid fdb churn. To prevent
> + * thrashing, inhibit hardware offloading of these transient
> + * forwarding states to the switching ASIC. Events are queued
> + * at the tail and processed from the head; when enqueueing a
> + * new operation, drop older pending opposite operations for the
> + * same MAC and port that have not yet reached hardware. When an
> + * opposite entry is removed, the new operation is dropped as well.
> + */
> +static bool rvu_sw_l2_offl_coalesce_pending_locked(struct rvu *rvu,
> + struct l2_entry *new_entry)
> +{
> + u64 opposite = (new_entry->flags & OTX2_FDB_ADD) ? OTX2_FDB_DEL : OTX2_FDB_ADD;
> + struct l2_entry *entry, *tmp;
> + bool coalesced = false;
> +
> + lockdep_assert_held(&l2_offl_list_lock);
> +
> + list_for_each_entry_safe(entry, tmp, &l2_offl_lh, list) {
> + if (!ether_addr_equal(new_entry->mac, entry->mac))
> + continue;
> +
> + if (new_entry->port_id != entry->port_id)
> + continue;
> +
> + if (!(entry->flags & opposite))
> + continue;
> +
> + list_del_init(&entry->list);
> + rvu_sw_l2_list_cnt_dec(rvu->dev, &l2_offl_list_cnt, "offload");
> + kfree(entry);
> + coalesced = true;
> + }
> +
> + return coalesced;
> +}
[Severity: Medium]
Can this leave a stale rule programmed in hardware? The coalescing has no
knowledge of which operations already reached the switch image:
ADD(mac,port) queued -> worker pushes it (hardware entry programmed)
ADD(mac,port) again (bridge fdb update / replace / external-learn re-add)
-> queued, no opposite entry pending
DEL(mac,port) -> matches the pending duplicate ADD, deletes it and
returns coalesced == true
The caller then does:
if (rvu_sw_l2_offl_coalesce_pending_locked(rvu, l2_entry)) {
mutex_unlock(&l2_offl_list_lock);
kfree(l2_entry);
return 0;
}
so the DEL never reaches hardware while the earlier ADD did. Should
coalescing only cancel a pair where neither side has been pushed, e.g. by
collapsing duplicates on enqueue?
> +static int rvu_sw_l2_offl_rule_push(struct rvu *rvu, struct l2_entry *l2_entry)
> +{
> + struct af2swdev_notify_req *req;
> + int swdev_pf;
> +
> + swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
> +
> + mutex_lock(&rvu->mbox_lock);
> + req = otx2_mbox_alloc_msg_af2swdev_notify(rvu, swdev_pf);
> + if (!req) {
> + mutex_unlock(&rvu->mbox_lock);
> + return -ENOMEM;
> + }
> +
> + ether_addr_copy(req->mac, l2_entry->mac);
> + req->flags = l2_entry->flags;
> + req->port_id = l2_entry->port_id;
> +
> + otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, swdev_pf);
> + otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, swdev_pf);
> +
> + mutex_unlock(&rvu->mbox_lock);
> + return 0;
> +}
[Severity: High]
Both new up-message senders here (and in rvu_sw_l2_fdb_refresh_send() on the
afpf and afvf regions) discard the boolean result of
otx2_mbox_wait_for_zero() and never call otx2_mbox_wait_for_rsp(). Every
other AF up-message producer waits for the response before releasing
mbox_lock, e.g.
drivers/net/ethernet/marvell/octeontx2/af/rvu_cgx.c:cgx_notify_pfs() {
otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, pfid);
otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, pfid);
otx2_mbox_wait_for_rsp(&rvu->afpf_wq_info.mbox_up, pfid);
otx2_mbox_msg_send_data() takes exclusive ownership of the shared region on
each send:
mdev->msg_size = 0;
mdev->rsp_size = 0;
mdev->msgs_acked = 0;
...
rx_hdr->num_msgs = 0;
Since rvu_sw_l2_offl_rule_wq_handler() sends up to 16 messages per run and
the refresh worker drains its whole list one message at a time, can this
clobber a message the peer's mbox_up worker is still parsing, and desync
num_msgs/msgs_acked?
> +static int rvu_sw_l2_fdb_refresh_send(struct rvu *rvu, u16 pcifunc, u8 *mac)
> +{
> + struct af2pf_fdb_refresh_req *req;
> + int pf, vf;
[ ... ]
> + if (pf != 0) {
> + if (pf >= rvu->afpf_wq_info.mbox_up.ndevs) {
> + mutex_unlock(&rvu->mbox_lock);
> + return -EINVAL;
> + }
> +
> + req = otx2_mbox_alloc_msg_af2pf_fdb_refresh(rvu, pf);
[Severity: Medium]
For the host switchdev model the changelog targets ("so the host bridge can
learn the updated FDB entry"), how does a refresh reach a representor
netdev? The offload direction accepts rep netdevs:
if (otx2_rep_dev(pdev)) {
struct rep_dev *rep = netdev_priv(dev);
return rep->mdev;
}
but the refresh is addressed by the represented pcifunc and delivered to
that function's parent PF mailbox (or the AF-VF mailbox for pf == 0), and
the only PF-side consumer raises the notifier on pf->netdev. The rvu_rep
driver processes no up-messages at all, so an entry learned on a rep bridge
port is never refreshed, ages out of the host bridge, and the resulting
SWITCHDEV_FDB_DEL_TO_DEVICE tears the hardware rule down each ageing
interval. Is that configuration meant to be supported yet?
> +static void rvu_sw_l2_fdb_refresh_wq_handler(struct work_struct *work)
> +{
[ ... ]
> + rvu_sw_l2_fdb_refresh_send(fdb_work->rvu, l2_entry->port_id,
> + l2_entry->mac);
> + kfree(l2_entry);
> + }
> +}
> +
> +static void rvu_sw_l2_offl_rule_wq_handler(struct work_struct *work)
> +{
[ ... ]
> + if (rvu_sw_l2_offl_rule_push(offl_work->rvu, l2_entry))
> + dev_err(offl_work->rvu->dev,
> + "%s: Error to push l2 rule\n",
> + __func__);
> + /*
> + * TODO: Requeue l2_entry on transient rvu_sw_l2_offl_rule_push()
> + * errors (e.g. ENOMEM, -EBUSY) to keep hardware FDB in sync with
> + * the bridge. Drop-on-failure is known deferred work.
> + */
> + kfree(l2_entry);
[Severity: Medium]
There seems to be no path that ever reconciles hardware with the bridge
after an update is lost. Updates are dropped in several places:
- rvu_mbox_handler_fdb_notify() returns 0 without keeping the update when
RVU_SWITCH_FLAG_FW_READY is clear, and again when rvu_sw_l2_offl_wq is
NULL
- rvu_sw_l2_destroy_wqs() frees all queued entries on firmware-down
- this handler frees the entry after rvu_sw_l2_offl_rule_push() fails
- rvu_sw_l2_fdb_refresh_wq_handler() ignores the return of
rvu_sw_l2_fdb_refresh_send() entirely
- sw_fdb_wq_handler() frees the entry after sw_fdb_add_or_del() fails
On the next firmware-up, rvu_sw_l2_ctrl_work_handler() only re-allocates the
queues and re-arms FW_READY; the bridge won't re-emit
SWITCHDEV_FDB_ADD_TO_DEVICE for entries it already considers offloaded. Is
a replay of the bridge FDB on firmware-up, or a periodic resync, planned?
A dropped delete leaves a stale hardware entry forwarding a MAC to a port
the bridge no longer uses.
> +static void rvu_sw_l2_ctrl_work_handler(struct work_struct *work)
> +{
[ ... ]
> + if (rvu_sw_l2_offl_wq || fdb_refresh_wq)
> + rvu_sw_l2_destroy_wqs(rvu);
> +
> + l2_offl_work.rvu = rvu;
> + INIT_WORK(&l2_offl_work.work, rvu_sw_l2_offl_rule_wq_handler);
> + rvu_sw_l2_offl_wq = alloc_workqueue("swdev_rvu_sw_l2_offl_wq", 0, 0);
> + if (!rvu_sw_l2_offl_wq) {
> + dev_err(rvu->dev, "L2 offl workqueue allocation failed\n");
> + return;
> + }
> +
> + fdb_refresh_work.rvu = rvu;
> + INIT_WORK(&fdb_refresh_work.work, rvu_sw_l2_fdb_refresh_wq_handler);
> + fdb_refresh_wq = alloc_workqueue("swdev_fdb_refresh_wq", 0, 0);
> + if (!fdb_refresh_wq) {
> + dev_err(rvu->dev, "fdb refresh workqueue allocation failed\n");
> + destroy_workqueue(rvu_sw_l2_offl_wq);
> + rvu_sw_l2_offl_wq = NULL;
> + return;
> + }
[Severity: Medium]
These global pointers and work containers are published with no lock and no
release barrier, while every other accessor uses the list mutexes.
rvu_sw_l2_fdb_list_entry_add() reads them under fdb_refresh_list_lock, which
the publisher never takes:
mutex_lock(&fdb_refresh_list_lock);
wq = fdb_refresh_wq;
...
queue_work(wq, &fdb_refresh_work.work);
On a weakly ordered machine, can a concurrent mailbox handler observe the
plain store to fdb_refresh_wq before the INIT_WORK() and .rvu stores are
visible, and then queue work with a stale work->func or stale .rvu?
The failure path here also does destroy_workqueue(rvu_sw_l2_offl_wq) plus
the NULL store outside l2_offl_list_lock and without cancel_work_sync(), and
rvu_sw_l2_shutdown() tests both pointers and dereferences l2_offl_work.rvu
with none of those locks held.
[ ... ]
> int rvu_mbox_handler_fdb_notify(struct rvu *rvu,
> struct fdb_notify_req *req,
> struct msg_rsp *rsp)
> {
> + struct workqueue_struct *wq;
> + struct l2_entry *l2_entry;
> + u32 port_id;
> +
> + spin_lock_bh(&rvu_sw_l2_state_lock);
> + if (!(rvu->rswitch.flags & RVU_SWITCH_FLAG_FW_READY)) {
> + spin_unlock_bh(&rvu_sw_l2_state_lock);
> + return 0;
> + }
> + spin_unlock_bh(&rvu_sw_l2_state_lock);
> +
> + port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
> + if (port_id == RVU_SW_INVALID_PORT_ID)
> + return -EINVAL;
[Severity: High]
Is any sender check missing here? The only validation is that the sender's
pcifunc maps to a representor port id, and rep2pfvf_map[] is populated with
CGX PFs *and* their VFs:
drivers/net/ethernet/marvell/octeontx2/af/rvu_rep.c:rvu_mbox_handler_get_rep_cnt() {
rvu->rep2pfvf_map[rep] = pcifunc;
...
rvu->rep2pfvf_map[rep] = pcifunc | ((vf + 1) & RVU_PFVF_FUNC_MASK);
So a guest VF can send FDB_NOTIFY with an arbitrary req->mac and
req->flags, and the AF queues an L2 rule binding that MAC to the VF's own
switch port (or an OTX2_FDB_DEL for a victim MAC). Legitimate senders come
from the host switchdev notifier via sw_fdb_add_or_del(), i.e. host
PF/representor-parent PFs only. Should VF senders be rejected here?
[Severity: High]
(same site) Related to the above: with the FW_STATUS authorization gap, an
attacker that has made itself rswitch.pcifunc also receives every L2 rule
push (each bridge FDB MAC plus port id) via af2swdev_notify.
> + if (atomic_read(&l2_offl_list_cnt) >= RVU_SW_L2_LIST_MAX) {
> + rvu_sw_l2_list_cnt_warn(rvu->dev, &l2_offl_list_cnt, "offload");
> + return -ENOMEM;
> + }
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
> index fcdf891f90b58..1d86cac5a7e83 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c
> @@ -114,6 +115,38 @@ static void otx2vf_vfaf_mbox_handler(struct work_struct *work)
> +#if IS_ENABLED(CONFIG_OCTEONTX_SWITCH)
> +static int otx2vf_mbox_af2pf_fdb_refresh(struct otx2_nic *vf,
> + struct af2pf_fdb_refresh_req *req,
> + struct msg_rsp *rsp)
> +{
> + struct switchdev_notifier_fdb_info item = {0};
> +
> + /* VM bridge + HW offload: vf->netdev is a br0 port in the guest.
> + * SWITCHDEV_FDB_ADD_TO_BRIDGE on this netdev refreshes the guest
> + * bridge FDB even when accelerated traffic bypasses eth0/eth1 RX
> + * (see rvu_sw_l2_fdb_refresh_send()).
> + */
> + item.addr = req->mac;
> + item.info.dev = vf->netdev;
> + if (req->flags & OTX2_FDB_DEL)
> + call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_BRIDGE,
> + item.info.dev, &item.info, NULL);
> + else
> + call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_BRIDGE,
> + item.info.dev, &item.info, NULL);
> +
> + return 0;
> +}
[Severity: High]
This runs from the VF mailbox-up work handler (otx2vf_vfaf_mbox_handler(),
plain process context) with neither rtnl_lock() nor rcu_read_lock() held.
The same applies to otx2_mbox_up_handler_af2pf_fdb_refresh() in sw_fdb.c
called from otx2_pfaf_mbox_up_handler(). The notifier callee documents the
requirement:
net/bridge/br.c
/* called with RTNL or RCU */
static int br_switchdev_event(...)
{
...
p = br_port_get_rtnl_rcu(dev);
if (!p)
goto out;
br = p->br;
Besides the CONFIG_PROVE_RCU splat from rcu_dereference_rtnl(), can
struct net_bridge_port be freed by a concurrent br_del_if() /
netdev_rx_handler_unregister() between that dereference and the use of
p->br? DSA's dsa_slave_switchdev_event_work() and the mlx5 bridge offload
take rtnl_lock() around this call.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
> index 500451e85b503..e5e20b08ee8ee 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fdb.c
> @@ -4,16 +4,276 @@
[ ... ]
> +static struct otx2_nic *sw_fdb_netdev_to_nic(struct net_device *dev)
> +{
> + struct device *parent = dev->dev.parent;
> +
> + if (parent && parent->bus == &pci_bus_type) {
> + struct pci_dev *pdev = to_pci_dev(parent);
> +
> + if (otx2_rep_dev(pdev)) {
> + struct rep_dev *rep = netdev_priv(dev);
> +
> + return rep->mdev;
> + }
> + }
> +
> + return netdev_priv(dev);
> +}
[Severity: Critical]
Can this return a pointer that isn't a struct otx2_nic? The fallthrough
returns netdev_priv(dev) for any netdev that isn't an RVU representor, and
sw_nb_is_valid_dev() explicitly admits VLAN uppers and bridge devices whose
lowers are merely Cavium-vendor PCI netdevs:
drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c:sw_nb_is_valid_dev() {
if (netif_is_bridge_master(netdev) || is_vlan_dev(netdev)) {
netdev_walk_all_lower_dev_rcu(netdev, sw_nb_check_slaves, &priv);
valid = priv.flags && cnt;
drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c:sw_nb_is_cavium_dev() {
if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
return false;
A VLAN netdev has dev->dev.parent == NULL (net/8021q/vlan_dev.c only does
SET_NETDEV_DEVTYPE, never SET_NETDEV_DEV), so the PCI branch is skipped and
vlan_dev_priv memory is returned typed as struct otx2_nic *.
sw_fdb_add_to_list() then does:
entry->pdev = pci_dev_get(pf->pdev);
reading pf->pdev from an offset past the end of the much smaller VLAN
private area, and the worker later calls pci_get_drvdata() and
mutex_lock(&pf->mbox.lock) through it. The if (!pf) guard can't catch this
because the pointer is non-NULL.
Reproducer shape:
ip link add link eth0 name eth0.100 type vlan id 100
ip link set eth0.100 master br0
# any learned (non-local) MAC on that port
Should sw_fdb_netdev_to_nic() return NULL unless the netdev is known to be
an rvu_nicpf/rvu_nicvf/rvu_rep netdev (e.g. by comparing
dev->netdev_ops or the PCI device id) instead of falling back to
netdev_priv()?
[ ... ]
> +static void sw_fdb_wq_handler(struct work_struct *work)
> +{
> + struct sw_fdb_list_entry *entry;
> + struct otx2_nic *pf;
> + struct workqueue_struct *wq;
> + LIST_HEAD(tlist);
> +
> + spin_lock(&sw_fdb_llock);
> + list_splice_init(&sw_fdb_lh, &tlist);
> + spin_unlock(&sw_fdb_llock);
[Severity: High]
sw_fdb_llock is taken with plain spin_lock() here and in sw_fdb_deinit()
from process context, but also with plain spin_lock() from softirq context
in sw_fdb_add_to_list() (the GFP_ATOMIC allocation and
netdev_hold(..., GFP_ATOMIC) there confirm the atomic context). The
bridge raises the switchdev notifier from the NET_RX softirq learning path:
net/bridge/br_fdb.c:br_fdb_update() {
...
br_switchdev_fdb_notify(br, fdb, RTM_DELNEIGH);
So while this worker holds the lock for list_splice_init(), a softirq on the
same CPU delivering a learned MAC calls sw_fdb_add_to_list() ->
spin_lock(&sw_fdb_llock) on the already-held lock. Should all these sites
use spin_lock_bh()?
> + while ((entry =
> + list_first_entry_or_null(&tlist,
> + struct sw_fdb_list_entry,
> + list)) != NULL) {
> + list_del_init(&entry->list);
> + sw_fdb_list_cnt_dec(entry->dev);
> +
> + spin_lock(&sw_fdb_llock);
> + wq = sw_fdb_wq;
> + spin_unlock(&sw_fdb_llock);
> +
> + pf = wq ? pci_get_drvdata(entry->pdev) : NULL;
> + if (pf && sw_fdb_add_or_del(pf, entry->mac, entry->add_fdb))
[Severity: Critical]
For PF and VF PCI devices, isn't the driver data a struct net_device * and
not a struct otx2_nic *?
drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c:otx2_probe() {
pci_set_drvdata(pdev, netdev);
drivers/net/ethernet/marvell/octeontx2/nic/otx2_vf.c:otx2vf_probe() {
pci_set_drvdata(pdev, netdev);
Only rep.c stores the struct otx2_nic *. The result is passed to
sw_fdb_add_or_del(), which does mutex_lock(&pf->mbox.lock) and
otx2_mbox_alloc_msg_fdb_notify(&pf->mbox) on memory that is actually inside
struct net_device. The void * return of pci_get_drvdata() hides the
mistype at compile time.
sw_fdb_add_to_list() already resolved the correct struct otx2_nic * via
sw_fdb_netdev_to_nic() and then discarded it, keeping only entry->pdev.
Could the entry carry the otx2_nic pointer (with whatever reference makes it
safe) instead of re-deriving it here?
[Severity: High]
Related lifetime question at the same site: what keeps pf->mbox alive until
this work runs? The global sw_fdb_wq is only torn down by
otx2_sw_nb_unregister() (driven by devlink eswitch mode on the RVU_REP
device), so removing the PF/VF instance that owns the referenced mailbox
doesn't cancel or purge the pending entries:
drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c:otx2_remove() {
otx2_pfaf_mbox_destroy(pf);
pci_free_irq_vectors(pf->pdev);
...
pci_set_drvdata(pdev, NULL);
free_netdev(netdev);
The if (pf) check can read a still-valid drvdata and then use pf->mbox after
otx2_pfaf_mbox_destroy() has destroyed the mailbox workqueue and
unmapped/freed the mailbox region. netdev_hold()/pci_dev_get() keep the
net_device and pci_dev allocations alive, but not the mailbox state that is
dereferenced. Should the remove path flush/purge the entries belonging to
that device?
[ ... ]
> +int otx2_mbox_up_handler_af2pf_fdb_refresh(struct otx2_nic *pf,
> + struct af2pf_fdb_refresh_req *req,
> + struct msg_rsp *rsp)
> +{
> + struct switchdev_notifier_fdb_info item = {0};
> +
> + /* FDB refresh is raised from the switch offload path (AF) after
> + * switchdev FDB updates. PF-local ports are refreshed on pf->netdev.
> + * TODO: When req->hdr.pcifunc targets a guest VF (VM-bridged offload),
> + * forward the refresh via the PF-VF mailbox instead of applying it to
> + * pf->netdev; otherwise guest-owned MACs may age out prematurely (see
> + * rvu_sw_l2_fdb_refresh_send()).
> + */
[Severity: Low]
This comment and the one in rvu_sw_l2_fdb_refresh_send() describe opposite
behaviour for the same message. Here the claim is that a VF-targeted
refresh is applied to pf->netdev and PF-VF forwarding is a TODO, but the PF
up-mbox dispatch never processes such a message locally:
drivers/net/ethernet/marvell/octeontx2/nic/otx2_pf.c:otx2_pfaf_mbox_up_handler() {
devid = msg->pcifunc & RVU_PFVF_FUNC_MASK;
/* Skip processing VF's messages */
if (!devid)
otx2_process_mbox_msg_up(pf, msg);
The rvu_sw_l2_fdb_refresh_send() comment states this correctly ("the parent
PF mailbox forwards the message to the guest VF driver"). Could the TODO
here be dropped or reworded?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> index b51d8d2d01b8c..c947f30becc83 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb.c
> @@ -191,13 +191,17 @@ static int sw_nb_fdb_event(struct notifier_block *unused,
> switch (event) {
> case SWITCHDEV_FDB_ADD_TO_DEVICE:
> - if (fdb_info->is_local)
> - break;
> - break;
> -
> case SWITCHDEV_FDB_DEL_TO_DEVICE:
> if (fdb_info->is_local)
> break;
> + /* dev is the bridge port that learned the FDB
> + * (SWITCHDEV_FDB_*_TO_DEVICE), not the bridge master.
> + * sw_nb_is_valid_dev() limits this to Cavium-offloaded
> + * setups; only Cavium PF/representor netdevs are supported
> + * as bridge ports today (VLAN/virt under bridge is TODO).
> + */
> + sw_fdb_add_to_list(dev, (u8 *)fdb_info->addr,
> + event == SWITCHDEV_FDB_ADD_TO_DEVICE);
> break;
[Severity: High]
The comment says representor netdevs are supported bridge ports, but does
the AF accept an FDB update that originates from one? fdb_notify_req has no
port field:
struct fdb_notify_req {
struct mbox_msghdr hdr;
u64 flags;
u8 mac[ETH_ALEN];
u8 rsvd[2];
};
so the AF derives the port from req->hdr.pcifunc only. For a rep bridge
port, sw_fdb_add_or_del() sends on the representor PF's own mailbox
(pf = rep->mdev), so hdr.pcifunc is the RVU_REP PF, not the represented
function. rvu_sw_port_id() looks that up in rep2pfvf_map[] (populated only
with CGX PFs and their VFs), rvu_rep_get_vlan_id() returns 0, the
rep2pfvf_map[0] != pcifunc recheck fails, and
rvu_mbox_handler_fdb_notify() returns -EINVAL:
port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
if (port_id == RVU_SW_INVALID_PORT_ID)
return -EINVAL;
That would make every learn/flush on a rep bridge port fail with "Error to
add/del fdb %pM entry", and even if accepted, all rep ports share one
mailbox so they'd collapse to a single identity. Does fdb_notify_req need a
port_id/pcifunc field for this to work?
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox
2026-08-31 13:19 ` [PATCH v9 net-next 8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox Ratheesh Kannoth
@ 2026-09-03 19:24 ` netdev-bot+sashiko
0 siblings, 0 replies; 18+ messages in thread
From: netdev-bot+sashiko @ 2026-09-03 19:24 UTC (permalink / raw)
To: rkannoth
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
Thank you for your contribution! Sashiko AI review found 14 potential
issue(s) to consider:
Critical: 0 · High: 5 · Medium: 7 · Low: 2
- [High] sw_fib_add_to_list() stores `netdev_priv(dev)` as `struct
otx2_nic *` in the queued sw_fib_list_entry and the work handler later…
- [High] rvu_sw_l3_offl_rule_push() allocates an af2swdev_notify_req in
the AF->PF up mailbox, fills it, and then does `rc =…
- [High] The new AF-side FIB queue has no length limit: every accepted
FIB_NOTIFY kcalloc()s an l3_entry of sizeof(*l3_entry) + cnt*80 bytes…
- [High] Global list `l3_offl_lh` mixes FIB offload entries from multiple
RVU devices, causing misdirected offloads and cross-device corruption.
- [High] FIB notification retry logic causes out-of-order application of
routing rules, leading to a stale hardware FIB.
- [Medium] rvu_mbox_handler_fib_notify() (af/switch/rvu_sw_l3.c) stores
the result of rvu_sw_port_id() without checking the documented failure…
- [Medium] In sw_nb_v4_fib_event() the comment "Point-to-point routes,
including default routes with no gateway, are not supported for switch…
- [Medium] The new offload path is best-effort and can permanently
desynchronise the switch FIB from the host FIB:…
- [Medium] sw_nb_v4_fib_event() now iterates
fib_info_num_path()/fib_info_nhc() and unconditionally does `fib_nh =…
- [Medium] The new L3 offload path reads rvu->rswitch.flags
(RVU_SWITCH_FLAG_FW_READY) in rvu_mbox_handler_fib_notify() and…
- [Medium] rvu_mbox_handler_fib_notify() accepts MBOX_MSG_FIB_NOTIFY from
any requester reaching the AF mailbox - including VFs, whose messages…
- [Medium] rvu_mbox_handler_fib_notify() re-reads req->cnt from the
AF<->PF shared mailbox region three times: for the bound check, to…
- [Low] The comment in sw_nb_v4_fib_event() states that external nexthop
routes (fi->nh set) are "intentionally skipped until…
- [Low] sw_fib_add_to_list() evaluates the queue bound
`sw_fib_list_count() >= SW_FIB_LIST_MAX` outside sw_fib_llock and…
--
octeontx2: switch: offload host FIB updates to switch via AF mailbox
This queues IPv4/IPv6 FIB-derived updates from the switchdev notifier
path into a per-PF work queue, and adds an AF-side handler for
fib_notify that batches fib_entry records and forwards them to the
switch PF over the AF-to-switchdev FIB_CMD. The AF path requires
RVU_SWITCH_FLAG_FW_READY before accepting work.
> diff --git a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
> index 2b798d5f06446..32735ae68e1d3 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/af/switch/rvu_sw_l3.c
[ ... ]
> +static struct af2swdev_notify_req __maybe_unused
> +*otx2_mbox_alloc_msg_af2swdev_notify(struct rvu *rvu, int devid)
> +{
> + struct af2swdev_notify_req *req;
> +
> + req = (struct af2swdev_notify_req *)
> + otx2_mbox_alloc_msg_rsp(&rvu->afpf_wq_info.mbox_up, devid,
> + sizeof(*req), sizeof(struct msg_rsp));
> + if (!req)
> + return NULL;
> + req->hdr.sig = OTX2_MBOX_REQ_SIG;
> + req->hdr.id = MBOX_MSG_AF2SWDEV;
> + return req;
> +}
[ ... ]
> +static int rvu_sw_l3_offl_rule_push(struct list_head *lh)
> +{
[ ... ]
> + rvu = l3_entry->rvu;
> + swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
[Severity: Medium]
Is rvu->rswitch.pcifunc stable here? rvu_mbox_handler_fib_notify() tests
RVU_SWITCH_FLAG_FW_READY, but this worker runs at least 10ms later and does
not re-check it. Both fields are written under the file-static
rvu_sw_l2_state_lock in rvu_sw_l2.c:
rvu_sw_l2_destroy_wqs()
spin_lock_bh(&rvu_sw_l2_state_lock);
rvu->rswitch.flags &= ~RVU_SWITCH_FLAG_FW_READY;
...
rvu->rswitch.pcifunc = 0;
and the peer reader rvu_mbox_handler_fdb_notify() takes that spinlock before
reading the flag, while the new L3 code reads both fields unlocked.
If firmware goes down between the handler and the worker, does this end up
computing swdev_pf = rvu_get_pf(rvu->pdev, 0) and sending an AF2SWDEV
up-message to an unrelated PF? rvu_sw_l2_destroy_wqs() also does not drain
l3_offl_lh or cancel l3_offl_work.
> + mutex_lock(&rvu->mbox_lock);
> + req = otx2_mbox_alloc_msg_af2swdev_notify(rvu, swdev_pf);
> + if (!req) {
> + mutex_unlock(&rvu->mbox_lock);
> + return -ENOMEM;
> + }
> +
> + dst = &req->entry[0];
[ ... ]
> + while ((l3_entry =
> + list_first_entry_or_null(lh,
> + struct l3_entry, list)) != NULL) {
> + entry = l3_entry->entry;
> + cnt = l3_entry->cnt;
> +
> + /* af2swdev_notify_req.entry[] holds RVU_SW_L3_ENTRY_MAX slots;
> + * stop before copying the next l3_entry when the mbox buffer
> + * would overflow. Leftovers stay on lh and are re-queued.
> + */
> + if (tot_cnt + cnt > RVU_SW_L3_ENTRY_MAX)
> + break;
> +
> + sz = sizeof(*entry) * cnt;
> +
> + memcpy(dst, entry, sz);
> + for (i = 0; i < cnt; i++)
> + dst[i].port_id = l3_entry->port_id;
[Severity: Medium]
Correction to the finding id above: the finding is gpt-5-6-sol-2-15.
Should the port id stamped into every fib_entry here be validated first?
rvu_mbox_handler_fib_notify() stores the result of rvu_sw_port_id() without
checking the sentinel:
l3_entry->port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
rvu_sw_port_id() in af/switch/rvu_sw.c returns RVU_SW_INVALID_PORT_ID
((u32)~0U) when !rvu->rep2pfvf_map || !rvu->rep_cnt, or when the map lookup
misses. rvu_rep_get_rep_cnt() only populates that map for CGX-mapped PFs
and their VFs, so a route whose nexthop netdev belongs to an LBK/SDP or
otherwise non-CGX-mapped RVU function produces the sentinel.
The peer handler checks it:
af/switch/rvu_sw_l2.c:rvu_mbox_handler_fdb_notify()
port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
if (port_id == RVU_SW_INVALID_PORT_ID)
return -EINVAL;
Should the L3 handler do the same rather than handing 0xFFFFFFFF to switch
firmware as an egress port?
> + tot_cnt += cnt;
> + dst += cnt;
> +
> + list_del_init(&l3_entry->list);
> + kfree(l3_entry);
> + }
[Severity: Medium]
Can l3_entry->cnt disagree with the number of fib_entry slots actually
allocated? rvu_mbox_handler_fib_notify() reads req->cnt from the shared
AF<->PF mailbox region three separate times:
if (!req->cnt || req->cnt > RVU_SW_L3_ENTRY_MAX)
return -EINVAL;
sz = req->cnt * sizeof(struct fib_entry);
l3_entry = kcalloc(1, sizeof(*l3_entry) + sz, GFP_KERNEL);
...
l3_entry->cnt = req->cnt;
The kcalloc() between the second and third fetch forces a reload, so if a
peer raises req->cnt in that window, l3_entry->cnt ends up larger than the
allocated flexible array. The consumer above then does:
sz = sizeof(*entry) * cnt;
memcpy(dst, entry, sz);
guarded only by tot_cnt + cnt > RVU_SW_L3_ENTRY_MAX, which would copy past
the allocation into the outgoing message. A stale value above 12 instead
makes the loop break every time, leaving an entry that never drains and
re-arms the delayed work every 100ms.
Would a single fetch into a local, used for the bound check, the allocation
size, the memcpy and the stored count, be preferable?
> + if (!tot_cnt) {
> + mutex_unlock(&rvu->mbox_lock);
> + return -EINVAL;
> + }
> +
> + req->flags = OTX2_FIB_CMD;
> + req->cnt = tot_cnt;
> +
> + rc = otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, swdev_pf);
> + if (rc)
> + otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, swdev_pf);
> +
> + mutex_unlock(&rvu->mbox_lock);
> + return rc ? 0 : -EFAULT;
> +}
[Severity: High]
What happens to the allocated up-message when otx2_mbox_wait_for_zero()
returns false? The doorbell is skipped, but otx2_mbox_alloc_msg_rsp() has
already advanced mdev->msg_size and mdev->num_msgs, and those are cleared
only by otx2_mbox_msg_send_data() or otx2_mbox_reset().
sizeof(struct af2swdev_notify_req) is 1000, ALIGN'd to 1008, and the usable
up-TX window is MBOX_UP_TX_SIZE(SZ_1K) minus the 16-byte header offset, also
1008. So one stranded message consumes the entire window and every later
AF->switch-PF up message (further FIB batches, af2swdev FDB notifies, link
events) fails to allocate until an unrelated up-response triggers
otx2_mbox_reset().
The sibling L2 path does not leave this state:
af/switch/rvu_sw_l2.c:rvu_sw_l2_offl_rule_push()
otx2_mbox_wait_for_zero(&rvu->afpf_wq_info.mbox_up, swdev_pf);
otx2_mbox_msg_send_up(&rvu->afpf_wq_info.mbox_up, swdev_pf);
otx2_mbox_wait_for_zero() polls, sleeps ~1ms, re-reads and returns false if
the peer has not acked, which is routine under load. Should the L3 path
send unconditionally too?
[Severity: Medium]
The entries are list_del/kfree'd inside the copy loop above, before delivery
is established, so on the -EFAULT return the batch is gone with nothing to
replay. sw_l3_offl_work_handler() only requeues entries still on the
temporary list:
if (rvu_sw_l3_offl_rule_push(&l3lh))
pr_err("%s: Error to push rules\n", __func__);
Combined with rvu_mbox_handler_fib_notify() returning -EAGAIN until
RVU_SWITCH_FLAG_FW_READY (with no FIB re-walk once firmware comes up), the
permanent drop after SW_FIB_NOTIFY_RETRY_MAX in sw_fib_work_handler(), and
the discarded sw_fib_add_to_list() return values in sw_nb_v4.c/sw_nb_v6.c,
can the switch FIB silently diverge from the host FIB? None of these
limitations are mentioned in the commit message.
[ ... ]
> int rvu_mbox_handler_fib_notify(struct rvu *rvu,
> struct fib_notify_req *req,
> struct msg_rsp *rsp)
> {
> + struct l3_entry *l3_entry;
> + int sz, rc;
> +
> + if (!(rvu->rswitch.flags & RVU_SWITCH_FLAG_FW_READY))
> + return -EAGAIN;
> +
> + /* Reject notifies larger than the source fib_notify_req.entry[]. */
> + if (!req->cnt || req->cnt > RVU_SW_L3_ENTRY_MAX)
> + return -EINVAL;
[Severity: High]
Should l3_offl_lh, sw_l3_offl_wq and going_down be per-device state
rather than file-static? struct rvu is allocated once per RVU PCI
function, so on a board with more than one RVU device every AF
instance queues onto this single list and shares one delayed work
item.
rvu_sw_l3_offl_rule_push() then picks its target from whichever
l3_entry happens to be at the head:
rvu = l3_entry->rvu;
swdev_pf = rvu_get_pf(rvu->pdev, rvu->rswitch.pcifunc);
and the copy loop below batches every following l3_entry into that
device's afpf_wq_info.mbox_up without ever comparing l3_entry->rvu
against the rvu it selected. Entries that arrived from a second RVU
device would be stamped with a port_id derived from that device's
rep2pfvf_map and programmed into the first device's switch FIB. The
comment asserting "one octeontx2 AF driver instance, one switch PF
(switchdev), and one sw_l3_offl_wq per SoC" is doing a lot of work
here - is that guaranteed by something in the AF driver, or is it the
single-card configuration that was tested?
The same assumption bites teardown: rvu_sw_l3_shutdown() sets
going_down, NULLs sw_l3_offl_wq, destroys the workqueue and drains
l3_offl_lh, so unbinding one RVU device discards queued work belonging
to the others and leaves them with offload disabled until some later
rvu_sw_l3_clear_shutdown() and rvu_sw_l3_ensure_wq() re-create it.
rvu_sw_l2_clear_shutdown() suggests the L2 path is built on the same
file-static going_down, so this may be a deliberate subsystem-wide
assumption rather than something new here. If multi-RVU really cannot
happen, could that be stated once (and enforced) instead of relied on
implicitly? If it can, both the list and the workqueue look like they
belong in struct rvu, or the push loop at minimum needs to stop
batching entries whose l3_entry->rvu differs from the selected one.
[Severity: Medium]
Is any sender authorization intended here? MBOX_MSG_FIB_NOTIFY is accepted
from any requester that reaches the AF mailbox, including VFs -
otx2_pf.c:otx2_pfvf_mbox_handler() relays them with the VF's pcifunc:
err = otx2_forward_vf_mbox_msgs(pf, mbox, MBOX_DIR_PFAF, vf_idx,
vf_mbox->num_msgs);
The fib_entry[] contents (the u64 cmd opcode, dst/dst_len, gw, mac and the
host/bridge/vlan/ipv6 bits) are forwarded to switch firmware verbatim; only
port_id is re-derived by the AF. Can a VF assigned to an untrusted guest
install or delete switch FIB/host-route and neighbour-MAC bindings for other
ports?
For context, rvu_mbox_handler_fdb_notify() has the same exposure today, so
this looks like the existing trust model of this subsystem rather than
something introduced here; the closest authorization precedent is
rvu_sw_swdev2af_sender_check(), which guards the opposite (switch->AF)
direction.
> +
> + sz = req->cnt * sizeof(struct fib_entry);
> +
> + l3_entry = kcalloc(1, sizeof(*l3_entry) + sz, GFP_KERNEL);
> + if (!l3_entry)
> + return -ENOMEM;
> +
> + l3_entry->port_id = rvu_sw_port_id(rvu, req->hdr.pcifunc);
> + l3_entry->rvu = rvu;
> + l3_entry->cnt = req->cnt;
> + INIT_LIST_HEAD(&l3_entry->list);
> + memcpy(l3_entry->entry, req->entry, sz);
> +
> + /* Host PFs on this RVU share one AF and one switch PF offload path. */
> + mutex_lock(&l3_offl_llock);
[ ... ]
> + list_add_tail(&l3_entry->list, &l3_offl_lh);
> + mutex_unlock(&l3_offl_llock);
> + rvu_sw_l3_queue_work();
> +
> return 0;
> }
[Severity: High]
Is there a bound on l3_offl_lh? Each accepted notify kcalloc()s
sizeof(*l3_entry) + cnt * 80 bytes with GFP_KERNEL and appends it
unconditionally, while rvu_sw_l3_offl_rule_push() forwards at most
RVU_SW_L3_ENTRY_MAX (12) records per push and re-arms after 10-100ms.
The sibling L2 handler caps its list:
af/switch/rvu_sw_l2.c:rvu_mbox_handler_fdb_notify()
if (atomic_read(&l2_offl_list_cnt) >= RVU_SW_L2_LIST_MAX) {
...
return -ENOMEM;
}
Since a PF or VF can emit FIB_NOTIFY directly, the PF-side SW_FIB_LIST_MAX
does not limit growth on the AF. Can sustained route churn or a buggy VF
grow this list without limit?
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
> index f4c47111d7633..318f7b68b8e4e 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_fib.c
[ ... ]
> +static void sw_fib_list_entry_destroy(struct sw_fib_list_entry *lentry)
> +{
> + struct net_device *dev = lentry->pf->netdev;
> +
> + sw_fib_list_cnt_dec(dev);
> + netdev_put(dev, &lentry->dev_tracker);
> + kfree(lentry->entry);
> + kfree(lentry);
> +}
> +
> +static int sw_fib_notify(struct otx2_nic *pf,
> + int cnt,
> + struct fib_entry *entry)
> +{
> + struct fib_notify_req *req;
> + int rc;
> +
> + if (cnt > RVU_SW_L3_ENTRY_MAX)
> + return -EINVAL;
> +
> + mutex_lock(&pf->mbox.lock);
> + req = otx2_mbox_alloc_msg_fib_notify(&pf->mbox);
[Severity: High]
Is lentry->pf guaranteed to point at a struct otx2_nic? sw_fib_add_to_list()
stores it from netdev_priv(dev) and this work-context code dereferences it
for mutex_lock(&pf->mbox.lock) and &pf->mbox, while
sw_fib_list_entry_destroy() uses lentry->pf->netdev as the netdev to
netdev_put() the tracked reference against.
The only vetting is sw_nb_is_valid_dev()/sw_nb_resolve_pf_dev(), which
bottom out in sw_nb_is_cavium_dev() checking just the PCI vendor:
nic/switch/sw_nb.c:sw_nb_is_cavium_dev()
if (pdev->vendor != PCI_VENDOR_ID_CAVIUM)
...
drivers/net/ethernet/cavium/liquidio, cavium/thunder/nicvf_main.c and
marvell/octeon_ep/octep_main.c all bind PCI_VENDOR_ID_CAVIUM devices and
register netdevs with SET_NETDEV_DEV(). If such a netdev is used as a route
nexthop or gets an address, does this take a mutex at whatever lies at
struct otx2_nic's mbox offset inside a foreign driver's private area, and
netdev_put() a tracker against the wrong netdev?
Before this patch the mis-typed pointer was only used to read pf->pcifunc
into a record that was freed immediately; this patch stores the pointer and
dereferences it later from work context.
> + if (!req) {
> + rc = -ENOMEM;
> + goto out;
> + }
[ ... ]
> +int sw_fib_add_to_list(struct net_device *dev,
> + struct fib_entry *entry, int cnt)
> +{
> + struct otx2_nic *pf = netdev_priv(dev);
> + struct sw_fib_list_entry *lentry;
> + struct workqueue_struct *wq;
[ ... ]
> + if (sw_fib_list_count() >= SW_FIB_LIST_MAX) {
> + kfree(entry);
> + return -ENOMEM;
> + }
[Severity: Low]
This bound is evaluated outside sw_fib_llock while the matching
sw_fib_list_cnt_inc()/list_add_tail() happen inside the lock a few lines
below, and sw_fib_list_entry_destroy() decrements outside the lock too.
Concurrent producers (the RTNL-context notifiers versus the netevent-driven
sw_nb_net_v4_neigh_update()/sw_nb_net_v6_neigh_update() on another CPU) can
all pass the check and push past the cap.
The added sw_fib_list_cnt_warn() "FIB list count overflow" branch seems to
acknowledge this. Would testing and incrementing inside the existing
spin_lock_bh() region avoid it?
> +
> + lentry = kcalloc(1, sizeof(*lentry), GFP_ATOMIC);
[ ... ]
> diff --git a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
> index 31009e00121f6..7ee3a98fc50d5 100644
> --- a/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
> +++ b/drivers/net/ethernet/marvell/octeontx2/nic/switch/sw_nb_v4.c
[ ... ]
> @@ -69,6 +76,10 @@ int sw_nb_v4_netdev_event(struct notifier_block *unused,
> entry->vlan_tag = cpu_to_be16(vlan_dev_vlan_id(dev));
> }
>
> + /* Switch offload is only enabled on OcteonTX2/CN10K SoCs. pf_dev is an
> + * octeontx2 PF or representor netdev, so netdev_priv() is otx2_nic even
> + * though sw_nb_is_cavium_dev() matches the shared Cavium PCI vendor ID.
> + */
[Severity: High]
Is the representor half of this comment accurate? rep.c allocates
representor netdevs with alloc_etherdev(sizeof(struct rep_dev)), so
netdev_priv() there is struct rep_dev, not struct otx2_nic. (In practice
representors do not reach here because they only call
SET_NETDEV_DEVLINK_PORT, leaving dev.parent NULL so sw_nb_is_cavium_dev()
rejects them - but the comment states the opposite and is used to justify
the unchecked cast discussed in sw_fib_notify() above.)
[ ... ]
> @@ -186,19 +205,16 @@ int sw_nb_v4_fib_event(struct notifier_block *nb,
> * are walked below; nhid and nexthop-group installs are intentionally
> * skipped until fib_info_num_path()/fib_info_nhc() handling is added.
> */
> - entries = kcalloc(fi->fib_nhs, sizeof(*entries), GFP_ATOMIC);
> - if (!entries)
> + if (!nhs)
> return NOTIFY_DONE;
[Severity: Low]
The retained comment says nhid and nexthop-group installs are "intentionally
skipped until fib_info_num_path()/fib_info_nhc() handling is added", but this
hunk switches the function to exactly those helpers. include/net/nexthop.h
dispatches them to nexthop_num_path(fi->nh)/nexthop_fib_nhc(fi->nh, i) when
fi->nh is set, so external nexthop routes are now walked. Should the comment
be updated?
[ ... ]
> - iter = entries;
> - fib_nh = fi->fib_nh;
> - for (i = 0; i < fi->fib_nhs; i++, fib_nh++) {
> + for (i = 0; i < nhs; i++) {
> + nhc = fib_info_nhc(fi, i);
> + fib_nh = container_of(nhc, struct fib_nh, nh_common);
> dev = fib_nh->fib_nh_dev;
[Severity: Medium]
Can nhc here belong to a struct fib6_nh? For a route installed as:
ip nexthop add id N via <v6addr> dev X
ip route add <v4prefix> nhid N
fib_info_nhc() -> nexthop_fib_nhc() returns the nh_common of a struct
fib6_nh. The existing fi->fib_nh_is_v6 guard above cannot catch it because
net/ipv4/fib_semantics.c sets that flag only inside the if (!fi->nh) branch.
The loop then reads fib_nh->nh_saddr, which exists only in struct fib_nh:
if (fib_nh->nh_saddr)
haddr[hcnt++] = fib_nh->nh_saddr;
and treats the first 4 bytes of nhc_gw.ipv6 as an IPv4 gateway, setting
gw_valid, programming it into the switch and passing it to:
neigh = ip_neigh_gw4(fib_nh->fib_nh_dev, fib_nh->fib_nh_gw4);
ip_neigh_gw4() calls __neigh_create(), so does this create a real ARP
neighbour entry (with ARP probes on the wire) for a fabricated IPv4 address?
Pre-patch the loop walked fi->fib_nh/fi->fib_nhs, which is empty for nhid
routes, so this looks newly reachable. Note nh_info is kzalloc'd and
nh_saddr sits past the end of struct fib6_nh, so the bytes read there are
zeros rather than leaked heap contents.
[ ... ]
> @@ -210,107 +226,118 @@ int sw_nb_v4_fib_event(struct notifier_block *nb,
> /* Point-to-point routes, including default routes with no
> * gateway, are not supported for switch offload.
> */
> - if (!fib_nh->fib_nh_gw4)
> + if (!fib_nh->fib_nh_gw4) {
> + if (!entry->dst && !entry->dst_len) {
> + kfree(entry);
> + continue;
> + }
> + sw_fib_add_to_list(nh_pf_dev, entry, 1);
> continue;
> - iter->gw_valid = 1;
> + }
[Severity: High]
The added TODO already describes this, but should a known
kernel-versus-hardware FIB divergence be merged in this state?
sw_fib_work_handler() splices the whole of sw_fib_lh into tlist and
walks it in order. When sw_fib_notify() fails - otx2_sync_mbox_msg()
returning an error while the AF mailbox is busy, or the AF handler
rvu_mbox_handler_fib_notify() returning -EAGAIN because
RVU_SWITCH_FLAG_FW_READY is not set yet - the failed lentry is put on
the tail of sw_fib_lh and the loop continues draining the rest of
tlist. Every subsequent entry, including a DEL for the same prefix
that the kernel generated after the failed ADD, is now applied before
the retry. When the ADD eventually succeeds the hardware keeps a
route the kernel has already withdrawn, and nothing later reconciles
it; after SW_FIB_NOTIFY_RETRY_MAX the entry is simply dropped, which
has the same effect in the opposite direction.
Since every producer in sw_nb_v4.c and sw_nb_v6.c funnels
single-entry records through sw_fib_add_to_list() into this one list,
the list order is the only thing carrying FIB event ordering to the
ASIC, so losing it is not a corner case under mailbox contention.
Would it work to keep the batch strictly in order on failure - put the
failed lentry back at the head of tlist, splice tlist in front of
sw_fib_lh, re-arm the delayed work and break out of the loop - so that
nothing behind the failed entry is applied early? If there is a
reason retries must not block later entries, could that reasoning go
in the commit message together with the divergence it implies?
[Severity: Medium]
The comment directly above says gateway-less routes are not supported for
switch offload, but this now offloads them for any prefix other than
0.0.0.0/0, with gw_valid and mac_valid left at 0. Should the comment be
updated, and should the commit message mention that directly connected IPv4
routes are now programmed into the ASIC?
The IPv6 side in sw_nb_v6_fib_event() leaves the same case as an explicit
TODO:
/*
* TODO: Offload directly connected IPv6 subnets without an IPv6
* gateway. ...
*/
so the two families now behave differently.
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260831131944.2649362-1-rkannoth%40marvell.com
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [v9,net-next,7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev
2026-09-03 19:24 ` [v9,net-next,7/8] " netdev-bot+sashiko
@ 2026-09-04 3:15 ` Ratheesh Kannoth
0 siblings, 0 replies; 18+ messages in thread
From: Ratheesh Kannoth @ 2026-09-04 3:15 UTC (permalink / raw)
To: netdev-bot+sashiko
Cc: linux-kernel, netdev, andrew+netdev, davem, edumazet, kuba,
pabeni, sgoutham
On 2026-09-04 at 00:54:50, netdev-bot+sashiko@kernel.org (netdev-bot+sashiko@kernel.org) wrote:
> Thank you for your contribution! Sashiko AI review found 19 potential
> issue(s) to consider:
>
> Critical: 3 · High: 6 · Medium: 9 · Low: 1
>
will address in v10
pw-bot: changes-requested
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-09-04 3:15 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-31 13:19 [PATCH v9 net-next 0/8] Switch support Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 1/8] octeontx2-af: switch: Add AF to switch mbox and skeleton files Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,1/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 2/8] octeontx2-af: switch: Add switch dev to AF mboxes Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,2/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 3/8] octeontx2-pf: switch: Add pf files hierarchy Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,3/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 4/8] octeontx2-af: switch: Representor for switch port Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,4/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 5/8] octeontx2-af: switch: TL1 scheduling and NPC channel control Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,5/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 6/8] octeontx2-pf: switch: Register notifiers for switch offload Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,6/8] " netdev-bot+sashiko
2026-08-31 13:19 ` [PATCH v9 net-next 7/8] octeontx2: switch: plumb bridge FDB updates through AF and switchdev Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,7/8] " netdev-bot+sashiko
2026-09-04 3:15 ` Ratheesh Kannoth
2026-08-31 13:19 ` [PATCH v9 net-next 8/8] octeontx2: switch: offload host FIB updates to switch via AF mailbox Ratheesh Kannoth
2026-09-03 19:24 ` [v9,net-next,8/8] " netdev-bot+sashiko
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox