From: netdev-bot+sashiko@kernel.org
To: rkannoth@marvell.com
Cc: linux-kernel@vger.kernel.org, netdev@vger.kernel.org,
andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, sgoutham@marvell.com
Subject: Re: [v9,net-next,6/8] octeontx2-pf: switch: Register notifiers for switch offload
Date: Thu, 03 Sep 2026 19:24:49 +0000 [thread overview]
Message-ID: <178846348905.3394541.7517069116859509457@kernel.org> (raw)
In-Reply-To: <20260831131944.2649362-7-rkannoth@marvell.com>
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
next prev parent reply other threads:[~2026-09-03 19:25 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` netdev-bot+sashiko [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=178846348905.3394541.7517069116859509457@kernel.org \
--to=netdev-bot+sashiko@kernel.org \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=rkannoth@marvell.com \
--cc=sgoutham@marvell.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox