* [net-next PATCH 06/10] net: dsa: realtek: rtl8365mb: add VLAN support
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Realtek RTL8365MB switches (a.k.a. RTL8367C family) use two different
structures for VLANs:
- VLAN4K: A full table with 4096 entries defining port membership and
tagging.
- VLANMC: A smaller table with 32 entries used primarily for PVID
assignment.
In this hardware, a port's PVID must point to an index in the VLANMC
table rather than a VID directly. Since the VLANMC table is limited to
32 entries, the driver implements a dynamic allocation scheme to
maximize resource usage:
- VLAN4K is treated by the driver as the source of truth for membership.
- A VLANMC entry is only allocated when a port is configured to use a
specific VID as its PVID.
- VLANMC entries are deleted when no longer needed as a PVID by any port.
Although VLANMC has a members field, the switch only checks membership
in the VLAN4K table. However, when a corresponding VLAN entry also exists
in VLANMC, this driver keeps both membership configurations in sync.
VLANMC index 0, although a valid entry, is reserved in this driver as a
neutral PVID value for ports not using a specific PVID.
In the subsequent RTL8367D switch family, VLANMC table was
removed and PVID assignment was delegated to a dedicated set of
registers.
All ports start isolated, forwarding exclusively to CPU ports, and
with VLAN transparent, ignoring VLAN membership. Once a member in a
bridge, the port isolation is expanded to include the bridge members.
When that bridge enables VLAN filtering, the VLAN transparent feature is
disabled, letting the switch filter based on VLAN setup.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
drivers/net/dsa/realtek/Makefile | 1 +
drivers/net/dsa/realtek/rtl8365mb_main.c | 256 ++++++++++
drivers/net/dsa/realtek/rtl8365mb_vlan.c | 805 +++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl8365mb_vlan.h | 30 ++
4 files changed, 1092 insertions(+)
diff --git a/drivers/net/dsa/realtek/Makefile b/drivers/net/dsa/realtek/Makefile
index 99654c4c5a3d..b7fc4e852fd8 100644
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -18,3 +18,4 @@ endif
obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
rtl8365mb-objs := rtl8365mb_main.o \
rtl8365mb_table.o \
+ rtl8365mb_vlan.o \
diff --git a/drivers/net/dsa/realtek/rtl8365mb_main.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
index 530ff5503543..c604bd744d38 100644
--- a/drivers/net/dsa/realtek/rtl8365mb_main.c
+++ b/drivers/net/dsa/realtek/rtl8365mb_main.c
@@ -104,6 +104,7 @@
#include "realtek-smi.h"
#include "realtek-mdio.h"
#include "rtl83xx.h"
+#include "rtl8365mb_vlan.h"
/* Family-specific data and limits */
#define RTL8365MB_PHYADDRMAX 7
@@ -292,6 +293,67 @@
#define RTL8365MB_MSTI_CTRL_PORT_STATE_MASK(_physport) \
(0x3 << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET((_physport)))
+/* Miscellaneous port configuration register, incl. VLAN egress mode */
+#define RTL8365MB_PORT_MISC_CFG_REG_BASE 0x000E
+#define RTL8365MB_PORT_MISC_CFG_REG(_p) \
+ (RTL8365MB_PORT_MISC_CFG_REG_BASE + ((_p) << 5))
+#define RTL8365MB_PORT_MISC_CFG_SMALL_TAG_IPG_MASK 0x8000
+#define RTL8365MB_PORT_MISC_CFG_TX_ITFSP_MODE_MASK 0x4000
+#define RTL8365MB_PORT_MISC_CFG_FLOWCTRL_INDEP_MASK 0x2000
+#define RTL8365MB_PORT_MISC_CFG_DOT1Q_REMARK_ENABLE_MASK 0x1000
+#define RTL8365MB_PORT_MISC_CFG_INGRESSBW_FLOWCTRL_MASK 0x0800
+#define RTL8365MB_PORT_MISC_CFG_INGRESSBW_IFG_MASK 0x0400
+#define RTL8365MB_PORT_MISC_CFG_RX_SPC_MASK 0x0200
+#define RTL8365MB_PORT_MISC_CFG_CRC_SKIP_MASK 0x0100
+#define RTL8365MB_PORT_MISC_CFG_PKTGEN_TX_FIRST_MASK 0x0080
+#define RTL8365MB_PORT_MISC_CFG_MAC_LOOPBACK_MASK 0x0040
+/* See &rtl8365mb_vlan_egress_mode */
+#define RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK 0x0030
+#define RTL8365MB_PORT_MISC_CFG_CONGESTION_SUSTAIN_TIME_MASK 0x000F
+
+/**
+ * enum rtl8365mb_vlan_egress_mode - port VLAN engress mode
+ * @RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL: follow untag mask in VLAN4k table entry
+ * @RTL8365MB_VLAN_EGRESS_MODE_KEEP: the VLAN tag format of egressed packets
+ * will remain the same as their ingressed format, but the priority and VID
+ * fields may be altered
+ * @RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG: always egress with priority tag
+ * @RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP: the VLAN tag format of egressed
+ * packets will remain the same as their ingressed format, and neither the
+ * priority nor VID fields can be altered
+ */
+enum rtl8365mb_vlan_egress_mode {
+ RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL = 0,
+ RTL8365MB_VLAN_EGRESS_MODE_KEEP = 1,
+ RTL8365MB_VLAN_EGRESS_MODE_PRI_TAG = 2,
+ RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP = 3,
+};
+
+/* VLAN control register */
+#define RTL8365MB_VLAN_CTRL_REG 0x07A8
+#define RTL8365MB_VLAN_CTRL_EN_MASK 0x0001
+
+/* VLAN ingress filter register */
+#define RTL8365MB_VLAN_INGRESS_REG 0x07A9
+#define RTL8365MB_VLAN_INGRESS_MASK GENMASK(10, 0)
+#define RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_OFFSET(_p) (_p)
+#define RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(_p) BIT(_p)
+
+/* VLAN "transparent" setting registers */
+#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE 0x09D0
+#define RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(_p) \
+ (RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG_BASE + (_p))
+
+/* Frame type filtering registers */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE 0x07aa
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
+ (RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
+/* required as FIELD_PREP cannot use non-constant masks */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
+ (0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
+ (((port) & 0x7) << 1)
+
/* MIB counter value registers */
#define RTL8365MB_MIB_COUNTER_BASE 0x1000
#define RTL8365MB_MIB_COUNTER_REG(_x) (RTL8365MB_MIB_COUNTER_BASE + (_x))
@@ -1196,6 +1258,183 @@ static void rtl8365mb_port_stp_state_set(struct dsa_switch *ds, int port,
val << RTL8365MB_MSTI_CTRL_PORT_STATE_OFFSET(port));
}
+static int rtl8365mb_port_set_transparent(struct realtek_priv *priv,
+ int igr_port, int egr_port,
+ bool enable)
+{
+ dev_dbg(priv->dev, "%s transparent VLAN from %d to %d\n",
+ enable ? "Enable" : "Disable", igr_port, egr_port);
+
+ /* "Transparent" between the two ports means that packets forwarded by
+ * igr_port and egressed on egr_port will not be filtered by the usual
+ * VLAN membership settings.
+ */
+ return regmap_update_bits(priv->map,
+ RTL8365MB_VLAN_EGRESS_TRANSPARENT_REG(egr_port),
+ BIT(igr_port), enable ? BIT(igr_port) : 0);
+}
+
+static int rtl8365mb_port_set_ingress_filtering(struct realtek_priv *priv,
+ int port, bool enable)
+{
+ /* Ingress filtering enabled: Discard VLAN-tagged frames if the port is
+ * not a member of the VLAN with which the packet is associated.
+ * Untagged packets will also be discarded unless the port has a PVID
+ * programmed. Priority-tagged frames are treated as untagged frames.
+ *
+ * Ingress filtering disabled: Accept all tagged and untagged frames.
+ */
+ return regmap_update_bits(priv->map, RTL8365MB_VLAN_INGRESS_REG,
+ RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port),
+ enable ?
+ RTL8365MB_VLAN_INGRESS_FILTER_PORT_EN_MASK(port) :
+ 0);
+}
+
+static int rtl8365mb_port_vlan_filtering(struct dsa_switch *ds, int port,
+ bool vlan_filtering,
+ struct netlink_ext_ack *extack)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ int ret;
+
+ dev_dbg(priv->dev, "port %d: %s VLAN filtering\n", port,
+ vlan_filtering ? "enable" : "disable");
+
+ /* (En|dis)able incoming packets filter, i.e. ignore VLAN membership */
+ dsa_switch_for_each_available_port(dp, ds) {
+ /* after considering port isolation, if not filtering
+ * allow forwarding from port to dp->index ignoring
+ * VLAN membership.
+ */
+ ret = rtl8365mb_port_set_transparent(priv, port, dp->index,
+ !vlan_filtering);
+ if (ret)
+ return ret;
+ }
+
+ /* If the port is not in the member set, the frame will be dropped */
+ return rtl8365mb_port_set_ingress_filtering(priv, port,
+ vlan_filtering);
+}
+
+static int rtl8365mb_port_vlan_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+ bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+ struct realtek_priv *priv = ds->priv;
+ int ret;
+
+ dev_dbg(priv->dev, "add VLAN %d on port %d, %s, %s\n",
+ vlan->vid, port, untagged ? "untagged" : "tagged",
+ pvid ? "PVID" : "no PVID");
+ /* add port to vlan4k. It knows nothing about PVID */
+ ret = rtl8365mb_vlan_4k_port_add(ds, port, vlan, extack);
+ if (ret)
+ return ret;
+
+ /* VlanMC knows nothing about untagged but it is required for PVID */
+ ret = rtl8365mb_vlan_mc_port_add(ds, port, vlan, extack);
+ if (ret)
+ goto undo_vlan_4k;
+
+ /* Set PVID if needed */
+ if (pvid) {
+ ret = rtl8365mb_vlan_pvid_port_add(ds, port, vlan, extack);
+ if (ret)
+ goto undo_vlan_mc;
+ }
+
+ return 0;
+
+undo_vlan_mc:
+ (void)rtl8365mb_vlan_mc_port_del(ds, port, vlan);
+
+undo_vlan_4k:
+ (void)rtl8365mb_vlan_4k_port_del(ds, port, vlan);
+ return ret;
+}
+
+static int rtl8365mb_port_vlan_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ bool untagged = !!(vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED);
+ bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+ struct realtek_priv *priv = ds->priv;
+ int ret1, ret2, ret3;
+
+ dev_dbg(priv->dev, "del VLAN %d on port %d, %s, %s\n",
+ vlan->vid, port, untagged ? "untagged" : "tagged",
+ pvid ? "PVID" : "no PVID");
+
+ ret1 = rtl8365mb_vlan_pvid_port_del(ds, port, vlan);
+ ret2 = rtl8365mb_vlan_mc_port_del(ds, port, vlan);
+ ret3 = rtl8365mb_vlan_4k_port_del(ds, port, vlan);
+
+ return ret1 ?: ret2 ?: ret3;
+}
+
+static int
+rtl8365mb_port_set_vlan_egress_mode(struct realtek_priv *priv, int port,
+ enum rtl8365mb_vlan_egress_mode mode)
+{
+ u32 val;
+
+ val = FIELD_PREP(RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, mode);
+ return regmap_update_bits(priv->map,
+ RTL8365MB_PORT_MISC_CFG_REG(port),
+ RTL8365MB_PORT_MISC_CFG_VLAN_EGRESS_MODE_MASK, val);
+}
+
+/* VLAN support is always enabled in the switch.
+ *
+ * All ports starts with vlan-unaware state, letting non-bridge port forward
+ * to CPU.
+ *
+ */
+static int rtl8365mb_vlan_setup(struct dsa_switch *ds)
+{
+ struct realtek_priv *priv = ds->priv;
+ enum rtl8365mb_vlan_egress_mode mode;
+ struct dsa_port *dp;
+ int ret;
+
+ dsa_switch_for_each_user_port(dp, ds) {
+ /* Disable vlan-filtering for all ports */
+ ret = rtl8365mb_port_vlan_filtering(ds, dp->index, false, NULL);
+ if (ret) {
+ dev_err(priv->dev,
+ "Failed to disable vlan filtering on port %d\n",
+ dp->index);
+ return ret;
+ }
+
+ /* The switch default is RTL8365MB_VLAN_EGRESS_MODE_REAL_KEEP,
+ * that forwards the packet as it was received. However,
+ * different untag settings will require the switch to update
+ * the tag.
+ */
+ mode = RTL8365MB_VLAN_EGRESS_MODE_ORIGINAL;
+ ret = rtl8365mb_port_set_vlan_egress_mode(priv, dp->index,
+ mode);
+ if (ret) {
+ dev_err(priv->dev,
+ "Failed to set port %d egress mode\n",
+ dp->index);
+ return ret;
+ }
+ }
+
+ /* VLAN is always enabled. */
+ ret = regmap_update_bits(priv->map, RTL8365MB_VLAN_CTRL_REG,
+ RTL8365MB_VLAN_CTRL_EN_MASK,
+ FIELD_PREP(RTL8365MB_VLAN_CTRL_EN_MASK, 1));
+ return ret;
+}
+
static int rtl8365mb_port_set_learning(struct realtek_priv *priv, int port,
bool enable)
{
@@ -2014,6 +2253,20 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
if (ret)
goto out_teardown_irq;
+ /* vlan config will only be effective for ports with vlan filtering */
+ ds->configure_vlan_while_not_filtering = true;
+ /* Set up VLAN */
+ ret = rtl8365mb_vlan_setup(ds);
+ if (ret)
+ goto out_teardown_irq;
+
+ /* Set maximum packet length to 1536 bytes */
+ ret = regmap_update_bits(priv->map, RTL8365MB_CFG0_MAX_LEN_REG,
+ RTL8365MB_CFG0_MAX_LEN_MASK,
+ FIELD_PREP(RTL8365MB_CFG0_MAX_LEN_MASK, 1536));
+ if (ret)
+ goto out_teardown_irq;
+
ret = rtl83xx_setup_user_mdio(ds);
if (ret) {
dev_err(priv->dev, "could not set up MDIO bus\n");
@@ -2124,6 +2377,9 @@ static const struct dsa_switch_ops rtl8365mb_switch_ops = {
.teardown = rtl8365mb_teardown,
.phylink_get_caps = rtl8365mb_phylink_get_caps,
.port_stp_state_set = rtl8365mb_port_stp_state_set,
+ .port_vlan_add = rtl8365mb_port_vlan_add,
+ .port_vlan_del = rtl8365mb_port_vlan_del,
+ .port_vlan_filtering = rtl8365mb_port_vlan_filtering,
.get_strings = rtl8365mb_get_strings,
.get_ethtool_stats = rtl8365mb_get_ethtool_stats,
.get_sset_count = rtl8365mb_get_sset_count,
diff --git a/drivers/net/dsa/realtek/rtl8365mb_vlan.c b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
new file mode 100644
index 000000000000..1ac36c06dcf7
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.c
@@ -0,0 +1,805 @@
+// SPDX-License-Identifier: GPL-2.0
+/* VLAN configuration interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ *
+ * VLAN configuration takes place in two separate domains of the switch: the
+ * VLAN4k table and the VLAN membership configuration (MC) database. While the
+ * VLAN4k table is exhaustive and can be fully populated with 4096 VLAN
+ * configurations, the same does not hold for the VLAN membership configuration
+ * database, which is limited to 32 entries.
+ *
+ * The switch will normally only use the VLAN4k table when making forwarding
+ * decisions. The VLAN membership configuration database is a vestigial ASIC
+ * design and is only used for a few specific features in the rtl8365mb
+ * family. This means that the limit of 32 entries should not hinder us in
+ * programming a huge number of VLANs into the switch.
+ *
+ * One necessary use of the VLAN membership configuration database is for the
+ * programming of a port-based VLAN ID (PVID). The PVID is programmed on a
+ * per-port basis via register field, which refers to a specific VLAN membership
+ * configuration via an index 0~31. In order to maintain coherent behaviour on a
+ * port with a PVID, it is necessary to keep the VLAN configuration synchronized
+ * between the VLAN4k table and the VLAN membership configuration database.
+ *
+ * Since VLAN membership configs are a scarce resource, it will only be used
+ * when strictly needed (i.e. a VLAN with members using PVID). Otherwise, the
+ * VLAN4k will be enough.
+ *
+ * With some exceptions, the entries in both the VLAN4k table and the VLAN
+ * membership configuration database offer the same configuration options. The
+ * differences are as follows:
+ *
+ * 1. VLAN4k entries can specify whether to use Independent or Shared VLAN
+ * Learning (IVL or SVL respectively). VLAN membership config entries
+ * cannot. This underscores the fact that VLAN membership configs are not
+ * involved in the learning process of the ASIC.
+ *
+ * 2. VLAN membership config entries use an "enhanced VLAN ID" (efid), which has
+ * a range 0~8191 compared with the standard 0~4095 range of the VLAN4k
+ * table. This underscores the fact that VLAN membership configs can be used
+ * to group ports on a layer beyond the standard VLAN configuration, which
+ * may be useful for ACL rules which specify alternative forwarding
+ * decisions.
+ *
+ * VLANMC index 0 is reserved as a neutral PVID, used for vlan-unaware ports.
+ *
+ */
+
+#include "rtl8365mb_vlan.h"
+#include "rtl8365mb_table.h"
+#include <linux/if_bridge.h>
+#include <linux/regmap.h>
+
+/* CVLAN (i.e. VLAN4k) table entry layout, u16[3] */
+#define RTL8365MB_CVLAN_ENTRY_SIZE 3 /* 48-bits */
+#define RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK GENMASK(7, 0)
+#define RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK GENMASK(15, 8)
+#define RTL8365MB_CVLAN_ENTRY_D1_FID_MASK GENMASK(3, 0)
+#define RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK GENMASK(4, 4)
+#define RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK GENMASK(7, 5)
+#define RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK GENMASK(8, 8)
+#define RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK GENMASK(13, 9)
+#define RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK GENMASK(14, 14)
+/* extends RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK GENMASK(2, 0)
+/* extends RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK GENMASK(5, 3)
+/* extends RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK */
+#define RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK GENMASK(6, 6)
+
+/* VLAN member configuration registers 0~31, u16[3] */
+#define RTL8365MB_VLAN_MC_BASE 0x0728
+#define RTL8365MB_VLAN_MC_ENTRY_SIZE 4 /* 64-bit */
+#define RTL8365MB_VLAN_MC_REG(index) \
+ (RTL8365MB_VLAN_MC_BASE + \
+ (RTL8365MB_VLAN_MC_ENTRY_SIZE * (index)))
+#define RTL8365MB_VLAN_MC_D0_MBR_MASK GENMASK(10, 0)
+#define RTL8365MB_VLAN_MC_D1_FID_MASK GENMASK(3, 0)
+
+#define RTL8365MB_VLAN_MC_D2_VBPEN_MASK GENMASK(0, 0)
+#define RTL8365MB_VLAN_MC_D2_VBPRI_MASK GENMASK(3, 1)
+#define RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK GENMASK(4, 4)
+#define RTL8365MB_VLAN_MC_D2_METERIDX_MASK GENMASK(10, 5)
+#define RTL8365MB_VLAN_MC_D3_EVID_MASK GENMASK(12, 0)
+
+/* Some limits for VLAN4k/VLAN membership config entries */
+#define RTL8365MB_PRIORITYMAX 7
+#define RTL8365MB_FIDMAX 15
+#define RTL8365MB_METERMAX 63
+#define RTL8365MB_VLAN_MCMAX 31
+
+/* RTL8367S supports 4k vlans (vid<=4095) and 32 enhanced vlans
+ * for VIDs up to 8191
+ */
+#define RTL8365MB_MAX_4K_VID 0x0FFF /* 4095 */
+#define RTL8365MB_MAX_MC_VID 0x1FFF /* 8191 */
+
+ /* Port-based VID registers 0~5 - each one holds an MC index for two ports */
+#define RTL8365MB_VLAN_PVID_CTRL_BASE 0x0700
+#define RTL8365MB_VLAN_PVID_CTRL_REG(_p) \
+ (RTL8365MB_VLAN_PVID_CTRL_BASE + ((_p) >> 1))
+#define RTL8365MB_VLAN_PVID_CTRL_PORT0_MCIDX_MASK 0x001F
+#define RTL8365MB_VLAN_PVID_CTRL_PORT1_MCIDX_MASK 0x1F00
+#define RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p) \
+ (((_p) & 1) << 3)
+#define RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(_p) \
+ (0x1F << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(_p))
+
+/* Frame type filtering registers */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE 0x07aa
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port) \
+ (RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_BASE + ((port) >> 3))
+/* required as FIELD_PREP cannot use non-constant masks */
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port) \
+ (0x3 << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port))
+#define RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port) \
+ (((port) & 0x7) << 1)
+
+/**
+ * struct rtl8365mb_vlan4k - VLAN4k table entry
+ * @vid: VLAN ID (0~4095)
+ * @member: port mask of ports in this VLAN
+ * @untag: port mask of ports which untag on egress
+ * @fid: filter ID - only used with SVL (unused)
+ * @priority: priority classification (unused)
+ * @priority_en: enable priority (unused)
+ * @policing_en: enable policing (unused)
+ * @ivl_en: enable IVL instead of default SVL
+ * @meteridx: metering index (unused)
+ *
+ * This structure is used to get/set entries in the VLAN4k table. The
+ * VLAN4k table dictates the VLAN configuration for the switch for the
+ * vast majority of features.
+ */
+struct rtl8365mb_vlan4k {
+ u16 vid;
+ u16 member;
+ u16 untag;
+ u8 fid : 4;
+ u8 priority : 3;
+ u8 priority_en : 1;
+ u8 policing_en : 1;
+ u8 ivl_en : 1;
+ u8 meteridx : 6;
+};
+
+/**
+ * struct rtl8365mb_vlanmc - VLAN membership config
+ * @evid: Enhanced VLAN ID (0~8191)
+ * @member: port mask of ports in this VLAN
+ * @fid: filter ID - only used with SVL (unused)
+ * @priority: priority classification (unused)
+ * @priority_en: enable priority (unused)
+ * @policing_en: enable policing (unused)
+ * @meteridx: metering index (unused)
+ *
+ * This structure is used to get/set entries in the VLAN membership
+ * configuration database. This feature is largely vestigial, but
+ * still needed for at least the following features:
+ * - PVID configuration
+ * - ACL configuration
+ * - selection of VLAN by the CPU tag when VSEL=1, although the switch
+ * can also select VLAN based on the VLAN tag if VSEL=0
+ *
+ * This is a low-level structure and it is recommended to interface with
+ * the VLAN membership config database via &struct rtl8365mb_vlanmc_entry.
+ */
+struct rtl8365mb_vlanmc {
+ u16 evid;
+ u16 member;
+ u8 fid : 4;
+ u8 priority : 3;
+ u8 priority_en : 1;
+ u8 policing_en : 1;
+ u8 meteridx : 6;
+};
+
+enum rtl8365mb_frame_ingress {
+ RTL8365MB_FRAME_TYPE_ANY_FRAME = 0,
+ RTL8365MB_FRAME_TYPE_TAGGED_ONLY,
+ RTL8365MB_FRAME_TYPE_UNTAGGED_ONLY,
+};
+
+static int rtl8365mb_vlan_4k_read(struct realtek_priv *priv, u16 vid,
+ struct rtl8365mb_vlan4k *vlan4k)
+{
+ u16 data[RTL8365MB_CVLAN_ENTRY_SIZE];
+ int ret;
+
+ ret = rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
+ RTL8365MB_TABLE_OP_READ, &vid, 0, 0,
+ data, ARRAY_SIZE(data));
+ if (ret)
+ return ret;
+
+ /* Unpack table entry */
+ memset(vlan4k, 0, sizeof(*vlan4k));
+ vlan4k->vid = vid;
+ vlan4k->member =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, data[0]) |
+ (FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK, data[2])
+ << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK));
+ vlan4k->untag =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, data[0]) |
+ (FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK, data[2])
+ << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK));
+ vlan4k->fid = FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, data[1]);
+ vlan4k->priority_en =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK, data[1]);
+ vlan4k->priority =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK, data[1]);
+ vlan4k->policing_en =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK, data[1]);
+ vlan4k->meteridx =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK, data[1]) |
+ (FIELD_GET(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK, data[2])
+ << FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK));
+ vlan4k->ivl_en =
+ FIELD_GET(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK, data[1]);
+
+ return 0;
+}
+
+static int rtl8365mb_vlan_4k_write(struct realtek_priv *priv,
+ const struct rtl8365mb_vlan4k *vlan4k)
+{
+ u16 data[RTL8365MB_CVLAN_ENTRY_SIZE] = { 0 };
+ u16 val;
+ u16 vid;
+
+ if (vlan4k->fid > RTL8365MB_FIDMAX ||
+ vlan4k->priority > RTL8365MB_PRIORITYMAX ||
+ vlan4k->meteridx > RTL8365MB_METERMAX)
+ return -EINVAL;
+
+ /* Pack table entry value */
+ data[0] |=
+ FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK, vlan4k->member);
+ data[0] |=
+ FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK, vlan4k->untag);
+
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_FID_MASK, vlan4k->fid);
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPEN_MASK,
+ vlan4k->priority_en);
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_VBPRI_MASK,
+ vlan4k->priority);
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_ENVLANPOL_MASK,
+ vlan4k->policing_en);
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK,
+ vlan4k->meteridx);
+ data[1] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D1_IVL_SVL_MASK,
+ vlan4k->ivl_en);
+
+ data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_MBR_EXT_MASK,
+ vlan4k->member >>
+ FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_MBR_MASK));
+ data[2] |= FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_UNTAG_EXT_MASK,
+ vlan4k->untag >>
+ FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D0_UNTAG_MASK));
+ val = FIELD_PREP(RTL8365MB_CVLAN_ENTRY_D2_METERIDX_EXT_MASK,
+ vlan4k->meteridx >>
+ FIELD_WIDTH(RTL8365MB_CVLAN_ENTRY_D1_METERIDX_MASK));
+ data[2] |= val;
+
+ vid = vlan4k->vid;
+ return rtl8365mb_table_query(priv, RTL8365MB_TABLE_CVLAN,
+ RTL8365MB_TABLE_OP_WRITE, &vid, 0, 0,
+ data, ARRAY_SIZE(data));
+}
+
+#define RTL_VLAN_ERR(msg) \
+ do { \
+ const char *__msg = (msg); \
+ \
+ if (extack) \
+ NL_SET_ERR_MSG_FMT_MOD(extack, "%s", __msg); \
+ dev_err(priv->dev, "%s", __msg); \
+ } while (0)
+
+static int
+rtl8365mb_vlan_4k_port_set(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack,
+ bool include)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct rtl8365mb_vlan4k vlan4k = {0};
+ int ret;
+
+ dev_dbg(priv->dev, "%s VLAN %d 4K on port %d\n",
+ include ? "add" : "del",
+ vlan->vid, port);
+
+ if (vlan->vid > RTL8365MB_MAX_4K_VID) {
+ RTL_VLAN_ERR("VLAN ID greater than "
+ __stringify(RTL8365MB_MAX_4K_VID));
+ return -EINVAL;
+ }
+
+ ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to read VLAN 4k table");
+ return ret;
+ }
+
+ if (include)
+ vlan4k.member |= BIT(port);
+ else
+ vlan4k.member &= ~BIT(port);
+
+ if (include && (vlan->flags & BRIDGE_VLAN_INFO_UNTAGGED))
+ vlan4k.untag |= BIT(port);
+ else
+ vlan4k.untag &= ~BIT(port);
+ vlan4k.ivl_en = true; /* always use Independent VLAN Learning */
+
+ ret = rtl8365mb_vlan_4k_write(priv, &vlan4k);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to write VLAN 4k table");
+ return ret;
+ }
+
+ return 0;
+}
+
+int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ return rtl8365mb_vlan_4k_port_set(ds, port, vlan, extack, true);
+}
+
+int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ return rtl8365mb_vlan_4k_port_set(ds, port, vlan, NULL, false);
+}
+
+/**
+ * rtl8365mb_vlan_mc_find() - find VLANMC index by VID or the first free index
+ *
+ * @priv: realtek_priv pointer
+ * @vid: VLAN ID
+ * @index: found index
+ * @first_free: found free index
+ *
+ * If a VLAN MC entry using @vid was found, @index will return the matched index
+ * and @first_free is undefined. If not found, @index will return 0 and
+ * @first_free will return the first found free index in VLAN MC or 0 if the
+ * table is full.
+ *
+ * Although 0 is a valid VLAN MC index, it is reserved for ports without PVID,
+ * including standalone, non-member ports.
+ *
+ * Both @index and @first_free will be in the * 1..@RTL8365MB_VLAN_MCMAX range.
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ *
+ */
+static int rtl8365mb_vlan_mc_find(struct realtek_priv *priv, u16 vid,
+ u8 *index, u8 *first_free)
+{
+ u32 vlan_entry_d3;
+ u8 vlanmc_idx;
+ u16 evid;
+ int ret;
+
+ if (!index)
+ return -EINVAL;
+ if (!first_free)
+ return -EINVAL;
+ if (!vid)
+ return -EINVAL;
+
+ *index = 0;
+ *first_free = 0;
+
+ /* look for existing entry or an empty one */
+ /* vlanmc index 0 is reserved as a neutral PVID value.
+ * Non-PVID ports can still reach the CPU via VLAN
+ * transparent mode.
+ **/
+ for (vlanmc_idx = 1; vlanmc_idx <= RTL8365MB_VLAN_MCMAX; vlanmc_idx++) {
+ /* just read the 4th word, where the evid is */
+ ret = regmap_read(priv->map,
+ RTL8365MB_VLAN_MC_REG(vlanmc_idx) + 3,
+ &vlan_entry_d3);
+ if (ret)
+ return ret;
+
+ evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlan_entry_d3);
+
+ if (evid == vid) {
+ *index = vlanmc_idx;
+ return 0;
+ }
+
+ if (evid == 0x0 && *first_free < 1)
+ *first_free = vlanmc_idx;
+ }
+ return 0;
+}
+
+static int rtl8365mb_vlan_port_set_pvid(struct realtek_priv *priv,
+ int port, u16 vlanmc_idx)
+{
+ int ret;
+ u32 val;
+
+ val = vlanmc_idx << RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
+ ret = regmap_update_bits(priv->map,
+ RTL8365MB_VLAN_PVID_CTRL_REG(port),
+ RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port),
+ val);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int rtl8365mb_vlan_port_get_pvid(struct realtek_priv *priv,
+ int port, u8 *vlanmc_idx)
+{
+ u32 data;
+ int ret;
+
+ ret = regmap_read(priv->map, RTL8365MB_VLAN_PVID_CTRL_REG(port), &data);
+ if (ret)
+ return ret;
+
+ *vlanmc_idx = (data & RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_MASK(port))
+ >> RTL8365MB_VLAN_PVID_CTRL_PORT_MCIDX_OFFSET(port);
+
+ return 0;
+}
+
+/**
+ * rtl8365mb_vlan_mc_pvid_members() - Get a bitmap of vlan PVID members
+ *
+ * @ds: DSA switch
+ * @vlanmc_idx: the index of a VLAN in VLAN MC table
+ * @members: the returned bitmap of members that have PVID status
+ *
+ * This function iterates over DSA ports and creates a bitmap representation of
+ * those ports that have PVID pointing to this VLAN (identified by its table
+ * index and not VID). If you need to get the table index from VID, see
+ * rtl8365mb_vlan_mc_find()
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ **/
+static int rtl8365mb_vlan_mc_pvid_members(struct dsa_switch *ds,
+ u8 vlanmc_idx, u16 *members)
+{
+ struct realtek_priv *priv = ds->priv;
+ struct dsa_port *dp;
+ u8 _vlanmc_idx;
+ int ret;
+
+ if (!members)
+ return -EINVAL;
+
+ *members = 0;
+
+ dsa_switch_for_each_port(dp, ds) {
+ ret = rtl8365mb_vlan_port_get_pvid(priv, dp->index,
+ &_vlanmc_idx);
+ if (ret)
+ return ret;
+
+ if (_vlanmc_idx == vlanmc_idx)
+ *members |= BIT(dp->index);
+ }
+
+ return 0;
+}
+
+static int
+rtl8365mb_vlan_port_set_framefilter(struct realtek_priv *priv,
+ int port,
+ enum rtl8365mb_frame_ingress accepted_frame)
+{
+ /* Even if ACCEPT_FRAME_TYPE_ANY, the switch will still check if the
+ * port is a member of vlan PVID
+ */
+ accepted_frame = accepted_frame
+ << RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_OFFSET(port);
+
+ return regmap_update_bits(priv->map,
+ RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_REG(port),
+ RTL8365MB_VLAN_ACCEPT_FRAME_TYPE_MASK(port),
+ accepted_frame);
+}
+
+static int rtl8365mb_vlan_mc_read(struct realtek_priv *priv, u32 index,
+ struct rtl8365mb_vlanmc *vlanmc)
+{
+ u16 data[RTL8365MB_VLAN_MC_ENTRY_SIZE];
+ int ret;
+
+ if (index > RTL8365MB_VLAN_MCMAX)
+ return -EINVAL;
+
+ ret = regmap_bulk_read(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+ RTL8365MB_VLAN_MC_ENTRY_SIZE);
+ if (ret)
+ return ret;
+
+ vlanmc->member = FIELD_GET(RTL8365MB_VLAN_MC_D0_MBR_MASK, data[0]);
+ vlanmc->fid = FIELD_GET(RTL8365MB_VLAN_MC_D1_FID_MASK, data[1]);
+ vlanmc->priority = FIELD_GET(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, data[2]);
+ vlanmc->evid = FIELD_GET(RTL8365MB_VLAN_MC_D3_EVID_MASK, data[3]);
+
+ return 0;
+}
+
+int rtl8365mb_vlan_pvid_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+ enum rtl8365mb_frame_ingress accepted_frame;
+ struct realtek_priv *priv = ds->priv;
+ u8 _unused_first_free_idx;
+ u8 vlanmc_idx;
+ int ret;
+
+ if (!pvid)
+ return 0;
+
+ /* look for existing entry */
+ ret = rtl8365mb_vlan_mc_find(priv, vlan->vid, &vlanmc_idx,
+ &_unused_first_free_idx);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to find a VLAN MC table index");
+ return ret;
+ }
+
+ if (!vlanmc_idx) {
+ RTL_VLAN_ERR("VLAN should already exist in VLAN MC");
+ return ret;
+ }
+
+ ret = rtl8365mb_vlan_port_set_pvid(priv, port, vlanmc_idx);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to set port PVID");
+ return ret;
+ }
+
+ /* Changing accept frame is what enables PVID (if not enabled before) */
+ accepted_frame = RTL8365MB_FRAME_TYPE_ANY_FRAME;
+ ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to set port frame filter");
+ return ret;
+ }
+
+ return 0;
+}
+
+int rtl8365mb_vlan_pvid_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ enum rtl8365mb_frame_ingress accepted_frame;
+ struct netlink_ext_ack *extack = NULL;
+ struct realtek_priv *priv = ds->priv;
+ struct rtl8365mb_vlanmc vlanmc = {0};
+ u8 vlanmc_idx;
+ int ret;
+
+ ret = rtl8365mb_vlan_port_get_pvid(priv, port, &vlanmc_idx);
+ if (ret)
+ return ret;
+
+ /* Port is not using PVID. Nothing to remove. */
+ if (!vlanmc_idx)
+ return 0;
+
+ ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to read VLAN MC table");
+ return ret;
+ }
+
+ /* We are leaving a non PVID vlan, Nothing to remove. */
+ if (vlanmc.evid != vlan->vid)
+ return 0;
+
+ /* Changing accept frame is what really removes PVID */
+ accepted_frame = RTL8365MB_FRAME_TYPE_TAGGED_ONLY;
+ ret = rtl8365mb_vlan_port_set_framefilter(priv, port, accepted_frame);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to set port frame filter");
+ return ret;
+ }
+
+ ret = rtl8365mb_vlan_port_set_pvid(priv, port, 0);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to set port PVID to 0");
+ return ret;
+ }
+
+ return 0;
+}
+
+static int rtl8365mb_vlan_mc_write(struct realtek_priv *priv, u32 index,
+ const struct rtl8365mb_vlanmc *vlanmc)
+{
+ u16 data[4] = { 0 };
+ int ret;
+
+ if (index > RTL8365MB_VLAN_MCMAX ||
+ vlanmc->fid > RTL8365MB_FIDMAX ||
+ vlanmc->priority > RTL8365MB_PRIORITYMAX ||
+ vlanmc->meteridx > RTL8365MB_METERMAX)
+ return -EINVAL;
+
+ data[0] |= FIELD_PREP(RTL8365MB_VLAN_MC_D0_MBR_MASK, vlanmc->member);
+ data[1] |= FIELD_PREP(RTL8365MB_VLAN_MC_D1_FID_MASK, vlanmc->fid);
+ data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_METERIDX_MASK,
+ vlanmc->meteridx);
+ data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_ENVLANPOL_MASK,
+ vlanmc->policing_en);
+ data[2] |=
+ FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPRI_MASK, vlanmc->priority);
+ data[2] |= FIELD_PREP(RTL8365MB_VLAN_MC_D2_VBPEN_MASK,
+ vlanmc->priority_en);
+ data[3] |= FIELD_PREP(RTL8365MB_VLAN_MC_D3_EVID_MASK, vlanmc->evid);
+
+ ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+ RTL8365MB_VLAN_MC_ENTRY_SIZE);
+
+ return ret;
+}
+
+static int rtl8365mb_vlan_mc_erase(struct realtek_priv *priv, u32 index)
+{
+ u16 data[4] = { 0 };
+ int ret;
+
+ if (index > RTL8365MB_VLAN_MCMAX)
+ return -EINVAL;
+
+ ret = regmap_bulk_write(priv->map, RTL8365MB_VLAN_MC_REG(index), &data,
+ RTL8365MB_VLAN_MC_ENTRY_SIZE);
+
+ return ret;
+}
+
+/** rtl8365mb_vlanmc_port_set() - include or exclude a port from vlanMC
+ * @ds: dsa switch
+ * @port: the port number
+ * @vlan: the vlan to include/exclude @port
+ * @extack: optional extack to return errors
+ * @include: whether to include or exclude @port
+ *
+ * This function is used to include/exclude ports to the vlanMC table.
+ *
+ * VlanMC stands for VLAN membership config and it is used exclusively for
+ * PVID. If @vlan members are not using PVID, this function will either
+ * remove or not create a new vlanMC entry.
+ *
+ * vlanMC members are kept in sync with vlan4k, although the switch only
+ * checks membership in vlan4k table.
+ *
+ * Port PVID and accepted frame type are updated as well.
+ *
+ * Return: Returns 0 on success, a negative error on failure.
+ *
+ */
+static
+int rtl8365mb_vlan_mc_port_set(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack,
+ bool include)
+{
+ bool pvid = !!(vlan->flags & BRIDGE_VLAN_INFO_PVID);
+ struct realtek_priv *priv = ds->priv;
+ struct rtl8365mb_vlan4k vlan4k = {0};
+ struct rtl8365mb_vlanmc vlanmc = {0};
+ u16 pvid_members = 0;
+ u8 first_unused = 0;
+ u8 vlanmc_idx = 0;
+ int ret;
+
+ dev_dbg(priv->dev, "%s VLAN %d MC on port %d\n",
+ include ? "add" : "del",
+ vlan->vid, port);
+
+ if (vlan->vid > RTL8365MB_MAX_MC_VID) {
+ RTL_VLAN_ERR("VLAN ID greater than "
+ __stringify(RTL8365MB_MAX_MC_VID));
+ return -EINVAL;
+ }
+
+ /* look for existing entry or an empty slot */
+ ret = rtl8365mb_vlan_mc_find(priv, vlan->vid, &vlanmc_idx,
+ &first_unused);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to find a VLAN MC table index");
+ return ret;
+ }
+
+ if (vlanmc_idx) {
+ ret = rtl8365mb_vlan_mc_read(priv, vlanmc_idx, &vlanmc);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to read VLAN MC table");
+ return ret;
+ }
+ } else if (include) {
+ /* for now, vlan_mc is only required for PVID. Defer allocation
+ * until at least one port uses PVID.
+ */
+ if (!pvid) {
+ dev_dbg(priv->dev,
+ "Not creating VlanMC for vlan %d until a port uses PVID (%d does not)\n",
+ vlan->vid, port);
+ return 0;
+ }
+
+ if (!first_unused) {
+ RTL_VLAN_ERR("All VLAN MC entries ("
+ __stringify(RTL8365MB_VLAN_MCMAX + 1)
+ ") are in use.");
+ return -E2BIG;
+ }
+
+ /* Retrieve vlan4k members as we might have deferred VlanMC
+ * before.
+ */
+ if (vlan->vid <= RTL8365MB_MAX_4K_VID) {
+ ret = rtl8365mb_vlan_4k_read(priv, vlan->vid, &vlan4k);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to read VLAN 4k table");
+ return ret;
+ }
+ }
+
+ vlanmc_idx = first_unused;
+ vlanmc.evid = vlan->vid;
+
+ /* for new vlan_mc, sync current vlan4k members,
+ * although only vlan4k members matter.
+ */
+ vlanmc.member |= vlan4k.member;
+ } else /* excluding and VLANMC not found */ {
+ return 0;
+ }
+
+ ret = rtl8365mb_vlan_mc_pvid_members(ds, vlanmc_idx,
+ &pvid_members);
+ if (ret) {
+ RTL_VLAN_ERR("Failed to read VLANMC PVID members");
+ return ret;
+ }
+ dev_dbg(priv->dev,
+ "VLAN %d (idx: %d) PVID curr members: %08x\n",
+ vlan->vid, vlanmc_idx, pvid_members);
+
+ /* here we either have an existing VLANMC (with PVID members) or the
+ * added port is using this VLAN as PVID
+ */
+ if (include) {
+ vlanmc.member |= BIT(port);
+ if (pvid)
+ pvid_members |= BIT(port);
+ } else {
+ vlanmc.member &= ~BIT(port);
+ pvid_members &= ~BIT(port);
+ }
+
+ /* just like we don't need to create a VLAN_MC when there is no port
+ * using it as PVID, we can erase it when there is no more port using
+ * it as PVID.
+ */
+ if (!pvid_members) {
+ dev_dbg(priv->dev,
+ "Clearing VlanMC index %d previously used by VID %d\n",
+ vlanmc_idx, vlan->vid);
+ ret = rtl8365mb_vlan_mc_erase(priv, vlanmc_idx);
+ } else {
+ dev_dbg(priv->dev,
+ "Saving VlanMC index %d with VID %d\n",
+ vlanmc_idx, vlan->vid);
+ ret = rtl8365mb_vlan_mc_write(priv, vlanmc_idx, &vlanmc);
+ }
+ if (ret) {
+ RTL_VLAN_ERR("Failed to write vlan MC entry");
+ return ret;
+ }
+
+ return 0;
+}
+
+int rtl8365mb_vlan_mc_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack)
+{
+ return rtl8365mb_vlan_mc_port_set(ds, port, vlan, extack, true);
+}
+
+int rtl8365mb_vlan_mc_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan)
+{
+ return rtl8365mb_vlan_mc_port_set(ds, port, vlan, NULL, false);
+}
diff --git a/drivers/net/dsa/realtek/rtl8365mb_vlan.h b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
new file mode 100644
index 000000000000..bd0cf4c804c8
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_vlan.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* VLAN configuration interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ *
+ */
+
+#ifndef _REALTEK_RTL8365MB_VLAN_H
+#define _REALTEK_RTL8365MB_VLAN_H
+
+#include <linux/types.h>
+
+#include "realtek.h"
+
+int rtl8365mb_vlan_4k_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_4k_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan);
+int rtl8365mb_vlan_mc_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_mc_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan);
+int rtl8365mb_vlan_pvid_port_add(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan,
+ struct netlink_ext_ack *extack);
+int rtl8365mb_vlan_pvid_port_del(struct dsa_switch *ds, int port,
+ const struct switchdev_obj_port_vlan *vlan);
+#endif /* _REALTEK_RTL8365MB_VLAN_H */
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 05/10] net: dsa: realtek: rtl8365mb: add table lookup interface
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Add a generic table lookup interface to centralize access to
the RTL8365MB internal tables.
This interface abstracts the low-level table access logic and
will be used by subsequent commits to implement FDB and VLAN
operations.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
drivers/net/dsa/realtek/Makefile | 1 +
drivers/net/dsa/realtek/rtl8365mb_table.c | 255 ++++++++++++++++++++++++++++++
drivers/net/dsa/realtek/rtl8365mb_table.h | 133 ++++++++++++++++
3 files changed, 389 insertions(+)
diff --git a/drivers/net/dsa/realtek/Makefile b/drivers/net/dsa/realtek/Makefile
index 3f986e04912f..99654c4c5a3d 100644
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -17,3 +17,4 @@ rtl8366-objs += rtl8366rb-leds.o
endif
obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
rtl8365mb-objs := rtl8365mb_main.o \
+ rtl8365mb_table.o \
diff --git a/drivers/net/dsa/realtek/rtl8365mb_table.c b/drivers/net/dsa/realtek/rtl8365mb_table.c
new file mode 100644
index 000000000000..e706ea2ccb85
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.c
@@ -0,0 +1,255 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#include "rtl8365mb_table.h"
+#include <linux/regmap.h>
+
+/* Table access control register */
+#define RTL8365MB_TABLE_CTRL_REG 0x0500
+/* Should be one of rtl8365mb_table enum members */
+#define RTL8365MB_TABLE_CTRL_TABLE_MASK GENMASK(2, 0)
+/* Should be one of rtl8365mb_table_op enum members */
+#define RTL8365MB_TABLE_CTRL_OP_MASK GENMASK(3, 3)
+/* Should be one of rtl8365mb_table_l2_method enum members */
+#define RTL8365MB_TABLE_CTRL_METHOD_MASK GENMASK(7, 4)
+/* NOTE: PORT_MASK is only 4 bit, which suggests that port-based
+ * look-up of the L2 table only works for physical port addresses
+ * 0~4. It could be that the Realtek driver is out-of-date and
+ * actually the mask is something like 0xFF00, but this is
+ * unconfirmed.
+ */
+#define RTL8365MB_TABLE_CTRL_PORT_MASK GENMASK(11, 8)
+
+/* Table access address register */
+#define RTL8365MB_TABLE_ACCESS_ADDR_REG 0x0501
+#define RTL8365MB_TABLE_ADDR_MASK GENMASK(13, 0)
+
+/* Table status register */
+#define RTL8365MB_TABLE_STATUS_REG 0x0502
+#define RTL8365MB_TABLE_STATUS_ADDRESS_MASK GENMASK(10, 0)
+/* set for L3, unset for L2 */
+#define RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK GENMASK(11, 11)
+#define RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK GENMASK(12, 12)
+#define RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK GENMASK(13, 13)
+#define RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK GENMASK(14, 14)
+
+/* Table read/write registers */
+#define RTL8365MB_TABLE_WRITE_BASE 0x0510
+#define RTL8365MB_TABLE_WRITE_REG(_x) \
+ (RTL8365MB_TABLE_WRITE_BASE + (_x))
+#define RTL8365MB_TABLE_READ_BASE 0x0520
+#define RTL8365MB_TABLE_READ_REG(_x) \
+ (RTL8365MB_TABLE_READ_BASE + (_x))
+#define RTL8365MB_TABLE_ENTRY_MAX_SIZE 10
+#define RTL8365MB_TABLE_10TH_DATA_MASK GENMASK(3, 0)
+#define RTL8365MB_TABLE_WRITE_10TH_REG \
+ RTL8365MB_TABLE_WRITE_REG(RTL8365MB_TABLE_ENTRY_MAX_SIZE - 1)
+
+static int rtl8365mb_table_poll_busy(struct realtek_priv *priv)
+{
+ u32 val;
+
+ return regmap_read_poll_timeout(priv->map_nolock,
+ RTL8365MB_TABLE_STATUS_REG, val,
+ !FIELD_GET(RTL8365MB_TABLE_STATUS_BUSY_FLAG_MASK, val),
+ 10, 100);
+}
+
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size)
+{
+ bool addr_as_input = true;
+ bool write_data = false;
+ int ret = 0;
+ u32 cmd;
+ u32 val;
+ u32 hit;
+
+ if (!addr) {
+ dev_err(priv->dev, "%s: addr is NULL\n", __func__);
+ return -EINVAL;
+ }
+
+ if (!data) {
+ dev_err(priv->dev, "%s: data is NULL\n", __func__);
+ return -EINVAL;
+ }
+
+ if (size > RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ dev_err(priv->dev, "%s: size too big: %zu\n", __func__, size);
+ return -E2BIG;
+ }
+
+ if (size == 0) {
+ dev_err(priv->dev, "%s: size is 0\n", __func__);
+ return -EINVAL;
+ }
+
+ if (!FIELD_FIT(RTL8365MB_TABLE_CTRL_TABLE_MASK, table)) {
+ dev_err(priv->dev, "%s: table %d does not fit in MASK\n",
+ __func__, table);
+ return -EINVAL;
+ }
+
+ /* Prepare target table and operation (read or write) */
+ cmd = 0;
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_TABLE_MASK, table);
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_OP_MASK, op);
+ if (op == RTL8365MB_TABLE_OP_READ && table == RTL8365MB_TABLE_L2) {
+ if (!FIELD_FIT(RTL8365MB_TABLE_CTRL_METHOD_MASK, method))
+ return -EINVAL;
+
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_METHOD_MASK, method);
+ switch (method) {
+ case RTL8365MB_TABLE_L2_METHOD_MAC:
+ /*
+ * Method MAC requires as input the same L2 table format
+ * you'll get as result. However, it might only use mac
+ * address and FID/VID fields.
+ */
+ write_data = true;
+
+ /* METHOD_MAC does not use addr as input, but may return
+ * the matched index.
+ */
+ addr_as_input = false;
+
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC:
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC:
+ break;
+ case RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT:
+ if (!FIELD_FIT(RTL8365MB_TABLE_CTRL_PORT_MASK, port))
+ return -EINVAL;
+
+ cmd |= FIELD_PREP(RTL8365MB_TABLE_CTRL_PORT_MASK, port);
+ break;
+ default:
+ return -EINVAL;
+ }
+ } else if (op == RTL8365MB_TABLE_OP_WRITE) {
+ write_data = true;
+
+ /* Writing to L2 does not use addr as input, as the table index
+ * is derived from key fields.
+ */
+ if (table == RTL8365MB_TABLE_L2)
+ addr_as_input = false;
+ }
+
+ /* Validate addr only when used as an input */
+ if (addr_as_input) {
+ if (!FIELD_FIT(RTL8365MB_TABLE_ADDR_MASK, *addr)) {
+ dev_err(priv->dev, "%s: addr %u does not fit in MASK\n",
+ __func__, *addr);
+ return -EINVAL;
+ }
+ }
+
+ /* To prevent concurrent access to the look-up tables, take the regmap
+ * lock manually and access via the map_nolock regmap.
+ */
+ mutex_lock(&priv->map_lock);
+
+ /* Write entry data if writing to the table (or L2_METHOD_MAC) */
+ if (write_data) {
+ /* bulk write data up to 9th byte */
+ ret = regmap_bulk_write(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_BASE,
+ data,
+ min_t(size_t, size,
+ RTL8365MB_TABLE_ENTRY_MAX_SIZE -
+ 1));
+ if (ret)
+ goto out;
+
+ /* 10th register uses only 4 less significant bits */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_PREP(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ ret = regmap_update_bits(priv->map_nolock,
+ RTL8365MB_TABLE_WRITE_10TH_REG,
+ RTL8365MB_TABLE_10TH_DATA_MASK,
+ val);
+ }
+
+ if (ret)
+ goto out;
+ }
+
+ /* Write address (if needed) */
+ if (addr_as_input) {
+ ret = regmap_write(priv->map_nolock,
+ RTL8365MB_TABLE_ACCESS_ADDR_REG,
+ FIELD_PREP(RTL8365MB_TABLE_ADDR_MASK,
+ *addr));
+ if (ret)
+ goto out;
+ }
+
+ /* Execute */
+ ret = regmap_write(priv->map_nolock, RTL8365MB_TABLE_CTRL_REG, cmd);
+ if (ret)
+ goto out;
+
+ /* Poll for completion */
+ ret = rtl8365mb_table_poll_busy(priv);
+ if (ret)
+ goto out;
+
+ /* For both reads and writes to the L2 table, check status */
+ if (table == RTL8365MB_TABLE_L2) {
+ ret = regmap_read(priv->map_nolock, RTL8365MB_TABLE_STATUS_REG,
+ &val);
+ if (ret)
+ goto out;
+
+ /* Did the query find an entry? */
+ hit = FIELD_GET(RTL8365MB_TABLE_STATUS_HIT_STATUS_MASK, val);
+ if (!hit) {
+ ret = -ENOENT;
+ goto out;
+ }
+
+ /* If so, extract the address */
+ *addr = 0;
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_MASK, val);
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDRESS_EXT_MASK, val)
+ << 11;
+ /* only set if it is a L3 address */
+ *addr |= FIELD_GET(RTL8365MB_TABLE_STATUS_ADDR_TYPE_MASK, val)
+ << 12;
+ }
+
+ /* Finally, get the table entry if we were reading */
+ if (op == RTL8365MB_TABLE_OP_READ) {
+ ret = regmap_bulk_read(priv->map_nolock,
+ RTL8365MB_TABLE_READ_BASE,
+ data, size);
+
+ /* For the biggest table entries, the uppermost table
+ * entry register has space for only one nibble. Mask
+ * out the remainder bits. Empirically I saw nothing
+ * wrong with omitting this mask, but it may prevent
+ * unwanted behaviour. FYI.
+ */
+ if (size == RTL8365MB_TABLE_ENTRY_MAX_SIZE) {
+ val = FIELD_GET(RTL8365MB_TABLE_10TH_DATA_MASK,
+ data[size - 1]);
+ data[size - 1] = val;
+ }
+ }
+
+out:
+ mutex_unlock(&priv->map_lock);
+
+ return ret;
+}
diff --git a/drivers/net/dsa/realtek/rtl8365mb_table.h b/drivers/net/dsa/realtek/rtl8365mb_table.h
new file mode 100644
index 000000000000..0b1a89bd81f1
--- /dev/null
+++ b/drivers/net/dsa/realtek/rtl8365mb_table.h
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Look-up table query interface for the rtl8365mb switch family
+ *
+ * Copyright (C) 2022 Alvin Šipraga <alsi@bang-olufsen.dk>
+ */
+
+#ifndef _REALTEK_RTL8365MB_TABLE_H
+#define _REALTEK_RTL8365MB_TABLE_H
+
+#include <linux/if_ether.h>
+#include <linux/types.h>
+
+#include "realtek.h"
+
+/**
+ * struct rtl8365mb_table - available switch tables
+ *
+ * @RTL8365MB_TABLE_ACL_RULE - ACL rules
+ * @RTL8365MB_TABLE_ACL_ACTION - ACL actions
+ * @RTL8365MB_TABLE_CVLAN - VLAN4k configurations
+ * @RTL8365MB_TABLE_L2 - filtering database (2K hash table)
+ * @RTL8365MB_TABLE_IGMP_GROUP - IGMP group database (readonly)
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_TABLE_MASK.
+ */
+enum rtl8365mb_table {
+ RTL8365MB_TABLE_ACL_RULE = 1,
+ RTL8365MB_TABLE_ACL_ACTION = 2,
+ RTL8365MB_TABLE_CVLAN = 3,
+ RTL8365MB_TABLE_L2 = 4,
+ RTL8365MB_TABLE_IGMP_GROUP = 5,
+};
+
+/**
+ * enum rtl8365mb_table_op - table query operation
+ *
+ * @RTL8365MB_TABLE_OP_READ: read an entry from the target table
+ * @RTL8365MB_TABLE_OP_WRITE: write an entry to the target table
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_OP_MASK.
+ */
+enum rtl8365mb_table_op {
+ RTL8365MB_TABLE_OP_READ = 0,
+ RTL8365MB_TABLE_OP_WRITE = 1,
+};
+
+/**
+ * enum rtl8365mb_table_l2_method - look-up method for read queries of L2 table
+ *
+ * @RTL8365MB_TABLE_L2_METHOD_MAC: look-up by source MAC address and FID (or
+ * VID)
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR: look-up by entry address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT: look-up next entry starting from the
+ * supplied address
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC: same as ADDR_NEXT but search only
+ * unicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC: same as ADDR_NEXT but search only
+ * multicast addresses
+ * @RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT: same as ADDR_NEXT_UC but
+ * search only entries with matching source port
+ *
+ * NOTE: Don't change the enum values. They must concur with the field
+ * described by @RTL8365MB_TABLE_CTRL_METHOD_MASK
+ */
+enum rtl8365mb_table_l2_method {
+ RTL8365MB_TABLE_L2_METHOD_MAC = 0,
+ RTL8365MB_TABLE_L2_METHOD_ADDR = 1,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT = 2,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC = 3,
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC = 4,
+ /*
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L3 = 5,
+ * RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_MC_L2L3 = 6,
+ */
+ RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT = 7,
+};
+
+/**
+ * rtl8365mb_table_query() - read from or write to a switch table
+ * @priv: driver context
+ * @table: target table, see &enum rtl8365mb_table
+ * @op: read or write operation, see &enum rtl8365mb_table_op
+ * @addr: table address. For indexed tables, this selects the entry to access.
+ * For L2 read queries, it is ignored as input for MAC-based lookup
+ * methods and used as input for address-based lookup methods. On
+ * successful L2 queries, it is updated with the matched entry address.
+ * @method: L2 table lookup method, see &enum rtl8365mb_table_l2_method.
+ * Ignored for non-L2 tables.
+ * @port: for L2 read queries using method
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, restrict the search
+ * to entries associated with this source port. Ignored otherwise.
+ * @data: data buffer used to read from or write to the table. For L2 MAC
+ * lookups, this buffer provides the lookup key and receives the
+ * matched entry contents on success.
+ * @size: size of @data in 16-bit words
+ *
+ * This function provides unified access to the internal tables of the switch.
+ * All tables except the L2 table are simple indexed tables, where @addr
+ * selects the entry and @op determines whether the access is a read or a
+ * write operation.
+ *
+ * The L2 table is a hash table and supports multiple lookup methods. For
+ * %RTL8365MB_TABLE_L2_METHOD_MAC, an entry is searched based on the MAC
+ * address and FID/VID fields provided in @data, using the same format as
+ * an L2 table entry. Address-based methods either read a specific entry
+ * (%RTL8365MB_TABLE_L2_METHOD_ADDR) or iterate over valid entries starting
+ * from @addr (%RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT and variants). When using
+ * %RTL8365MB_TABLE_L2_METHOD_ADDR_NEXT_UC_PORT, only entries associated with
+ * the specified @port are considered.
+ *
+ * On successful L2 lookups, @addr is updated with the matched table address
+ * and @data contains the corresponding table entry. If no matching entry
+ * is found, -ENOENT is returned.
+ *
+ * The contents of @data are used as input when writing to tables or when
+ * specifying the lookup key for L2 MAC searches, and as output for all
+ * successful read operations. If an error occurs, the contents of @addr
+ * and @data are undefined.
+ *
+ * @size must match the size of the target table entry, expressed in 16-bit
+ * words. This function only validates that it is non-zero and fits in the
+ * available register space.
+ *
+ */
+int rtl8365mb_table_query(struct realtek_priv *priv,
+ enum rtl8365mb_table table,
+ enum rtl8365mb_table_op op, u16 *addr,
+ enum rtl8365mb_table_l2_method method,
+ u16 port, u16 *data, size_t size);
+
+#endif /* _REALTEK_RTL8365MB_TABLE_H */
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 04/10] bitfield.h: add FIELD_WIDTH()
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
Some hardware stores logical fields split across multiple registers,
requiring partial values to be reassembled using shifts and masks.
Add FIELD_WIDTH(), a helper that returns the number of contiguous bits
covered by a bitfield mask. This allows callers to determine how much a
partial value needs to be shifted when reconstructing a field.
This complements FIELD_MAX() and FIELD_FIT(), and avoids reimplementing
bit counting logic in drivers.
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
include/linux/bitfield.h | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/include/linux/bitfield.h b/include/linux/bitfield.h
index 54aeeef1f0ec..8d6c1c309c3b 100644
--- a/include/linux/bitfield.h
+++ b/include/linux/bitfield.h
@@ -111,6 +111,19 @@
(typeof(_mask))((_mask) >> __bf_shf(_mask)); \
})
+/**
+ * FIELD_WIDTH() - return the width of a bitfield
+ * @_mask: shifted mask defining the field's length and position
+ *
+ * Returns the number of contiguous bits covered by @_mask.
+ * This corresponds to the bit width of FIELD_MAX(@_mask).
+ */
+#define FIELD_WIDTH(_mask) \
+ ({ \
+ __BF_FIELD_CHECK(_mask, 0ULL, 0ULL, "FIELD_WIDTH: "); \
+ __bf_shf(~FIELD_MAX(_mask)); \
+ })
+
/**
* FIELD_FIT() - check if value fits in the field
* @_mask: shifted mask defining the field's length and position
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 03/10] net: dsa: realtek: rtl8365mb: prepare for multiple source files
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Rename rtl8365mb.c to rtl8365mb_main.c in preparation for subsequent
commits which add additional source files to the driver.
The trailing backslash in the Makefile is deliberate. It allows for new
files to be added without clobbering git history.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
drivers/net/dsa/realtek/Makefile | 1 +
drivers/net/dsa/realtek/{rtl8365mb.c => rtl8365mb_main.c} | 0
2 files changed, 1 insertion(+)
diff --git a/drivers/net/dsa/realtek/Makefile b/drivers/net/dsa/realtek/Makefile
index 17367bcba496..3f986e04912f 100644
--- a/drivers/net/dsa/realtek/Makefile
+++ b/drivers/net/dsa/realtek/Makefile
@@ -16,3 +16,4 @@ ifdef CONFIG_NET_DSA_REALTEK_RTL8366RB_LEDS
rtl8366-objs += rtl8366rb-leds.o
endif
obj-$(CONFIG_NET_DSA_REALTEK_RTL8365MB) += rtl8365mb.o
+rtl8365mb-objs := rtl8365mb_main.o \
diff --git a/drivers/net/dsa/realtek/rtl8365mb.c b/drivers/net/dsa/realtek/rtl8365mb_main.c
similarity index 100%
rename from drivers/net/dsa/realtek/rtl8365mb.c
rename to drivers/net/dsa/realtek/rtl8365mb_main.c
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 02/10] net: dsa: realtek: rtl8365mb: set STP state to disabled early
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Since we are going around and manipulating a number of port settings
during setup, it's safer to assume that the port is disabled to begin
with. This is purely defensive.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
drivers/net/dsa/realtek/rtl8365mb.c | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/drivers/net/dsa/realtek/rtl8365mb.c b/drivers/net/dsa/realtek/rtl8365mb.c
index 31fa94dac627..530ff5503543 100644
--- a/drivers/net/dsa/realtek/rtl8365mb.c
+++ b/drivers/net/dsa/realtek/rtl8365mb.c
@@ -1989,6 +1989,12 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
if (dsa_is_unused_port(ds, i))
continue;
+ /* Set the initial STP state of all ports to DISABLED, otherwise
+ * ports will still forward frames to the CPU despite being
+ * administratively down by default.
+ */
+ rtl8365mb_port_stp_state_set(ds, i, BR_STATE_DISABLED);
+
/* Forward only to the CPU */
ret = rtl8365mb_port_set_isolation(priv, i, cpu->mask);
if (ret)
@@ -1999,12 +2005,6 @@ static int rtl8365mb_setup(struct dsa_switch *ds)
if (ret)
goto out_teardown_irq;
- /* Set the initial STP state of all ports to DISABLED, otherwise
- * ports will still forward frames to the CPU despite being
- * administratively down by default.
- */
- rtl8365mb_port_stp_state_set(ds, i, BR_STATE_DISABLED);
-
/* Set up per-port private data */
p->priv = priv;
p->index = i;
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 01/10] net: dsa: tag_rtl8_4: update format description
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
In-Reply-To: <20260331-realtek_forward-v1-0-44fb63033b7e@gmail.com>
From: Alvin Šipraga <alsi@bang-olufsen.dk>
Document the updated tag layout fields (EFID, VSEL/VIDX) and clarify
which bits are set/cleared when emitting tags.
Co-developed-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Alvin Šipraga <alsi@bang-olufsen.dk>
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
net/dsa/tag_rtl8_4.c | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/net/dsa/tag_rtl8_4.c b/net/dsa/tag_rtl8_4.c
index 2464545da4d2..b7ed39c5419f 100644
--- a/net/dsa/tag_rtl8_4.c
+++ b/net/dsa/tag_rtl8_4.c
@@ -17,8 +17,8 @@
* | (8-bit) | (8-bit) |
* | Protocol [0x04] | REASON | b
* |-----------------------------------+-----------------------------------| y
- * | (1) | (1) | (2) | (1) | (3) | (1) | (1) | (1) | (5) | t
- * | FID_EN | X | FID | PRI_EN | PRI | KEEP | X | LEARN_DIS | X | e
+ * | (1) | (3) | (1) | (3) | (1) | (1) | (1) | (5) | t
+ * | EFID_EN | EFID | PRI_EN | PRI | KEEP | VSEL | LEARN_DIS | VIDX | e
* |-----------------------------------+-----------------------------------| s
* | (1) | (15-bit) | |
* | ALLOW | TX/RX | v
@@ -32,19 +32,22 @@
* EtherType | note that Realtek uses the same EtherType for
* | other incompatible tag formats (e.g. tag_rtl4_a.c)
* Protocol | 0x04: indicates that this tag conforms to this format
- * X | reserved
* ------------+-------------
* REASON | reason for forwarding packet to CPU
* | 0: packet was forwarded or flooded to CPU
* | 80: packet was trapped to CPU
- * FID_EN | 1: packet has an FID
- * | 0: no FID
- * FID | FID of packet (if FID_EN=1)
+ * EFID_EN | 1: packet has an EFID
+ * | 0: no EFID
+ * EFID | Extended filter ID (EFID) of packet (if EFID_EN=1)
* PRI_EN | 1: force priority of packet
* | 0: don't force priority
* PRI | priority of packet (if PRI_EN=1)
* KEEP | preserve packet VLAN tag format
+ * VSEL | 0: switch should classify packet according to VLAN tag
+ * | 1: switch should classify packet according to VLAN membership
+ * | configuration with index VIDX
* LEARN_DIS | don't learn the source MAC address of the packet
+ * VIDX | index of a VLAN membership configuration to use with VSEL
* ALLOW | 1: treat TX/RX field as an allowance port mask, meaning the
* | packet may only be forwarded to ports specified in the
* | mask
@@ -111,7 +114,7 @@ static void rtl8_4_write_tag(struct sk_buff *skb, struct net_device *dev,
/* Set Protocol; zero REASON */
tag16[1] = htons(FIELD_PREP(RTL8_4_PROTOCOL, RTL8_4_PROTOCOL_RTL8365MB));
- /* Zero FID_EN, FID, PRI_EN, PRI, KEEP; set LEARN_DIS */
+ /* Zero EFID_EN, EFID, PRI_EN, PRI, VSEL, VIDX, KEEP; set LEARN_DIS */
tag16[2] = htons(FIELD_PREP(RTL8_4_LEARN_DIS, 1));
/* Zero ALLOW; set RX (CPU->switch) forwarding port mask */
--
2.53.0
^ permalink raw reply related
* [net-next PATCH 00/10] net: dsa: realtek: rtl8365mb: bridge offloading and VLAN support
From: Luiz Angelo Daros de Luca @ 2026-03-31 23:00 UTC (permalink / raw)
To: Andrew Lunn, Vladimir Oltean, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Linus Walleij,
Alvin Šipraga, Yury Norov, Rasmus Villemoes, Russell King
Cc: netdev, linux-kernel, Luiz Angelo Daros de Luca
This series introduces bridge offloading, FDB management, and VLAN support
for the Realtek rtl8365mb DSA switch driver. The primary goal is to
enable hardware frame forwarding between bridge ports, reducing CPU
overhead and providing advanced features like VLAN and FDB isolation.
Some of these patches are based on original work by Alvin Šipraga,
subsequently adapted and updated for the current net-next state.
---
I attempted to reach Alvin for review of the final version but was
unable to establish contact. Any regressions in this version are my
responsibility.
Patch 04 introduces FIELD_WIDTH() in include/linux/bitfield.h. Although
not strictly part of the driver itself, it is required by subsequent
patches. I am happy to split this into a separate submission if
preferred.
To: Andrew Lunn <andrew@lunn.ch>
To: Vladimir Oltean <olteanv@gmail.com>
To: David S. Miller <davem@davemloft.net>
To: Eric Dumazet <edumazet@google.com>
To: Jakub Kicinski <kuba@kernel.org>
To: Paolo Abeni <pabeni@redhat.com>
To: Simon Horman <horms@kernel.org>
To: Linus Walleij <linusw@kernel.org>
To: Alvin Šipraga <alsi@bang-olufsen.dk>
To: Yury Norov <yury.norov@gmail.com>
To: Rasmus Villemoes <linux@rasmusvillemoes.dk>
To: Russell King <linux@armlinux.org.uk>
Cc: netdev@vger.kernel.org
Cc: linux-kernel@vger.kernel.org
Signed-off-by: Luiz Angelo Daros de Luca <luizluca@gmail.com>
---
Alvin Šipraga (8):
net: dsa: tag_rtl8_4: update format description
net: dsa: realtek: rtl8365mb: set STP state to disabled early
net: dsa: realtek: rtl8365mb: prepare for multiple source files
net: dsa: realtek: rtl8365mb: add table lookup interface
net: dsa: realtek: rtl8365mb: add VLAN support
net: dsa: realtek: rtl8365mb: add port_bridge_{join,leave}
net: dsa: realtek: rtl8365mb: add FDB support
net: dsa: realtek: rtl8365mb: add bridge port flags
Luiz Angelo Daros de Luca (2):
bitfield.h: add FIELD_WIDTH()
net: dsa: tag_rtl8_4: set KEEP flag
drivers/net/dsa/realtek/Makefile | 4 +
drivers/net/dsa/realtek/rtl8365mb_l2.c | 465 ++++++++++++
drivers/net/dsa/realtek/rtl8365mb_l2.h | 59 ++
.../dsa/realtek/{rtl8365mb.c => rtl8365mb_main.c} | 711 +++++++++++++++++-
drivers/net/dsa/realtek/rtl8365mb_table.c | 255 +++++++
drivers/net/dsa/realtek/rtl8365mb_table.h | 133 ++++
drivers/net/dsa/realtek/rtl8365mb_vlan.c | 805 +++++++++++++++++++++
drivers/net/dsa/realtek/rtl8365mb_vlan.h | 30 +
include/linux/bitfield.h | 13 +
net/dsa/tag_rtl8_4.c | 21 +-
10 files changed, 2481 insertions(+), 15 deletions(-)
---
base-commit: 1a8dd88469bf742fd5eda91cd8e0f720a983ec5a
change-id: 20260323-realtek_forward-1bac3a77c664
Best regards,
--
Luiz Angelo Daros de Luca <luizluca@gmail.com>
^ permalink raw reply
* Re: [PATCH net-next v11 0/6] tls: Add TLS 1.3 hardware offload support
From: Jakub Kicinski @ 2026-03-31 22:58 UTC (permalink / raw)
To: Rishikesh Jethwani
Cc: netdev, saeedm, tariqt, mbloch, borisp, john.fastabend, sd, davem,
pabeni, edumazet, leon
In-Reply-To: <20260331163757.149343-1-rjethwani@purestorage.com>
On Tue, 31 Mar 2026 10:37:51 -0600 Rishikesh Jethwani wrote:
> Changes in v11:
> - tls_device_complete_rekey(): flush pending open_rec (from MSG_MORE)
> via tls_sw_push_pending_record() before switching back to HW offload.
> Without this, data in an open SW record would be silently lost when
> the AEAD cipher is freed.
> - Selftest: enforce MIN_BUF_SIZE (16 bytes) on both client and server
> receive buffers to prevent KeyUpdate handshake message truncation
> with small -b values.
Not more than 1 version in a 24h period, please. No matter how trivial
the reason.
https://www.kernel.org/doc/html/next/process/maintainer-netdev.html
--
pv-bot: 24h
^ permalink raw reply
* Re: RFC: phylink: disable PHY autonomous EEE when MAC manages LPI
From: Andrew Lunn @ 2026-03-31 22:56 UTC (permalink / raw)
To: Nicolai Buchwitz; +Cc: netdev, Russell King
In-Reply-To: <d86c53213a6328b701b8aabbde5d1c83@tipi-net.de>
On Tue, Mar 31, 2026 at 11:27:20PM +0200, Nicolai Buchwitz wrote:
> Hi
>
> Some PHYs (e.g. Broadcom BCM54xx "AutogrEEEn") manage EEE LPI
> autonomously without forwarding LPI signaling to the MAC. This works
> fine when the MAC doesn't support EEE, but conflicts with MACs that
> implement their own LPI control via phylink's mac_enable_tx_lpi /
> mac_disable_tx_lpi.
>
> When the MAC has lpi_capabilities set, the PHY should use Native EEE
> mode so the MAC can control LPI entry/exit and observe RX LPI
> transitions.
>
> I'm thinking of a flag on phy_device (similar to mac_managed_pm) that
> phylink sets when the MAC has lpi_capabilities, and that PHY drivers
> check in config_init to disable their autonomous EEE mode:
>
> /* set by phylink when MAC manages LPI */
> phydev->mac_managed_lpi = true;
>
> /* in bcm54xx_config_init: */
> if (phydev->mac_managed_lpi)
> bcm_phy_modify_exp(phydev, MII_BUF_CNTL0,
> AUTOGREEEN_EN, 0);
>
> This came up while adding EEE support to the Cadence macb driver (used
> on Raspberry Pi 5 with a BCM54210PE PHY). The PHY's AutogrEEEn mode
> prevented the MAC from tracking LPI state. As a workaround, the
> Raspberry Pi tree currently carries a downstream patch that
> unconditionally disables AutogrEEEn for all BCM54xx PHYs, since both
> genet and macb now support EEE properly. That's obviously not suitable
> for upstream where other MACs paired with BCM54xx may not have LPI
> support.
>
> Does this seem like a reasonable approach, or is there a better way to
> handle PHY autonomous EEE within phylink?
You should also think about it from the other direction. What happens
with the MAC does not indicate it supports EEE, but the PHY has
autonomous EEE. At some point in the future we are going to want the
PHY drivers to export API calls so that phylink/phylib can implement
ethtool set_eee and get_eee using this autonomous EEE.
Maybe it makes more sense for Broadcom BCM54xx etc, to implement a
.set_eee() call which only accepts eee_enable == False, and when
called so, disables autonomous EEE. .get_eee() can similarly return
that EEE is disabled. When phylink sees the MAC implements EEE it can
calll into the PHY driver to turn off autonomous EEE. And we have a
path to later add support for turning on and fully configuring
autonomous EEE.
Andrew
^ permalink raw reply
* Re: [PATCH] [RESEND, net-next] net: ethernet: ti-cpsw:: rename soft_reset() function
From: Jakub Kicinski @ 2026-03-31 22:53 UTC (permalink / raw)
To: Arnd Bergmann
Cc: Andrew Lunn, David S. Miller, Eric Dumazet, Paolo Abeni,
Ilias Apalodimas, Murali Karicheri, Grygorii Strashko,
Arnd Bergmann, Siddharth Vadapalli, Roger Quadros,
Vladimir Oltean, Kevin Hao, Alexander Sverdlin, Saeed Mahameed,
Daniel Zahka, linux-omap, netdev, linux-kernel
In-Reply-To: <20260331161814.3725730-1-arnd@kernel.org>
On Tue, 31 Mar 2026 18:17:55 +0200 Arnd Bergmann wrote:
> I just realized that this patch needs to come before
> https://lore.kernel.org/netdev/20260331161254.3450606-1-arnd@kernel.org/T/#u
>
> The change itself is not important, but it renames a symbol that
> gets exported under the new name by the other patch, so either
> this gets applied first, or the other one needs a trivial change
> to fix the export.
Neither of the patches apply by themselves. Please post a proper series
once you're reading (and remember about the 24h spacing between postings
on netdev).
^ permalink raw reply
* Re: [PATCH bpf v3 5/5] bpf, sockmap: Adapt for af_unix-specific lock
From: Michal Luczaj @ 2026-03-31 22:43 UTC (permalink / raw)
To: Martin KaFai Lau
Cc: Jiayuan Chen, John Fastabend, Jakub Sitnicki, Eric Dumazet,
Kuniyuki Iwashima, Paolo Abeni, Willem de Bruijn, David S. Miller,
Jakub Kicinski, Simon Horman, Yonghong Song, Andrii Nakryiko,
Alexei Starovoitov, Daniel Borkmann, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Shuah Khan, Cong Wang, netdev, bpf, linux-kernel, linux-kselftest
In-Reply-To: <ae86a1e5-769c-44d8-ad7e-ad724800e9e4@linux.dev>
On 3/31/26 02:20, Martin KaFai Lau wrote:
> On 3/30/26 4:03 PM, Michal Luczaj wrote:
>> On 3/26/26 07:26, Martin KaFai Lau wrote:
>>> On 3/15/26 4:58 PM, Michal Luczaj wrote:
>>>>> Beside, from looking at the may_update_sockmap(), I don't know if it is
>>>>> even doable (or useful) to bpf_map_update_elem(unix_sk) in
>>>>> tc/flow_dissector/xdp. One possible path is the SOCK_FILTER when looking
>>>>> at unix_dgram_sendmsg() => sk_filter(). It was not the original use case
>>>>> when the bpf_map_update_elem(sockmap) support was added iirc.
>>>>
>>>> What about a situation when unix_sk is stored in a sockmap, then tc prog
>>>> looks it up and invokes bpf_map_update_elem(unix_sk)? I'm not sure it's
>>>> useful, but seems doable.
>>>
>>> [ Sorry for the late reply ]
>>>
>>> It is a bummer that the bpf_map_update_elem(unix_sk) path is possible
>>> from tc :(
>>>
>>> Then unix_state_lock() in its current form cannot be safely acquired in
>>> sock_map_update_elem(). It is currently a spin_lock() instead of
>>> spin_lock_bh().
>>
>> Is there a specific deadlock you have in your mind?
>
> e.g. unix_stream_connect() is taking unix_state_lock(). Can a tc's
> ingress bpf prog call unix_state_lock()?
Ah, right, that's the problem, thanks for explaining.
But, as I've asked in the parallel thread, do we really need to take the
unix_state_lock() in sock_map_update_elem()? Taking it in
sock_map_update_elem_sys() fixes the null-ptr-deref and does not lead to a
deadlock. Taking unix_state_lock() in sock_map_update_elem() seems
unnecessary. Well, at least under the assumption progs can only access
unix_sk via the sockmap lookup.
>> ...
>> And sock_{map,hash}_seq_show() (being a part of bpf iter machinery) needs
>> to take lock_sock() just as well? Would that require a special-casing
>> (unix_state_lock()) for af_unix?
>
> I would think so for lock_sock() considering the current bh_lock_sock
> without !sock_owned_by_user() usage is incorrect in
> sock_map_update_elem(). [ this probably should be a separate issue for
> another patch ]
All right, leaving that for later.
> Some more side-tracking... from looking at the code, the bpf_iter of
> sock_{map,hash} can do bpf_map_lookup_elem(&sock_map, ...). This
> bpr_iter program probably will be failed to load because the
> bpf_sk_release() is not available.
I think ability to bpf_map_lookup_elem(sockmap) was added way before bpf
iter in
https://lore.kernel.org/bpf/20200429181154.479310-2-jakub@cloudflare.com/
and iter "allows" it somewhat accidentally. Would you rather have it
explicitly dropped?
> I still don't have good idea what to do with the tc's prog calling
> sock_map_update_elem().
^ permalink raw reply
* Re: [PATCH bpf v3 5/5] bpf, sockmap: Adapt for af_unix-specific lock
From: Michal Luczaj @ 2026-03-31 22:43 UTC (permalink / raw)
To: Kuniyuki Iwashima
Cc: Martin KaFai Lau, Jiayuan Chen, John Fastabend, Jakub Sitnicki,
Eric Dumazet, Paolo Abeni, Willem de Bruijn, David S. Miller,
Jakub Kicinski, Simon Horman, Yonghong Song, Andrii Nakryiko,
Alexei Starovoitov, Daniel Borkmann, Eduard Zingerman, Song Liu,
Yonghong Song, KP Singh, Stanislav Fomichev, Hao Luo, Jiri Olsa,
Shuah Khan, Cong Wang, netdev, bpf, linux-kernel, linux-kselftest
In-Reply-To: <CAAVpQUAtF29CPDAdEUchYOP=oy8D2-B3GH3fWs2pwTmnYoTu=w@mail.gmail.com>
On 3/31/26 01:27, Kuniyuki Iwashima wrote:
> On Mon, Mar 30, 2026 at 4:04 PM Michal Luczaj <mhal@rbox.co> wrote:
>> On 3/26/26 07:26, Martin KaFai Lau wrote:
>>> On 3/15/26 4:58 PM, Michal Luczaj wrote:
>>>>> Beside, from looking at the may_update_sockmap(), I don't know if it is
>>>>> even doable (or useful) to bpf_map_update_elem(unix_sk) in
>>>>> tc/flow_dissector/xdp. One possible path is the SOCK_FILTER when looking
>>>>> at unix_dgram_sendmsg() => sk_filter(). It was not the original use case
>>>>> when the bpf_map_update_elem(sockmap) support was added iirc.
>>>>
>>>> What about a situation when unix_sk is stored in a sockmap, then tc prog
>>>> looks it up and invokes bpf_map_update_elem(unix_sk)? I'm not sure it's
>>>> useful, but seems doable.
>>>
>>> [ Sorry for the late reply ]
>>>
>>> It is a bummer that the bpf_map_update_elem(unix_sk) path is possible
>>> from tc :(
>>>
>>> Then unix_state_lock() in its current form cannot be safely acquired in
>>> sock_map_update_elem(). It is currently a spin_lock() instead of
>>> spin_lock_bh().
>>
>> Is there a specific deadlock you have in your mind?
>
> lockdep would complain if we used the same lock from
> different contexts.
>
> e.g.)
> Process context holding unix_state_lock() with the normal spin_lock()
> -> BH interrupt
> -> tc prog trying to hold the same lock with spin_lock(). (_bh())
> -> deadlock
OK, I get it, thanks.
So here's one more idea: the null-ptr-deref issue is connect() racing
against sock_map_update_elem_*SYS*() coming from user-space, not the
can-be-called-from-BH sock_map_update_elem() variant. So can't we assume
that for any sock_map_update_elem(unix_sk) invoked by a tc prog, unix_sk
will always be "stable", i.e. in a state that cannot lead to that
null-ptr-deref?
IOW, if for a tc prog the only way to get hold of unix_sk is look it up in
a sockmap, then (by the fact that unix_sk _is_ in the sockmap) unix_sk will
be already safe to use by sock_map_update_elem() without taking the af_unix
state lock.
Long story short: take the unix state lock in sock_map_update_elem_sys(),
don't bother in sock_map_update_elem()?
>> ...
>> And sock_{map,hash}_seq_show() (being a part of bpf iter machinery) needs
>> to take lock_sock() just as well? Would that require a special-casing
>> (unix_state_lock()) for af_unix?
>
> Right, lock_sock() + unix_state_lock() + SOCK_DEAD check
> should be best.
OK, got it.
^ permalink raw reply
* Re: [PATCH net] ipv4: nexthop: allocate skb dynamically in rtm_get_nexthop()
From: Jakub Kicinski @ 2026-03-31 22:41 UTC (permalink / raw)
To: Fernando Fernandez Mancera
Cc: netdev, horms, pabeni, edumazet, davem, dsahern, Yiming Qian
In-Reply-To: <3025cc67-ddbf-43f6-a313-602193979ace@suse.de>
On Tue, 31 Mar 2026 19:40:38 +0200 Fernando Fernandez Mancera wrote:
> thanks for sharing. As I replied to Eric this is the main reason why a
> V2 is needed. There is also another bug I discovered while fixing this.
> When dumping the stats NHA_HW_STATS_ENABLE is being included twice per
> group. I am fixing that in another patch that will be included on the V2
> series.
If you remember -- please try to slap a pw-bot: cr on your own reply
when you notice that a follow up is needed, especially if the patch
is breaking selftests. We still lack automation to attribute failures
and bad changes get stuck in the testing branch :(
^ permalink raw reply
* Re: [PATCH v9 net-next 5/5] selftest/net: psp: Add test for dev-assoc/disassoc
From: Wei Wang @ 2026-03-31 22:09 UTC (permalink / raw)
To: Daniel Zahka
Cc: netdev, Jakub Kicinski, Willem de Bruijn, David Wei, Andrew Lunn,
David S . Miller, Eric Dumazet, Simon Horman, Wei Wang
In-Reply-To: <956aefbd-b795-48be-ab73-58f489b1b535@gmail.com>
On Tue, Mar 31, 2026 at 7:04 AM Daniel Zahka <daniel.zahka@gmail.com> wrote:
>
>
> On 3/30/26 6:31 PM, Wei Wang wrote:
> > +
> > +def _dev_assoc_no_nsid(cfg):
> > + """ Test dev-assoc and dev-disassoc without nsid attribute """
> > + _init_psp_dev(cfg, True)
> > + psp_dev_id = cfg.psp_dev_id
> > +
> > + # Get nk_host's ifindex (in host namespace, same as caller)
> > + nk_host_dev = ip(f"link show dev {cfg._nk_host_ifname}", json=True)[0]
> > + nk_host_ifindex = nk_host_dev['ifindex']
> > +
> > + # Associate without nsid - should look up ifindex in caller's netns
> > + cfg.pspnl.dev_assoc({'id': psp_dev_id, 'ifindex': nk_host_ifindex})
> > +
> > + # Verify assoc-list contains the device
> > + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id})
> > + ksft_true('assoc-list' in dev_info, "No assoc-list after association")
> > + found = False
> > + for assoc in dev_info['assoc-list']:
> > + if assoc['ifindex'] == nk_host_ifindex:
> > + found = True
> > + break
> > + ksft_true(found, "Associated device not found in assoc-list")
> > +
> > + # Disassociate without nsid - should also use caller's netns
> > + cfg.pspnl.dev_disassoc({'id': psp_dev_id, 'ifindex': nk_host_ifindex})
> > +
> > + # Verify assoc-list no longer contains the device
> > + dev_info = cfg.pspnl.dev_get({'id': psp_dev_id})
> > + found = False
> > + if 'assoc-list' in dev_info:
> > + for assoc in dev_info['assoc-list']:
> > + if assoc['ifindex'] == nk_host_ifindex:
> > + found = True
> > + break
> > + ksft_true(not found, "Device should not be in assoc-list after disassociation")
> > +
> > + del cfg.psp_dev_id
> > + del cfg.psp_info
> > +
> ...
> > +
> > +@ksft_variants([
> > + KsftNamedVariant(f"v{v}_ip6", v, "6")
> > + for v in range(4)
> > +])
> > +def dev_assoc_no_nsid(cfg, version, ipver):
> > + cfg.require_ipver(ipver)
> > + _dev_assoc_no_nsid(cfg)
> > +
>
>
> Why do we need ip connectivity for this test? Why are you parameterizing
> over PSP version?
Yea. This test does not need ip connectivity. And no need to test over
multiple PSP versions. Will remove the variants.
>
>
^ permalink raw reply
* Re: [PATCH bpf v4 1/2] bpf: tcp: Reject non-TCP skb in bpf_sk_assign_tcp_reqsk()
From: Kuniyuki Iwashima @ 2026-03-31 22:04 UTC (permalink / raw)
To: Martin KaFai Lau
Cc: Jiayuan Chen, bot+bpf-ci, bpf, clm, daniel, john.fastabend, sdf,
ast, andrii, eddyz87, song, yonghong.song, kpsingh, haoluo, jolsa,
davem, edumazet, kuba, pabeni, horms, shuah, netdev, linux-kernel,
linux-kselftest, martin.lau, ihor.solodrai
In-Reply-To: <79805496-9996-40b6-8cbf-0beaf7dd5473@linux.dev>
On Mon, Mar 30, 2026 at 8:19 PM Martin KaFai Lau <martin.lau@linux.dev> wrote:
>
> On 3/30/26 6:35 PM, Kuniyuki Iwashima wrote:
> > On Mon, Mar 30, 2026 at 2:26 AM Jiayuan Chen <jiayuan.chen@linux.dev> wrote:
> >>
> >>
> >> On 3/30/26 5:00 PM, bot+bpf-ci@kernel.org wrote:
> >>>> #if IS_BUILTIN(CONFIG_IPV6)
> >>>> case htons(ETH_P_IPV6):
> >>>> + if (ipv6_hdr(skb)->nexthdr != IPPROTO_TCP)
> >>>> + return -EINVAL;
> >>>> +
> >>>> ops = &tcp6_request_sock_ops;
> >>> For IPv6, ipv6_hdr(skb)->nexthdr gives the type of the header
> >>> immediately following the base IPv6 header. If extension headers
> >>> are present (hop-by-hop options, routing, etc.), nexthdr would be
> >>> the extension header type rather than IPPROTO_TCP, even when the
> >>> packet is a valid TCP segment.
> >>>
> >>> Would it be worth using ipv6_find_hdr() here, similar to
> >>> bpf_update_srh_state() in the same file, to walk past any extension
> >>> headers? The IPv4 check above is fine since ip_hdr(skb)->protocol
> >>> always identifies the transport protocol directly.
> >>>
> >>> In practice this is very unlikely to matter since TCP SYN packets
> >>> with IPv6 extension headers are essentially non-existent, but the
> >>> check as written would reject them.
> >>>
> >>>
> >>> ---
> >>> AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
> >>> See:https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
> >>>
> >>> CI run summary:https://github.com/kernel-patches/bpf/actions/runs/23735111188
> >>
> >>
> >> Thanks for the analysis.
> >>
> >> There are many places in the kernel that check nexthdr directly without
> >> walking extension headers.
> >>
> >> I'd prefer to keep it simple for now and leave ipv6_find_hdr() as a
> >> potential future improvement if needed.
> >
> > +1.
> >
> > Given this feature is to create a reqsk to process on this running
> > host, it does not make sense to support such ext options.
>
> While at header reading, does it need a pskb_may_pull() before reading?
Ah good point, now that we read skb->data, pskb_may_pull() is needed indeed.
^ permalink raw reply
* Re: [PATCH v9 net-next 3/5] psp: add a new netdev event for dev unregister
From: Wei Wang @ 2026-03-31 21:51 UTC (permalink / raw)
To: Daniel Zahka
Cc: netdev, Jakub Kicinski, Willem de Bruijn, David Wei, Andrew Lunn,
David S . Miller, Eric Dumazet, Simon Horman, Wei Wang
In-Reply-To: <1d013236-e945-46fc-8acb-c7ab9ac32a1f@gmail.com>
On Tue, Mar 31, 2026 at 6:30 AM Daniel Zahka <daniel.zahka@gmail.com> wrote:
>
>
> On 3/30/26 6:31 PM, Wei Wang wrote:
> > +static bool psp_notifier_registered;
> > +
> > +/**
> > + * psp_attach_netdev_notifier() - register netdev notifier on first use
> > + *
> > + * Register the netdevice notifier when the first device association
> > + * is created. In many installations no associations will be created and
> > + * the notifier won't be needed.
> > + *
> > + * Must be called without psd->lock held, due to lock ordering:
> > + * rtnl_lock -> psd->lock (the notifier callback runs under rtnl_lock
> > + * and takes psd->lock).
> > + */
> > +void psp_attach_netdev_notifier(void)
> > +{
> > + if (READ_ONCE(psp_notifier_registered))
> > + return;
> > +
> > + mutex_lock(&psp_devs_lock);
> > + if (!psp_notifier_registered) {
> > + if (!register_netdevice_notifier(&psp_netdev_notifier))
> > + WRITE_ONCE(psp_notifier_registered, true);
> > + }
> > + mutex_unlock(&psp_devs_lock);
> > +}
> ...
> >
> > +int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops,
> > + struct sk_buff *skb, struct genl_info *info)
> > +{
> > + psp_attach_netdev_notifier();
> > +
> > + return __psp_device_get_locked(ops, skb, info, false);
> > +}
>
>
> From an AI review, but seems plausible to me:
>
> psp_device_get_locked_dev_assoc() will proceed even in the path where
> register_netdevice_notifier() and psp_notifier_registered stays false.
>
This behavior is intended. In case register_netdevice_notifier() fails
for some reason, this current dev_assoc will proceed but the next
dev_assoc operation will try to register_netdevice_notifier() again.
>
^ permalink raw reply
* Re: [PATCH v9 net-next 2/5] psp: add new netlink cmd for dev-assoc and dev-disassoc
From: Wei Wang @ 2026-03-31 21:48 UTC (permalink / raw)
To: Daniel Zahka
Cc: netdev, Jakub Kicinski, Willem de Bruijn, David Wei, Andrew Lunn,
David S . Miller, Eric Dumazet, Simon Horman, Wei Wang
In-Reply-To: <a9c76822-cca7-4108-9395-4af6a4ff1eca@gmail.com>
On Tue, Mar 31, 2026 at 6:03 AM Daniel Zahka <daniel.zahka@gmail.com> wrote:
>
>
> On 3/30/26 6:31 PM, Wei Wang wrote:
> > + -
> > + name: dev-assoc
> > + doc: Associate a network device with a PSP device.
> > + attribute-set: dev
> > + do:
> > + request:
> > + attributes:
> > + - id
> > + - ifindex
> > + - nsid
> > + reply:
> > + attributes: []
> > + pre: psp-device-get-locked
> > + post: psp-device-unlock
> > + -
> > + name: dev-disassoc
> > + doc: Disassociate a network device from a PSP device.
> > + attribute-set: dev
> > + do:
> > + request:
> > + attributes:
> > + - id
> > + - ifindex
> > + - nsid
> > + reply:
> > + attributes: []
> > + pre: psp-device-get-locked
> > + post: psp-device-unlock
> >
>
> Not sure where we ended up on the netns isolation model, but I'll just
> note that these ops are not "admin" so they can be done from any netns.
> I don't think that's an issue per se, but I wonder if someone with a
> different use case will come along down the road and want to go for the
> more restrictive deployment option.
I will add a note before the function definition of psp_device_get_locked().
>
> >
> > + list_for_each_entry_safe(entry, entry_tmp, &psd->assoc_dev_list,
> > + dev_list) {
> > + list_del(&entry->dev_list);
> > + rcu_assign_pointer(entry->assoc_dev->psp_dev, NULL);
> > + netdev_put(entry->assoc_dev, &entry->dev_tracker);
> > + kfree(entry);
> > + }
> > +
> > rcu_assign_pointer(psd->main_netdev->psp_dev, NULL);
> >
> > psd->ops = NULL;
> > @@ -361,5 +381,4 @@ static int __init psp_init(void)
> >
> > return genl_register_family(&psp_nl_family);
> > }
> > -
> > subsys_initcall(psp_init);
>
> nit: spurious line deletion
>
> > +int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info)
> > +{
> > + struct psp_dev *psd = info->user_ptr[0];
> > + struct psp_assoc_dev *psp_assoc_dev;
> > + struct net_device *assoc_dev;
> > + u32 assoc_ifindex;
> > + struct sk_buff *rsp;
>
> nit: reverse xmas issue
>
> > +
> > +int psp_nl_dev_disassoc_doit(struct sk_buff *skb, struct genl_info *info)
> > +{
> > + struct psp_assoc_dev *entry, *found = NULL;
> > + struct psp_dev *psd = info->user_ptr[0];
> > + u32 assoc_ifindex;
> > + struct sk_buff *rsp;
>
> nit: reverse xmas issue
>
Ack.
^ permalink raw reply
* RFC: phylink: disable PHY autonomous EEE when MAC manages LPI
From: Nicolai Buchwitz @ 2026-03-31 21:27 UTC (permalink / raw)
To: netdev; +Cc: Russell King, Andrew Lunn
Hi
Some PHYs (e.g. Broadcom BCM54xx "AutogrEEEn") manage EEE LPI
autonomously without forwarding LPI signaling to the MAC. This works
fine when the MAC doesn't support EEE, but conflicts with MACs that
implement their own LPI control via phylink's mac_enable_tx_lpi /
mac_disable_tx_lpi.
When the MAC has lpi_capabilities set, the PHY should use Native EEE
mode so the MAC can control LPI entry/exit and observe RX LPI
transitions.
I'm thinking of a flag on phy_device (similar to mac_managed_pm) that
phylink sets when the MAC has lpi_capabilities, and that PHY drivers
check in config_init to disable their autonomous EEE mode:
/* set by phylink when MAC manages LPI */
phydev->mac_managed_lpi = true;
/* in bcm54xx_config_init: */
if (phydev->mac_managed_lpi)
bcm_phy_modify_exp(phydev, MII_BUF_CNTL0,
AUTOGREEEN_EN, 0);
This came up while adding EEE support to the Cadence macb driver (used
on Raspberry Pi 5 with a BCM54210PE PHY). The PHY's AutogrEEEn mode
prevented the MAC from tracking LPI state. As a workaround, the
Raspberry Pi tree currently carries a downstream patch that
unconditionally disables AutogrEEEn for all BCM54xx PHYs, since both
genet and macb now support EEE properly. That's obviously not suitable
for upstream where other MACs paired with BCM54xx may not have LPI
support.
Does this seem like a reasonable approach, or is there a better way to
handle PHY autonomous EEE within phylink?
Thanks
Nicolai
^ permalink raw reply
* RE: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support for sideband queue operations
From: Keller, Jacob E @ 2026-03-31 21:20 UTC (permalink / raw)
To: Korba, Przemyslaw, intel-wired-lan@lists.osuosl.org
Cc: netdev@vger.kernel.org, Nguyen, Anthony L, Kitszel, Przemyslaw
In-Reply-To: <PH0PR11MB4904757F3E979FAD9ED44EE29453A@PH0PR11MB4904.namprd11.prod.outlook.com>
> -----Original Message-----
> From: Korba, Przemyslaw <przemyslaw.korba@intel.com>
> Sent: Tuesday, March 31, 2026 5:57 AM
> To: Keller, Jacob E <jacob.e.keller@intel.com>; intel-wired-lan@lists.osuosl.org
> Cc: netdev@vger.kernel.org; Nguyen, Anthony L
> <anthony.l.nguyen@intel.com>; Kitszel, Przemyslaw
> <przemyslaw.kitszel@intel.com>
> Subject: RE: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support
> for sideband queue operations
>
> > -----Original Message-----
> > From: Keller, Jacob E <jacob.e.keller@intel.com>
> > Sent: Thursday, March 26, 2026 12:42 AM
> > To: Korba, Przemyslaw <przemyslaw.korba@intel.com>; intel-wired-
> lan@lists.osuosl.org
> > Cc: netdev@vger.kernel.org; Nguyen, Anthony L
> <anthony.l.nguyen@intel.com>; Kitszel, Przemyslaw
> <przemyslaw.kitszel@intel.com>
> > Subject: Re: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support
> for sideband queue operations
> >
> > On 3/19/2026 4:13 AM, Przemyslaw Korba wrote:
> > > On E830, PTP time adjustment commands sent via
> > > SBQ don't generate completion responses, causing the driver to
> > > timeout waiting and return -EIO, when trying:
> > >
> > Hm. Is this really for E830? I didn't realize that device actually
> > sends SBQ commands for PTP?
> >
> > According to ice_ptp_adj_clock for E830, it says "E830 sync PHYs
> > automatically after setting GLTSYN_SHADJ".
> >
> > Did you mean E825-C here? Can you confirm the device type affected? Or
> > am I missing something?
> >
> > > phc_ctl eth8 get adj 2 get
> > > dmesg: ice 0000:1a:00.0: PTP failed to adjust time, err -5
> > >
> > > Add support for posted mode not to wait for completion response.
> > >
> > > Fixes: 8f5ee3c477a8 ("ice: add support for sideband messages")
> > > Signed-off-by: Przemyslaw Korba <przemyslaw.korba@intel.com>
> > > ---
> > > v2:
> > > - change "postpone" to "posted"
> > > - init struct with {} instead of {0}
> > > v1:
> > > https://lore.kernel.org/intel-wired-lan/20260310110700.345904-1-
> przemyslaw.korba@intel.com/
> > >
> > > drivers/net/ethernet/intel/ice/ice_common.c | 7 ++++++-
> > > drivers/net/ethernet/intel/ice/ice_controlq.c | 4 ++++
> > > drivers/net/ethernet/intel/ice/ice_controlq.h | 1 +
> > > 3 files changed, 11 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/drivers/net/ethernet/intel/ice/ice_common.c
> b/drivers/net/ethernet/intel/ice/ice_common.c
> > > index 8866902efb91..c89c6ca1281b 100644
> > > --- a/drivers/net/ethernet/intel/ice/ice_common.c
> > > +++ b/drivers/net/ethernet/intel/ice/ice_common.c
> > > @@ -1765,6 +1765,7 @@ int ice_sbq_rw_reg(struct ice_hw *hw, struct
> ice_sbq_msg_input *in, u16 flags)
> > > {
> >
> > This affects the ice_sbq_rw_reg function which is used by several
> > variants including the E822 devices, E825-C with ETH56G, and even E810
> > devices.
> >
> > Do all these devices not provide completion? Or do we simply not care
> > about waiting?
> >
> > I don't see a single call to ice_sbq_rw_reg for E830, so I suspect this
> > is correct but for a different device, and the commit message is just a
> > typo?
>
> Hi Jake,
> Thanks for review! Grzegorz's patch he mentioned indeed makes it so E830
> uses this functionality.
> You are right there are other devices we need to consider. I've found more
> complete Karol's patch that does exactly that, but got stuck on reviews:
> https://lore.kernel.org/intel-wired-lan/20250520110823.1937981-7-
> karol.kolacinski@intel.com/
> I addressed past reviews, and will try to merge this patch, I sent updated
> version to our internal mailing list for review : )
>
Great, thanks!
^ permalink raw reply
* RE: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support for sideband queue operations
From: Keller, Jacob E @ 2026-03-31 21:19 UTC (permalink / raw)
To: Nitka, Grzegorz, Korba, Przemyslaw,
intel-wired-lan@lists.osuosl.org
Cc: netdev@vger.kernel.org, Nguyen, Anthony L, Kitszel, Przemyslaw
In-Reply-To: <IA1PR11MB6219D3F43CF0192FFAE07CD89252A@IA1PR11MB6219.namprd11.prod.outlook.com>
> -----Original Message-----
> From: Nitka, Grzegorz <grzegorz.nitka@intel.com>
> Sent: Monday, March 30, 2026 9:23 AM
> To: Keller, Jacob E <jacob.e.keller@intel.com>; Korba, Przemyslaw
> <przemyslaw.korba@intel.com>; intel-wired-lan@lists.osuosl.org
> Cc: netdev@vger.kernel.org; Nguyen, Anthony L
> <anthony.l.nguyen@intel.com>; Kitszel, Przemyslaw
> <przemyslaw.kitszel@intel.com>
> Subject: RE: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support
> for sideband queue operations
>
>
>
> > -----Original Message-----
> > From: Intel-wired-lan <intel-wired-lan-bounces@osuosl.org> On Behalf Of
> > Jacob Keller
> > Sent: Thursday, March 26, 2026 12:42 AM
> > To: Korba, Przemyslaw <przemyslaw.korba@intel.com>; intel-wired-
> > lan@lists.osuosl.org
> > Cc: netdev@vger.kernel.org; Nguyen, Anthony L
> > <anthony.l.nguyen@intel.com>; Kitszel, Przemyslaw
> > <przemyslaw.kitszel@intel.com>
> > Subject: Re: [Intel-wired-lan] [PATCH iwl-net v2] ice: fix posted write support
> > for sideband queue operations
> >
> > On 3/19/2026 4:13 AM, Przemyslaw Korba wrote:
> > > On E830, PTP time adjustment commands sent via
> > > SBQ don't generate completion responses, causing the driver to
> > > timeout waiting and return -EIO, when trying:
> > >
> > Hm. Is this really for E830? I didn't realize that device actually
> > sends SBQ commands for PTP?
> >
> > According to ice_ptp_adj_clock for E830, it says "E830 sync PHYs
> > automatically after setting GLTSYN_SHADJ".
> >
> > Did you mean E825-C here? Can you confirm the device type affected? Or
> > am I missing something?
> >
> > > phc_ctl eth8 get adj 2 get
> > > dmesg: ice 0000:1a:00.0: PTP failed to adjust time, err -5
> > >
> > > Add support for posted mode not to wait for completion response.
> > >
> > > Fixes: 8f5ee3c477a8 ("ice: add support for sideband messages")
> > > Signed-off-by: Przemyslaw Korba <przemyslaw.korba@intel.com>
> > > ---
> > > v2:
> > > - change "postpone" to "posted"
> > > - init struct with {} instead of {0}
> > > v1:
> > > https://lore.kernel.org/intel-wired-lan/20260310110700.345904-1-
> > przemyslaw.korba@intel.com/
> > >
> > > drivers/net/ethernet/intel/ice/ice_common.c | 7 ++++++-
> > > drivers/net/ethernet/intel/ice/ice_controlq.c | 4 ++++
> > > drivers/net/ethernet/intel/ice/ice_controlq.h | 1 +
> > > 3 files changed, 11 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/drivers/net/ethernet/intel/ice/ice_common.c
> > b/drivers/net/ethernet/intel/ice/ice_common.c
> > > index 8866902efb91..c89c6ca1281b 100644
> > > --- a/drivers/net/ethernet/intel/ice/ice_common.c
> > > +++ b/drivers/net/ethernet/intel/ice/ice_common.c
> > > @@ -1765,6 +1765,7 @@ int ice_sbq_rw_reg(struct ice_hw *hw, struct
> > ice_sbq_msg_input *in, u16 flags)
> > > {
> >
> > This affects the ice_sbq_rw_reg function which is used by several
> > variants including the E822 devices, E825-C with ETH56G, and even E810
> > devices.
> >
> > Do all these devices not provide completion? Or do we simply not care
> > about waiting?
> >
> > I don't see a single call to ice_sbq_rw_reg for E830, so I suspect this
> > is correct but for a different device, and the commit message is just a
> > typo?
> >
>
> I strongly believe this patch is needed for E830 devices.
> Please note that after this fix:
> https://patchwork.ozlabs.org/project/intel-wired-
> lan/patch/20251218094428.1762860-1-grzegorz.nitka@intel.com/
> there is actually a call to ice_sbq_rw_write. And we used 'posted' variant for
> this case.
>
> Regards
>
> Grzegorz
>
Hmm. True. I think the commit description could still better indicate it affects multiple devices, but thanks for clarifying that it does in fact also affect E830. Good!
Looking at the fix you posted, I think we might actually have to do the same fix for incvalue writing as well, since that function also didn't issue a timer command. Can we confirm this? It looks somewhat similar, but different enough it might not actually be a problem.
Thanks,
Jake
> > > struct ice_sbq_cmd_desc desc = {0};
> > > struct ice_sbq_msg_req msg = {0};
> > > + struct ice_sq_cd cd = {};
> > > u16 msg_len;
> > > int status;
> > >
> > > @@ -1785,10 +1786,14 @@ int ice_sbq_rw_reg(struct ice_hw *hw, struct
> > ice_sbq_msg_input *in, u16 flags)
> > > */
> > > msg_len -= sizeof(msg.data);
> > >
> > > + if (in->opcode == ice_sbq_msg_wr)
> > > + cd.posted = 1;
> > > +
> > > desc.flags = cpu_to_le16(flags);
> > > desc.opcode = cpu_to_le16(ice_sbq_opc_neigh_dev_req);
> > > desc.param0.cmd_len = cpu_to_le16(msg_len);
> > > - status = ice_sbq_send_cmd(hw, &desc, &msg, msg_len, NULL);
> > > + status = ice_sbq_send_cmd(hw, &desc, &msg, msg_len, &cd);
> > > +
> > > if (!status && !in->opcode)
> > > in->data = le32_to_cpu
> > > (((struct ice_sbq_msg_cmpl *)&msg)->data);
> > > diff --git a/drivers/net/ethernet/intel/ice/ice_controlq.c
> > b/drivers/net/ethernet/intel/ice/ice_controlq.c
> > > index dcb837cadd18..a6008dc77fa4 100644
> > > --- a/drivers/net/ethernet/intel/ice/ice_controlq.c
> > > +++ b/drivers/net/ethernet/intel/ice/ice_controlq.c
> > > @@ -1086,6 +1086,10 @@ ice_sq_send_cmd(struct ice_hw *hw, struct
> > ice_ctl_q_info *cq,
> > > wr32(hw, cq->sq.tail, cq->sq.next_to_use);
> > > ice_flush(hw);
> > >
> > > + /* If the message is posted, don't wait for completion. */
> > > + if (cd && cd->posted)
> > > + goto sq_send_command_error;
> > > +
> > > /* Wait for the command to complete. If it finishes within the
> > > * timeout, copy the descriptor back to temp.
> > > */
> > > diff --git a/drivers/net/ethernet/intel/ice/ice_controlq.h
> > b/drivers/net/ethernet/intel/ice/ice_controlq.h
> > > index 788040dd662e..c50d6fcbacba 100644
> > > --- a/drivers/net/ethernet/intel/ice/ice_controlq.h
> > > +++ b/drivers/net/ethernet/intel/ice/ice_controlq.h
> > > @@ -77,6 +77,7 @@ struct ice_ctl_q_ring {
> > > /* sq transaction details */
> > > struct ice_sq_cd {
> > > struct libie_aq_desc *wb_desc;
> > > + u8 posted : 1;
> > > };
> > >
> > > /* rq event information */
> > >
> > > base-commit: acd2abc52dea91c3bc3d1b6dd8a92b9631d48bbf
^ permalink raw reply
* Re: [net-next v6 09/12] net: bnxt: Add SW GSO completion and teardown support
From: Joe Damato @ 2026-03-31 21:14 UTC (permalink / raw)
To: Jakub Kicinski
Cc: netdev, Michael Chan, David S. Miller, Eric Dumazet, Paolo Abeni,
andrew+netdev, horms, pavan.chebbi, linux-kernel, leon
In-Reply-To: <20260330165748.2edb501a@kernel.org>
On Mon, Mar 30, 2026 at 04:57:48PM -0700, Jakub Kicinski wrote:
> On Thu, 26 Mar 2026 16:52:28 -0700 Joe Damato wrote:
> > @@ -4645,6 +4687,10 @@ static int bnxt_init_tx_rings(struct bnxt *bp)
> >
> > bp->tx_wake_thresh = max_t(int, bp->tx_ring_size / 2,
> > BNXT_MIN_TX_DESC_CNT);
> > + if (!(bp->flags & BNXT_FLAG_UDP_GSO_CAP) &&
> > + (bp->dev->features & NETIF_F_GSO_UDP_L4))
> > + bp->tx_wake_thresh = max_t(int, bp->tx_wake_thresh,
> > + BNXT_SW_USO_MAX_DESCS);
> >
> > for (i = 0; i < bp->tx_nr_rings; i++) {
> > struct bnxt_tx_ring_info *txr = &bp->tx_ring[i];
> > @@ -13832,6 +13878,11 @@ static netdev_features_t bnxt_fix_features(struct net_device *dev,
> > if ((features & NETIF_F_NTUPLE) && !bnxt_rfs_capable(bp, false))
> > features &= ~NETIF_F_NTUPLE;
> >
> > + if ((features & NETIF_F_GSO_UDP_L4) &&
> > + !(bp->flags & BNXT_FLAG_UDP_GSO_CAP) &&
> > + bp->tx_ring_size < 2 * BNXT_SW_USO_MAX_DESCS)
> > + features &= ~NETIF_F_GSO_UDP_L4;
> > +
> > if ((bp->flags & BNXT_FLAG_NO_AGG_RINGS) || bp->xdp_prog)
> > features &= ~(NETIF_F_LRO | NETIF_F_GRO_HW);
> >
> > @@ -13877,6 +13928,15 @@ static int bnxt_set_features(struct net_device *dev, netdev_features_t features)
> > int rc = 0;
> > bool re_init = false;
> >
> > + if (!(bp->flags & BNXT_FLAG_UDP_GSO_CAP)) {
> > + if (features & NETIF_F_GSO_UDP_L4)
> > + bp->tx_wake_thresh = max_t(int, bp->tx_wake_thresh,
> > + BNXT_SW_USO_MAX_DESCS);
> > + else
> > + bp->tx_wake_thresh = max_t(int, bp->tx_ring_size / 2,
> > + BNXT_MIN_TX_DESC_CNT);
>
> Adding extra handling for min ring size all over the place looks a bit
> messy. Can you factor something out of this logic?
Could add something like:
static int bnxt_min_tx_desc_cnt(struct bnxt *bp)
{
if (!(bp->flags & BNXT_FLAG_UDP_GSO_CAP) &&
(bp->dev->features & NETIF_F_GSO_UDP_L4))
return BNXT_SW_USO_MAX_DESCS;
return BNXT_MIN_TX_DESC_CNT;
}
and then when setting the tx_wake_thresh, it becomes:
bp->tx_wake_thresh = max_t(int, bp->tx_ring_size / 2,
bnxt_min_tx_desc_cnt(bp));
and fix_features can use the same helper.
Question then is just do we still want to bump BNXT_MIN_TX_DESC_CNT (as per
your previous comment)?
^ permalink raw reply
* Re: [net-next v6 08/12] net: bnxt: Implement software USO
From: Joe Damato @ 2026-03-31 21:11 UTC (permalink / raw)
To: Jakub Kicinski
Cc: netdev, Michael Chan, David S. Miller, Eric Dumazet, Paolo Abeni,
andrew+netdev, horms, pavan.chebbi, linux-kernel, leon
In-Reply-To: <20260330165357.31a30b68@kernel.org>
On Mon, Mar 30, 2026 at 04:53:57PM -0700, Jakub Kicinski wrote:
> On Mon, 30 Mar 2026 09:53:33 -0700 Joe Damato wrote:
> > > > + bp->tx_wake_thresh);
> > >
> > > Is tx_wake_thresh larger than the max USO even for smallest ring size?
> >
> > Yes, it is.
> >
> > Maybe its worth adding a comment in the code somewhere to make
> > this more clear? Not sure where would be an appropriate place, but maybe
> > bnxt_init_tx_rings?
>
> Hm, as long as BNXT_MIN_TX_DESC_CNT is updated I don't think we need
> any bespoke comments
I am hesitant to update BNXT_MIN_TX_DESC_CNT because it affects all hardware,
including devices that do USO in hardware.
It may not matter in practice since I would be bumping it from 19 to 210, but
we could just leave it as is and let fix_features and init_tx_rings deal with
ring size (as they do now).
IDK. Feels "safer" (but maybe less clear) to leave it as is. WDYT?
^ permalink raw reply
* Re: [PATCH v3] docs: octeontx2:Fix typo in documentation
From: Shuah Khan @ 2026-03-31 21:08 UTC (permalink / raw)
To: ShravyaPanchagiri, netdev, linux-kernel, linux-doc
Cc: sgoutham, lcherian, gakula, hkelam, sbhatta, davem, edumazet,
kuba, pabeni, horms, corbet, Shuah Khan
In-Reply-To: <20260312162715.35408-1-shravy112@gmail.com>
On 3/12/26 10:27, ShravyaPanchagiri wrote:
> Correct a spelling mistake.
> ---
> v3:
> - Moved spelling fix details from the subject line to the changelog.
> - Simplified the commit message.
> v2:
> - Fixed the subject prefix formatting (added space after "docs:").
> - Moved the long description into the commit body.
A few things to fix in the patch:
- Signed-off-by is missing
- version to version change information should be placed under ---
below the Signed-ff-by
- Check submitting patches documentation for details on how to
submit patches.
thanks,
-- Shuah
^ permalink raw reply
* [RFC PATCH net-next 4/4] selftests: net: add IPv4 address lookup stress test
From: hawk @ 2026-03-31 21:07 UTC (permalink / raw)
To: netdev
Cc: davem, dsahern, edumazet, kuba, pabeni, horms, shuah,
linux-kselftest, hawk, ivan, kernel-team
In-Reply-To: <20260331210739.3998753-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Add a test that exercises the IPv4 local address hash table
(inet_addr_lst) insert, lookup, and remove paths under load:
- Add/remove 1000 addresses to trigger rhltable growth and shrinking
- Unconnected UDP sendmsg stress to exercise the __ip_dev_find()
lookup hot path (each sendto triggers a hash table lookup)
- Duplicate key test: same IP on two different interfaces
- Address lifetime expiry via check_lifetime() work function
- Ping-based lookup verification from sampled addresses
The test uses network namespaces and veth pairs to avoid polluting the
host. A C helper (ipv4_addr_lookup_udp_sender) pre-creates sockets
during setup for low-noise measurement with per-round statistics.
Optional bpftrace integration (--bpftrace, --bpftrace-debug) provides
latency histograms and resize event tracing for A/B kernel comparison.
A virtme-ng wrapper script is included for isolated VM testing.
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
---
tools/testing/selftests/net/Makefile | 4 +
.../selftests/net/ipv4_addr_lookup_test.sh | 804 ++++++++++++++++++
.../net/ipv4_addr_lookup_test_virtme.sh | 282 ++++++
.../selftests/net/ipv4_addr_lookup_trace.bt | 178 ++++
.../net/ipv4_addr_lookup_udp_sender.c | 401 +++++++++
5 files changed, 1669 insertions(+)
create mode 100755 tools/testing/selftests/net/ipv4_addr_lookup_test.sh
create mode 100755 tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
create mode 100644 tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
create mode 100644 tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6bced3ed798b..1724d1478020 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -42,6 +42,7 @@ TEST_PROGS := \
gre_ipv6_lladdr.sh \
icmp.sh \
icmp_redirect.sh \
+ ipv4_addr_lookup_test.sh \
io_uring_zerocopy_tx.sh \
ioam6.sh \
ip6_gre_headroom.sh \
@@ -127,6 +128,8 @@ TEST_PROGS := \
# end of TEST_PROGS
TEST_PROGS_EXTENDED := \
+ ipv4_addr_lookup_test_virtme.sh \
+ ipv4_addr_lookup_trace.bt \
xfrm_policy_add_speed.sh \
# end of TEST_PROGS_EXTENDED
@@ -135,6 +138,7 @@ TEST_GEN_FILES := \
cmsg_sender \
fin_ack_lat \
hwtstamp_config \
+ ipv4_addr_lookup_udp_sender \
io_uring_zerocopy_tx \
ioam6_parser \
ip_defrag \
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_test.sh b/tools/testing/selftests/net/ipv4_addr_lookup_test.sh
new file mode 100755
index 000000000000..df9924e165af
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_test.sh
@@ -0,0 +1,804 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Stress test for IPv4 address hash table (inet_addr_lst / rhltable).
+#
+# Exercises the rhltable insert, lookup, and remove paths by:
+# 1. Adding many IPv4 addresses (triggers rhltable growth/resizing)
+# 2. Sending unconnected UDP to exercise the __ip_dev_find lookup hot path
+# 3. Removing all addresses (triggers rhltable shrinking)
+# 4. Testing duplicate keys (same IP on different devices)
+#
+# Uses veth pairs in network namespaces to avoid polluting the host.
+#
+# Options:
+# --num-addrs N Number of addresses to add (default: 1000)
+# --rounds N Measurement rounds for UDP benchmark (default: 10)
+# --duration S Seconds per measurement round (default: 3)
+# --bench-only Only run the UDP sendmsg benchmark (skip other tests)
+# --sink Use C receiver to count packets (adds CPU overhead)
+# --threaded-napi Move veth RX to separate CPU (cleaner perf profiles)
+# --verbose Show detailed output
+# --help Show usage
+
+source "$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")/lib.sh"
+
+NUM_ADDRS=1000
+ROUNDS=10
+DURATION=3
+BENCH_ONLY=0
+VERBOSE=0
+USE_BPFTRACE=0
+BPFTRACE_DEBUG=0
+USE_SINK=0
+USE_THREADED_NAPI=0
+RET=0
+BPFTRACE_PID=0
+BPFTRACE_LOG=""
+
+usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo " --num-addrs N Number of IPv4 addresses to add (default: $NUM_ADDRS)"
+ echo " --rounds N Measurement rounds for benchmark (default: $ROUNDS)"
+ echo " --duration S Seconds per measurement round (default: $DURATION)"
+ echo " --bench-only Only run the UDP sendmsg benchmark"
+ echo " --verbose Show detailed output"
+ echo " --bpftrace Trace __ip_dev_find latency (minimal overhead for A/B)"
+ echo " --sink Use C receiver to count packets (adds CPU overhead)"
+ echo " --threaded-napi Move veth RX to separate CPU (cleaner perf profiles)"
+ echo " --bpftrace-debug Trace all code paths (lookup, insert, remove, resize)"
+ exit 0
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --num-addrs) NUM_ADDRS="$2"; shift 2 ;;
+ --rounds) ROUNDS="$2"; shift 2 ;;
+ --duration) DURATION="$2"; shift 2 ;;
+ --bench-only) BENCH_ONLY=1; shift ;;
+ --verbose) VERBOSE=1; shift ;;
+ --bpftrace) USE_BPFTRACE=1; shift ;;
+ --sink) USE_SINK=1; shift ;;
+ --threaded-napi) USE_THREADED_NAPI=1; shift ;;
+ --bpftrace-debug) USE_BPFTRACE=1; BPFTRACE_DEBUG=1; shift ;;
+ --help) usage ;;
+ *) echo "Unknown option: $1"; usage ;;
+ esac
+done
+
+log() {
+ [ "$VERBOSE" -eq 1 ] && echo " $*"
+}
+
+log_config() {
+ echo " Config: $*"
+}
+
+PASS=0
+FAIL=0
+
+# ---------------------------------------------------------------------------
+# bpftrace helpers
+# ---------------------------------------------------------------------------
+
+BT_SCRIPT_GEN=""
+
+# Check if a kernel function is actually kprobe-able (not notrace)
+can_kprobe() {
+ local f="$1"
+ # available_filter_functions lists what kprobes can actually attach to
+ local aff
+ for aff in /sys/kernel/tracing/available_filter_functions \
+ /sys/kernel/debug/tracing/available_filter_functions; do
+ [ -r "$aff" ] && { grep -qw "$f" "$aff" 2>/dev/null; return; }
+ done
+ # Fallback: check kallsyms (may include notrace functions)
+ grep -q "^[0-9a-f]* [tT] ${f}$" /proc/kallsyms 2>/dev/null
+}
+
+# Build bpftrace script dynamically based on available symbols.
+# Sets NPROBES and writes to BT_SCRIPT_GEN (must be set before calling).
+bpftrace_build_script() {
+ NPROBES=0
+
+ # Resolve bucket_table_alloc (may have .isra.0 suffix from GCC)
+ local bta_sym=""
+ local aff
+ for aff in /sys/kernel/tracing/available_filter_functions \
+ /sys/kernel/debug/tracing/available_filter_functions; do
+ [ -r "$aff" ] && {
+ bta_sym=$(grep -oP 'bucket_table_alloc\S*' "$aff" 2>/dev/null | head -1)
+ break
+ }
+ done
+ [ -z "$bta_sym" ] && \
+ bta_sym=$(grep -oP '(?<= )[tT] \K(bucket_table_alloc[.\w]*)' \
+ /proc/kallsyms 2>/dev/null | head -1)
+
+ # --- BEGIN block ---
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ cat > "$BT_SCRIPT_GEN" <<'BTEOF'
+BEGIN {
+ printf("Tracing inet_addr_lst rhltable paths (debug mode)...\n\n");
+ @ipdev_count = 0; @lookup_count = 0;
+ @insert_count = 0; @insert_slow = 0; @remove_count = 0;
+ @resize_events = 0; @bucket_allocs = 0; @rehash_count = 0;
+ @tbl_size = 0; @tbl_resizes = 0;
+}
+BTEOF
+ else
+ cat > "$BT_SCRIPT_GEN" <<'BTEOF'
+BEGIN {
+ printf("Tracing inet_addr_lst rhltable paths...\n\n");
+ @ipdev_count = 0;
+}
+BTEOF
+ fi
+
+ # Detect old (hlist) vs new (rhltable) kernel:
+ # old kernel: inet_hash_insert does hlist hash+insert, visible to kprobe
+ # new kernel: inet_hash_insert wraps rhltable_insert, inlined away
+ local has_rhltable=0
+ if can_kprobe inet_hash_insert; then
+ log " detected OLD kernel (inet_hash_insert is kprobe-able)"
+ else
+ has_rhltable=1
+ log " detected NEW kernel (inet_hash_insert inlined -> rhltable)"
+ fi
+
+ # --- Core probe: __ip_dev_find (always, minimal overhead for A/B) ---
+ if can_kprobe __ip_dev_find; then
+ log " probe: __ip_dev_find (full lookup)"
+ if [ "$BPFTRACE_DEBUG" -eq 1 ] && [ "$has_rhltable" -eq 1 ]; then
+ # New kernel: read rhltable bucket count via BTF to detect resize
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:__ip_dev_find {
+ @ipdev_entry[tid] = nsecs;
+ $net = (struct net *)arg0;
+ $tbl = $net->ipv4.inet_addr_lst.ht.tbl;
+ $size = $tbl->size;
+ if ($size != @tbl_size) {
+ printf("TABLE RESIZE: buckets %lld -> %d (nelems=%d)\n",
+ @tbl_size, $size, $net->ipv4.inet_addr_lst.ht.nelems.counter);
+ @tbl_size = $size;
+ @tbl_resizes++;
+ }
+}
+BTEOF
+ else
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:__ip_dev_find { @ipdev_entry[tid] = nsecs; }
+BTEOF
+ fi
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kretprobe:__ip_dev_find /@ipdev_entry[tid]/ {
+ $dt = nsecs - @ipdev_entry[tid];
+ @ipdev_ns = hist($dt); @ipdev_stats = stats($dt); @ipdev_count++;
+ delete(@ipdev_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ # --- Debug probes (only with --bpftrace-debug) ---
+ local has_lookup=0 has_resize_wq=0 has_bta=0 has_rehash=0
+
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ log " debug mode: attaching extra probes"
+
+ if can_kprobe inet_lookup_ifaddr_rcu; then
+ has_lookup=1
+ log " probe: inet_lookup_ifaddr_rcu (inner lookup)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_lookup_ifaddr_rcu { @lookup_entry[tid] = nsecs; }
+kretprobe:inet_lookup_ifaddr_rcu /@lookup_entry[tid]/ {
+ $dt = nsecs - @lookup_entry[tid];
+ @lookup_ns = hist($dt); @lookup_stats = stats($dt); @lookup_count++;
+ delete(@lookup_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe inet_hash_insert; then
+ log " probe: inet_hash_insert (old kernel insert path)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_hash_insert { @insert_count++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rhashtable_insert_slow; then
+ log " probe: rhashtable_insert_slow (insert slow path)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rhashtable_insert_slow { @insert_slow++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe inet_hash_remove; then
+ log " probe: inet_hash_remove (remove)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_hash_remove { @remove_count++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rht_deferred_worker; then
+ has_resize_wq=1
+ log " probe: rht_deferred_worker (resize worker)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rht_deferred_worker {
+ @resize_wq_entry[tid] = nsecs; @resize_events++;
+ printf(">>> RESIZE #%lld: deferred_worker started\n", @resize_events);
+}
+kretprobe:rht_deferred_worker /@resize_wq_entry[tid]/ {
+ $dt = nsecs - @resize_wq_entry[tid];
+ @resize_wq_ns = hist($dt);
+ printf(" RESIZE: done in %lld us\n", $dt / 1000);
+ delete(@resize_wq_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if [ -n "$bta_sym" ] && can_kprobe "$bta_sym"; then
+ has_bta=1
+ log " probe: $bta_sym (table alloc, arg1=nbuckets)"
+ cat >> "$BT_SCRIPT_GEN" <<BTEOF
+kprobe:${bta_sym} {
+ @bucket_allocs++; @last_alloc_size = arg1;
+ printf(" RESIZE: bucket_table_alloc nbuckets=%lld\\n", arg1);
+ print(kstack(5));
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rhashtable_rehash_table; then
+ has_rehash=1
+ log " probe: rhashtable_rehash_table (data migration)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rhashtable_rehash_table { @rehash_entry[tid] = nsecs; }
+kretprobe:rhashtable_rehash_table /@rehash_entry[tid]/ {
+ $dt = nsecs - @rehash_entry[tid];
+ @rehash_ns = hist($dt); @rehash_count++;
+ printf(" RESIZE: rehash done in %lld us\n", $dt / 1000);
+ delete(@rehash_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+ fi
+
+ # --- END block -- only reference maps that actually exist ---
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+END {
+ printf("\n========================================================\n");
+ printf(" inet_addr_lst rhltable trace summary\n");
+ printf("========================================================\n\n");
+ printf("--- __ip_dev_find latency (ns) ---\n");
+ print(@ipdev_ns);
+ printf(" stats (count/avg/total): "); print(@ipdev_stats);
+ printf("\nCOMPARISON: __ip_dev_find calls=%lld\n", @ipdev_count);
+BTEOF
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ if [ "$has_rhltable" -eq 1 ]; then
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rhltable state (via BTF struct reads) ---\n");
+ printf(" kernel type : rhltable (new)\n");
+ printf(" final bucket count : %8lld\n", @tbl_size);
+ printf(" resize events observed : %8lld\n", @tbl_resizes);
+BTEOF
+ else
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- hash table type ---\n");
+ printf(" kernel type : hlist (old)\n");
+BTEOF
+ fi
+ [ "$has_lookup" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- inet_lookup_ifaddr_rcu latency (ns) ---\n");
+ print(@lookup_ns);
+ printf(" stats (count/avg/total): "); print(@lookup_stats);
+ printf("COMPARISON: inet_lookup_ifaddr_rcu calls=%lld\n", @lookup_count);
+ clear(@lookup_entry);
+BTEOF
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- Debug call counts ---\n");
+ printf(" inet_hash_insert : %8lld\n", @insert_count);
+ printf(" rhashtable_insert_slow : %8lld\n", @insert_slow);
+ printf(" inet_hash_remove : %8lld\n", @remove_count);
+ printf(" rht_deferred_worker : %8lld\n", @resize_events);
+ printf(" bucket_table_alloc : %8lld\n", @bucket_allocs);
+BTEOF
+ [ "$has_rehash" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf(" rhashtable_rehash : %8lld\n", @rehash_count);
+BTEOF
+ [ "$has_resize_wq" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rht_deferred_worker duration (ns) ---\n");
+ print(@resize_wq_ns);
+ clear(@resize_wq_entry);
+BTEOF
+ [ "$has_rehash" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rhashtable_rehash_table duration (ns) ---\n");
+ print(@rehash_ns);
+ clear(@rehash_entry);
+BTEOF
+ [ "$has_bta" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ clear(@last_alloc_size);
+BTEOF
+ fi
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ clear(@ipdev_entry);
+}
+BTEOF
+}
+
+bpftrace_start() {
+ [ "$USE_BPFTRACE" -eq 0 ] && return
+
+ if ! command -v bpftrace >/dev/null 2>&1; then
+ echo "WARN: bpftrace not found, skipping tracing"
+ USE_BPFTRACE=0
+ return
+ fi
+
+ BT_SCRIPT_GEN=$(mktemp /tmp/rhltable_trace_XXXXXX.bt)
+ echo "Probing /proc/kallsyms for available trace points..."
+ bpftrace_build_script
+
+ if [ "$NPROBES" -eq 0 ]; then
+ echo "WARN: no kprobe-able symbols found, skipping tracing"
+ USE_BPFTRACE=0
+ rm -f "$BT_SCRIPT_GEN"
+ return
+ fi
+ echo "Built dynamic bpftrace script with $NPROBES probe groups"
+ log "Script: $BT_SCRIPT_GEN"
+
+ BPFTRACE_LOG=$(mktemp /tmp/rhltable_trace.XXXXXX)
+ bpftrace "$BT_SCRIPT_GEN" > "$BPFTRACE_LOG" 2>&1 &
+ BPFTRACE_PID=$!
+ # Give bpftrace time to attach probes
+ sleep 2
+ if ! kill -0 $BPFTRACE_PID 2>/dev/null; then
+ echo "WARN: bpftrace failed to start"
+ cat "$BPFTRACE_LOG"
+ USE_BPFTRACE=0
+ rm -f "$BT_SCRIPT_GEN"
+ return
+ fi
+ echo "bpftrace attached (pid $BPFTRACE_PID)"
+}
+
+bpftrace_stop() {
+ [ "$USE_BPFTRACE" -eq 0 ] && return
+ [ "$BPFTRACE_PID" -eq 0 ] && return
+
+ # Send INT so bpftrace prints its END summary
+ kill -INT $BPFTRACE_PID 2>/dev/null || true
+ wait $BPFTRACE_PID 2>/dev/null || true
+ BPFTRACE_PID=0
+
+ echo ""
+ echo "============================================"
+ echo "bpftrace output"
+ echo "============================================"
+ cat "$BPFTRACE_LOG"
+ echo ""
+
+ # Validate expected code paths were hit
+ local rc=0
+ if grep -q '__ip_dev_find calls=0' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "FAIL: __ip_dev_find was never called"
+ rc=1
+ elif grep -q 'COMPARISON: __ip_dev_find' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: __ip_dev_find lookup path verified"
+ fi
+ if grep -q 'TABLE RESIZE:' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: rhltable resize detected (BTF struct reads)"
+ elif grep -q 'RESIZE.*bucket_table_alloc' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: rhltable resize detected (kprobe)"
+ else
+ echo "INFO: no resize observed (use --bpftrace-debug to detect via BTF)"
+ fi
+ check_result "bpftrace code path verification" $rc
+
+ rm -f "$BPFTRACE_LOG" "$BT_SCRIPT_GEN"
+}
+
+check_result() {
+ local desc="$1"
+ local rc="$2"
+
+ if [ "$rc" -eq 0 ]; then
+ echo "PASS: $desc"
+ PASS=$((PASS + 1))
+ else
+ echo "FAIL: $desc"
+ FAIL=$((FAIL + 1))
+ RET=1
+ fi
+}
+
+cleanup() {
+ # Stop bpftrace if running
+ if [ "$BPFTRACE_PID" -ne 0 ]; then
+ kill -INT $BPFTRACE_PID 2>/dev/null || true
+ wait $BPFTRACE_PID 2>/dev/null || true
+ BPFTRACE_PID=0
+ fi
+
+ # Kill any other background jobs
+ local jobs
+ jobs="$(jobs -p 2>/dev/null)" || true
+ [ -n "$jobs" ] && kill $jobs 2>/dev/null || true
+ wait 2>/dev/null || true
+
+ cleanup_all_ns
+ [ -n "$BPFTRACE_LOG" ] && rm -f "$BPFTRACE_LOG"
+}
+
+trap cleanup EXIT
+
+# Helper: generate address from index (spreads across octets to avoid /24 limits)
+# Returns 10.B2.B3.1 where B2.B3 encodes the index
+idx_to_addr() {
+ local i=$1
+ local b2=$(( (i >> 8) & 0xff ))
+ local b3=$(( i & 0xff ))
+ echo "10.${b2}.${b3}.1"
+}
+
+# ---------------------------------------------------------------------------
+# Setup
+# ---------------------------------------------------------------------------
+
+setup() {
+ if ! setup_ns NS_SRC NS_DST; then
+ echo "SKIP: Could not create namespaces"
+ exit $ksft_skip
+ fi
+
+ # Create veth pair
+ ip link add veth_src type veth peer name veth_dst
+ ip link set veth_src netns "$NS_SRC"
+ ip link set veth_dst netns "$NS_DST"
+ ip -n "$NS_SRC" link set veth_src up
+ ip -n "$NS_DST" link set veth_dst up
+
+ if [ "$USE_THREADED_NAPI" -eq 1 ]; then
+ # Move veth RX to a separate NAPI kthread for cleaner perf profiles.
+ # Disable TSO on src so packets travel individually through the
+ # veth ptr_ring (256 entries), enable GRO on dst for NAPI polling.
+ ip netns exec "$NS_SRC" ethtool -K veth_src tso off 2>/dev/null || true
+ ip netns exec "$NS_DST" ethtool -K veth_dst gro on 2>/dev/null || true
+ ip netns exec "$NS_DST" \
+ bash -c 'echo 1 > /sys/class/net/veth_dst/threaded' 2>/dev/null || true
+ log_config "threaded-napi: veth_dst (TSO off, GRO on, NAPI kthread on CPU 0)"
+ fi
+
+ # Base addresses for connectivity
+ ip -n "$NS_SRC" addr add 192.168.1.1/24 dev veth_src
+ ip -n "$NS_DST" addr add 192.168.1.2/24 dev veth_dst
+
+ # Accept packets from any source on dst side
+ ip netns exec "$NS_DST" sysctl -wq net.ipv4.conf.all.rp_filter=0
+ ip netns exec "$NS_DST" sysctl -wq net.ipv4.conf.veth_dst.rp_filter=0
+
+ # Route the 10.0.0.0/8 range toward veth_src from dst side
+ ip -n "$NS_DST" route add 10.0.0.0/8 via 192.168.1.1
+
+ log "Namespaces: NS_SRC=$NS_SRC NS_DST=$NS_DST"
+}
+
+# ---------------------------------------------------------------------------
+# Test 1: Add many addresses (rhltable insert + resize)
+# ---------------------------------------------------------------------------
+
+test_add_many_addrs() {
+ local i addr
+ local rc=0
+
+ echo "Test: Adding $NUM_ADDRS addresses..."
+ local batch
+ batch=$(mktemp /tmp/ip_batch_add.XXXXXX)
+ for ((i = 1; i <= NUM_ADDRS; i++)); do
+ echo "addr add 10.$(( (i >> 8) & 0xff )).$(( i & 0xff )).1/32 dev veth_src"
+ done > "$batch"
+ ip -n "$NS_SRC" -batch "$batch" 2>/dev/null || true
+ rm -f "$batch"
+
+ # Verify address count
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "inet " || true)
+ log "Addresses on veth_src: $count (expected $((NUM_ADDRS + 1)))"
+
+ [ "$count" -ge "$NUM_ADDRS" ] || rc=1
+ check_result "add $NUM_ADDRS addresses" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 2: Verify lookup works (ping from specific source addresses)
+# ---------------------------------------------------------------------------
+
+test_lookup_ping() {
+ local rc=0
+
+ echo "Test: Verify address lookup via ping..."
+ # Ping dst from a few of the added addresses
+ for idx in 1 100 $((NUM_ADDRS / 2)) $NUM_ADDRS; do
+ [ "$idx" -gt "$NUM_ADDRS" ] && continue
+ local addr
+ addr=$(idx_to_addr $idx)
+ if ! ip netns exec "$NS_SRC" ping -c 1 -W 1 -I "$addr" 192.168.1.2 \
+ >/dev/null 2>&1; then
+ log "ping from $addr failed"
+ rc=1
+ else
+ log "ping from $addr OK"
+ fi
+ done
+
+ check_result "address lookup via ping" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 3: Unconnected UDP sendmsg stress (exercises __ip_dev_find hot path)
+# ---------------------------------------------------------------------------
+
+test_udp_sendmsg_stress() {
+ local rc=0
+
+ local total_time=$((ROUNDS * DURATION + 1))
+ echo "Test: UDP sendmsg bench ($NUM_ADDRS addrs, ${ROUNDS}x${DURATION}s + 1s warmup = ~${total_time}s)..."
+
+ # Locate C binary (used for both sink and sender)
+ local sender_bin=""
+ local script_dir
+ script_dir=$(dirname "$0")
+
+ if [ -x "${script_dir}/ipv4_addr_lookup_udp_sender" ]; then
+ sender_bin="${script_dir}/ipv4_addr_lookup_udp_sender"
+ elif gcc -O2 -Wall -o /tmp/udp_sender \
+ "${script_dir}/ipv4_addr_lookup_udp_sender.c" 2>/dev/null; then
+ sender_bin="/tmp/udp_sender"
+ else
+ echo "SKIP: ipv4_addr_lookup_udp_sender not found (run make first)"
+ check_result "UDP sender binary available" 1
+ return
+ fi
+
+ local sink_pid=0 sink_log=""
+
+ if [ "$USE_SINK" -eq 1 ]; then
+ # C receiver counts packets (adds CPU overhead to perf profiles)
+ log_config "sink: C receiver on CPU 0 (verifies packet counts)"
+ sink_log=$(mktemp /tmp/udp_sink.XXXXXX)
+ ip netns exec "$NS_DST" \
+ taskset -c 0 "$sender_bin" --sink > "$sink_log" 2>&1 &
+ sink_pid=$!
+ sleep 0.2
+ else
+ # Default: iptables DROP -- zero userspace overhead in perf profiles
+ ip netns exec "$NS_DST" \
+ iptables -A INPUT -p udp --dport 9000 -j DROP
+ fi
+
+ if [ "$USE_THREADED_NAPI" -eq 1 ]; then
+ # Pin veth_dst NAPI kthread to CPU 0 (sender is on CPU 1)
+ local napi_pid
+ napi_pid=$(pgrep -f "napi/veth_dst" 2>/dev/null | head -1)
+ if [ -n "$napi_pid" ]; then
+ taskset -p 0x1 "$napi_pid" >/dev/null 2>&1 || true
+ log "Pinned NAPI thread (pid $napi_pid) to CPU 0"
+ fi
+ fi
+
+ # Snapshot softnet_stat before sending (per-CPU: processed, time_squeeze)
+ local softnet_before
+ softnet_before=$(mktemp /tmp/softnet_before.XXXXXX)
+ cat /proc/net/softnet_stat > "$softnet_before"
+
+ # Send unconnected UDP from many source addresses.
+ # Each sendto() triggers ip_route_output -> __ip_dev_find -> rhltable_lookup.
+ local sender_log
+ sender_log=$(mktemp /tmp/udp_sender.XXXXXX)
+
+ log "Using C UDP sender (pre-created sockets, $ROUNDS rounds)"
+ local sndbuf_arg=""
+ [ "$USE_THREADED_NAPI" -eq 1 ] && sndbuf_arg="--sndbuf 4194304"
+
+ ip netns exec "$NS_SRC" \
+ taskset -c 1 "$sender_bin" "$NUM_ADDRS" "$ROUNDS" "$DURATION" $sndbuf_arg \
+ 2>&1 | tee "$sender_log"
+ [ "${PIPESTATUS[0]}" -ne 0 ] && rc=1
+
+ # Show per-CPU softnet activity (detect same-CPU vs multi-CPU NAPI)
+ local cpu=0 active_cpus=""
+ while read -r line; do
+ # shellcheck disable=SC2086
+ set -- $line
+ local cur_p=$((0x${1})) cur_sq=$((0x${3}))
+ local prev_p=0 prev_sq=0
+ if [ -n "$softnet_before" ]; then
+ local prev_line
+ prev_line=$(sed -n "$((cpu + 1))p" "$softnet_before")
+ if [ -n "$prev_line" ]; then
+ # shellcheck disable=SC2086
+ set -- $prev_line
+ prev_p=$((0x${1})); prev_sq=$((0x${3}))
+ fi
+ fi
+ local dp=$((cur_p - prev_p))
+ [ "$dp" -gt 0 ] && active_cpus="${active_cpus} cpu${cpu}(+${dp})"
+ cpu=$((cpu + 1))
+ done < /proc/net/softnet_stat
+ rm -f "$softnet_before"
+ local n_active
+ n_active=$(echo "$active_cpus" | wc -w)
+ local cpu_mode="single-CPU"
+ [ "$n_active" -gt 1 ] && cpu_mode="multi-CPU(${n_active})"
+ echo " softnet: ${cpu_mode}:${active_cpus}"
+
+ [ "$sender_bin" = "/tmp/udp_sender" ] && rm -f "$sender_bin"
+
+ if [ "$USE_SINK" -eq 1 ] && [ "$sink_pid" -ne 0 ]; then
+ # Let last packets reach socket buffer, then stop the sink
+ sleep 0.1
+ kill -TERM $sink_pid 2>/dev/null || true
+ wait $sink_pid 2>/dev/null || true
+
+ # Verify no packet drops: sent (includes warmup) should equal received
+ local total_sent sink_received
+ total_sent=$(sed -n 's/.*sent=\([0-9]*\).*/\1/p' "$sender_log" | head -1)
+ sink_received=$(sed -n 's/.*received=\([0-9]*\).*/\1/p' "$sink_log" | head -1)
+ rm -f "$sink_log"
+
+ if [ -n "$total_sent" ] && [ -n "$sink_received" ]; then
+ if [ "$total_sent" -eq "$sink_received" ]; then
+ echo " Sink received: $sink_received (matches sent)"
+ else
+ local diff=$((total_sent - sink_received))
+ echo " WARN: sent=$total_sent but sink received=$sink_received (diff=$diff)"
+ fi
+ fi
+ else
+ ip netns exec "$NS_DST" \
+ iptables -D INPUT -p udp --dport 9000 -j DROP 2>/dev/null
+ fi
+ rm -f "$sender_log"
+
+ check_result "unconnected UDP sendmsg stress" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 4: Duplicate keys (same IP on two different veth devices)
+# ---------------------------------------------------------------------------
+
+test_duplicate_addrs() {
+ local rc=0
+
+ echo "Test: Duplicate address keys (same IP, different devices)..."
+
+ # Create a second veth pair in NS_SRC
+ ip link add veth_src2 type veth peer name veth_dup
+ ip link set veth_src2 netns "$NS_SRC" up
+ ip link set veth_dup netns "$NS_DST" up
+ ip -n "$NS_DST" link set veth_dup up
+
+ # Add the same address that's already on veth_src
+ local dup_addr
+ dup_addr=$(idx_to_addr 1)
+ ip -n "$NS_SRC" addr add "${dup_addr}/32" dev veth_src2 2>/dev/null || true
+
+ # Verify both devices have the address
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show | grep -c "$dup_addr" || true)
+ log "Address $dup_addr appears on $count devices"
+
+ [ "$count" -ge 2 ] || rc=1
+
+ # Lookup should still work
+ if ! ip netns exec "$NS_SRC" ping -c 1 -W 1 -I "$dup_addr" 192.168.1.2 \
+ >/dev/null 2>&1; then
+ log "ping from duplicate addr failed (expected -- routing may prefer one)"
+ fi
+
+ # Remove duplicate and verify no crash
+ ip -n "$NS_SRC" addr del "${dup_addr}/32" dev veth_src2 2>/dev/null || true
+ ip -n "$NS_SRC" link del veth_src2 2>/dev/null || true
+
+ check_result "duplicate address keys" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 5: Remove all addresses (rhltable shrink)
+# ---------------------------------------------------------------------------
+
+test_remove_all_addrs() {
+ local i addr
+ local rc=0
+
+ echo "Test: Removing $NUM_ADDRS addresses..."
+ local batch
+ batch=$(mktemp /tmp/ip_batch_del.XXXXXX)
+ for ((i = 1; i <= NUM_ADDRS; i++)); do
+ echo "addr del 10.$(( (i >> 8) & 0xff )).$(( i & 0xff )).1/32 dev veth_src"
+ done > "$batch"
+ ip -n "$NS_SRC" -batch "$batch" 2>/dev/null || true
+ rm -f "$batch"
+
+ # Verify only the base address remains
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "inet " || true)
+ log "Addresses remaining: $count (expected 1)"
+
+ [ "$count" -eq 1 ] || rc=1
+ check_result "remove all addresses (rhltable shrink)" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 6: Re-add and check address lifetime (exercises check_lifetime)
+# ---------------------------------------------------------------------------
+
+test_addr_lifetime() {
+ local rc=0
+
+ echo "Test: Address lifetime expiry..."
+
+ # Add an address with short valid/preferred lifetime
+ ip -n "$NS_SRC" addr add 10.99.99.1/32 dev veth_src \
+ valid_lft 3 preferred_lft 2
+
+ # Verify it exists
+ local exists
+ exists=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "10.99.99.1" || true)
+ [ "$exists" -ge 1 ] || { rc=1; check_result "address lifetime" $rc; return; }
+
+ log "Address 10.99.99.1 added with valid_lft=3s"
+
+ # Wait for it to expire (check_lifetime runs periodically)
+ sleep 5
+
+ exists=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "10.99.99.1" || true)
+ log "After 5s: addr present=$exists (expected 0)"
+
+ [ "$exists" -eq 0 ] || rc=1
+ check_result "address lifetime expiry" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+echo "============================================"
+echo "inet_addr_lst rhltable stress test"
+echo " addresses: $NUM_ADDRS"
+echo " rounds: $ROUNDS x ${DURATION}s"
+[ "$BENCH_ONLY" -eq 1 ] && echo " mode: bench-only"
+echo "============================================"
+
+setup
+bpftrace_start
+
+if [ "$BENCH_ONLY" -eq 1 ]; then
+ test_add_many_addrs
+ test_udp_sendmsg_stress
+else
+ test_add_many_addrs
+ test_lookup_ping
+ test_udp_sendmsg_stress
+ test_duplicate_addrs
+ test_remove_all_addrs
+ test_addr_lifetime
+fi
+
+bpftrace_stop
+
+echo ""
+echo "============================================"
+echo "Results: $PASS passed, $FAIL failed"
+echo "============================================"
+
+exit $RET
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh b/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
new file mode 100755
index 000000000000..4d308b3e5346
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
@@ -0,0 +1,282 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Launch ipv4_addr_lookup stress test inside virtme-ng
+#
+# Must be run from the kernel build tree root.
+#
+# Options:
+# --verbose Show kernel console (vng boot messages) in real time.
+# --taskset CPUS Pin the VM to specific CPUs via taskset.
+# Example: --taskset 12-19 (pin to E-cores on i7-12800H)
+# --isolated Run VM in bench.slice cgroup (proper CPU isolation).
+# --no-turbo Disable turbo boost for stable CPU frequency.
+# --freq MHZ Pin CPU frequency on bench CPUs (e.g. --freq 1200).
+# Sets scaling_min_freq=scaling_max_freq for thermal stability.
+# All other options are forwarded to ipv4_addr_lookup_test.sh (see --help).
+#
+# bench.slice setup (required for --isolated):
+# The --isolated option uses a dedicated cgroup slice to pin the VM to
+# specific CPUs while keeping other system processes off those CPUs.
+# The script also sets cpuset.cpus.partition=isolated at runtime to
+# remove bench CPUs from the scheduler's load balancing domain
+# (similar to isolcpus= but reversible). Restored on exit.
+#
+# One-time setup (as root, adjust CPU range for your system):
+#
+# # Create the slice (example: reserve CPUs 12-19 for benchmarks)
+# systemctl set-property --runtime bench.slice AllowedCPUs=12-19
+#
+# # Confine everything else to the remaining CPUs
+# systemctl set-property --runtime user.slice AllowedCPUs=0-11
+# systemctl set-property --runtime system.slice AllowedCPUs=0-11
+# systemctl set-property --runtime init.scope AllowedCPUs=0-11
+#
+# To make persistent, drop the --runtime flag (writes to /etc/systemd).
+#
+# Examples (run from kernel tree root):
+# ./tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
+# --num-addrs 1000 --duration 10
+# --verbose --num-addrs 2000
+# --taskset 12-19 --num-addrs 10000 # pinned to E-cores
+# --isolated --num-addrs 10000 # proper cgroup isolation
+
+set -eu
+
+# Parse options consumed here (not forwarded to the inner test).
+VERBOSE=""
+TASKSET_CPUS=""
+BENCH_SLICE=0
+NO_TURBO=0
+PIN_FREQ_KHZ=0
+INNER_ARGS=()
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --verbose) VERBOSE="--verbose"; INNER_ARGS+=("--verbose"); shift ;;
+ --taskset) TASKSET_CPUS="$2"; shift 2 ;;
+ --isolated) BENCH_SLICE=1; shift ;;
+ --no-turbo) NO_TURBO=1; shift ;;
+ --freq) PIN_FREQ_KHZ=$(( $2 * 1000 )); shift 2 ;;
+ *) INNER_ARGS+=("$1"); shift ;;
+ esac
+done
+TEST_ARGS=""
+[ ${#INNER_ARGS[@]} -gt 0 ] && TEST_ARGS=$(printf '%q ' "${INNER_ARGS[@]}")
+
+if [ ! -f "vmlinux" ]; then
+ echo "ERROR: virtme-ng needs vmlinux; run from a compiled kernel tree:" >&2
+ echo " cd /path/to/kernel && $0" >&2
+ exit 1
+fi
+
+# Verify .config has the options needed for virtme-ng and this test.
+KCONFIG=".config"
+if [ ! -f "$KCONFIG" ]; then
+ echo "ERROR: No .config found -- build the kernel first" >&2
+ exit 1
+fi
+
+MISSING=""
+for opt in CONFIG_VIRTIO CONFIG_VIRTIO_PCI CONFIG_VIRTIO_NET \
+ CONFIG_VIRTIO_CONSOLE CONFIG_NET_9P CONFIG_NET_9P_VIRTIO \
+ CONFIG_9P_FS CONFIG_VETH CONFIG_IP_MULTIPLE_TABLES; do
+ if ! grep -q "^${opt}=[ym]" "$KCONFIG"; then
+ MISSING+=" $opt\n"
+ fi
+done
+if [ -n "$MISSING" ]; then
+ echo "ERROR: .config is missing options required by virtme-ng:" >&2
+ echo -e "$MISSING" >&2
+ echo "Consider: vng --kconfig (or make defconfig + enable above)" >&2
+ exit 1
+fi
+
+TESTDIR="tools/testing/selftests/net"
+TESTNAME="ipv4_addr_lookup_test.sh"
+LOGFILE="ipv4_addr_lookup_test.log"
+LOGPATH="$TESTDIR/$LOGFILE"
+CONSOLELOG="ipv4_addr_lookup_console.log"
+rm -f "$LOGPATH" "$CONSOLELOG"
+
+log_config() {
+ echo " Config: $*"
+}
+
+echo "Starting VM... test output in $LOGPATH, kernel console in $CONSOLELOG"
+
+# earlycon on COM2 for reliable kernel console capture.
+SERIAL_CONSOLE="earlycon=uart8250,io,0x2f8,115200"
+SERIAL_CONSOLE+=" console=uart8250,io,0x2f8,115200"
+CPU_PIN_CMD=""
+if [ "$BENCH_SLICE" -eq 1 ]; then
+ # bench.slice + systemd overrides confine all other processes to CPUs 0-11.
+ # Move ourselves into bench.slice cgroup (user.slice blocks affinity to
+ # CPUs 12-19), then use taskset. vng needs a PTY so systemd-run --scope
+ # is not an option.
+ BENCH_CPUS=$(systemctl show bench.slice -p AllowedCPUs --value 2>/dev/null)
+ if [ -z "$BENCH_CPUS" ]; then
+ echo "ERROR: bench.slice cgroup not configured." >&2
+ echo "" >&2
+ echo "One-time setup (adjust CPU range for your system):" >&2
+ echo " sudo systemctl set-property --runtime bench.slice AllowedCPUs=12-19" >&2
+ echo " sudo systemctl set-property --runtime user.slice AllowedCPUs=0-11" >&2
+ echo " sudo systemctl set-property --runtime system.slice AllowedCPUs=0-11" >&2
+ echo " sudo systemctl set-property --runtime init.scope AllowedCPUs=0-11" >&2
+ echo "" >&2
+ echo "Or use --taskset CPUS for simple pinning without isolation." >&2
+ exit 1
+ fi
+ # Set partition to isolated: removes bench CPUs from scheduler load
+ # balancing (like isolcpus= but reversible). Restore in EXIT trap.
+ PARTITION_PATH="/sys/fs/cgroup/bench.slice/cpuset.cpus.partition"
+ ORIG_PARTITION=""
+ if [ -f "$PARTITION_PATH" ]; then
+ ORIG_PARTITION=$(cat "$PARTITION_PATH")
+ if [ "$ORIG_PARTITION" != "isolated" ]; then
+ echo isolated | sudo tee "$PARTITION_PATH" >/dev/null 2>&1 || true
+ fi
+ fi
+ log_config "bench.slice CPUs: $BENCH_CPUS (partition=isolated)"
+ echo $$ | sudo tee /sys/fs/cgroup/bench.slice/cgroup.procs >/dev/null
+ CPU_PIN_CMD="taskset -c $BENCH_CPUS"
+elif [ -n "$TASKSET_CPUS" ]; then
+ # Try taskset directly first. If it fails (e.g. user.slice excludes
+ # the requested CPUs), move into bench.slice and retry.
+ if ! taskset -cp "$TASKSET_CPUS" $$ >/dev/null 2>&1; then
+ if [ -d /sys/fs/cgroup/bench.slice ]; then
+ echo $$ | sudo tee /sys/fs/cgroup/bench.slice/cgroup.procs >/dev/null
+ log_config "moved into bench.slice to reach CPUs $TASKSET_CPUS"
+ else
+ echo "ERROR: taskset to CPUs $TASKSET_CPUS failed and no bench.slice available" >&2
+ exit 1
+ fi
+ fi
+ log_config "taskset CPUs: $TASKSET_CPUS"
+ CPU_PIN_CMD="taskset -c $TASKSET_CPUS"
+fi
+
+# Disable turbo boost for stable frequencies during benchmarks
+TURBO_RESTORED=0
+NO_TURBO_PATH="/sys/devices/system/cpu/intel_pstate/no_turbo"
+ORIG_FREQS=()
+cleanup() {
+ # Restore CPU frequencies
+ for entry in "${ORIG_FREQS[@]}"; do
+ local cpu="${entry%%:*}" freq="${entry#*:}"
+ echo "$freq" | sudo tee /sys/devices/system/cpu/cpu"$cpu"/cpufreq/scaling_max_freq >/dev/null 2>&1 || true
+ echo "$freq" | sudo tee /sys/devices/system/cpu/cpu"$cpu"/cpufreq/scaling_min_freq >/dev/null 2>&1 || true
+ done
+ # Restore turbo boost
+ if [ "$NO_TURBO" -eq 1 ] && [ -f "$NO_TURBO_PATH" ]; then
+ echo 0 | sudo tee "$NO_TURBO_PATH" >/dev/null 2>&1 || true
+ fi
+ # Restore cpuset partition
+ if [ -n "${ORIG_PARTITION:-}" ] && [ -f "${PARTITION_PATH:-}" ]; then
+ echo "$ORIG_PARTITION" | sudo tee "$PARTITION_PATH" >/dev/null 2>&1 || true
+ fi
+}
+trap cleanup EXIT
+
+if [ "$NO_TURBO" -eq 1 ]; then
+ if [ -f "$NO_TURBO_PATH" ]; then
+ echo 1 | sudo tee "$NO_TURBO_PATH" >/dev/null
+ log_config "turbo boost disabled (will restore on exit)"
+ else
+ echo "WARN: $NO_TURBO_PATH not found, cannot disable turbo" >&2
+ fi
+fi
+
+# Pin CPU frequency for thermal stability
+if [ "$PIN_FREQ_KHZ" -gt 0 ]; then
+ # Determine which CPUs to pin: bench.slice CPUs, --taskset CPUs, or all
+ if [ -n "${BENCH_CPUS:-}" ]; then
+ FREQ_CPUS="$BENCH_CPUS"
+ elif [ -n "$TASKSET_CPUS" ]; then
+ FREQ_CPUS="$TASKSET_CPUS"
+ else
+ echo "WARN: --freq without --isolated or --taskset, skipping" >&2
+ PIN_FREQ_KHZ=0
+ fi
+ if [ "$PIN_FREQ_KHZ" -gt 0 ]; then
+ # Expand CPU list (e.g. "12-15,18" -> "12 13 14 15 18")
+ FREQ_CPU_LIST=""
+ IFS=',' read -ra parts <<< "$FREQ_CPUS"
+ for part in "${parts[@]}"; do
+ if [[ "$part" == *-* ]]; then
+ IFS='-' read -r a b <<< "$part"
+ FREQ_CPU_LIST+=" $(seq "$a" "$b")"
+ else
+ FREQ_CPU_LIST+=" $part"
+ fi
+ done
+ PIN_FREQ_MHZ=$((PIN_FREQ_KHZ / 1000))
+ for cpu in $FREQ_CPU_LIST; do
+ freq_dir="/sys/devices/system/cpu/cpu${cpu}/cpufreq"
+ [ -d "$freq_dir" ] || continue
+ orig=$(cat "$freq_dir/scaling_max_freq" 2>/dev/null) || continue
+ ORIG_FREQS+=("${cpu}:${orig}")
+ echo "$PIN_FREQ_KHZ" | sudo tee "$freq_dir/scaling_max_freq" >/dev/null 2>&1 || true
+ echo "$PIN_FREQ_KHZ" | sudo tee "$freq_dir/scaling_min_freq" >/dev/null 2>&1 || true
+ done
+ log_config "CPU frequency pinned to ${PIN_FREQ_MHZ} MHz on CPUs: $FREQ_CPUS (will restore on exit)"
+ fi
+fi
+
+echo "(VM is booting, please wait ~30s)"
+set +e
+$CPU_PIN_CMD vng $VERBOSE --cpus 4 --memory 2G \
+ --rwdir "$TESTDIR" \
+ --append "panic=5 loglevel=4 $SERIAL_CONSOLE" \
+ --qemu-opts="-serial file:$CONSOLELOG" \
+ --exec "cd $TESTDIR && \
+ ./$TESTNAME $TEST_ARGS 2>&1 | \
+ tee $LOGFILE; echo EXIT_CODE=\$? >> $LOGFILE"
+VNG_RC=$?
+set -e
+
+echo ""
+if [ "$VNG_RC" -ne 0 ]; then
+ echo "***********************************************************"
+ echo "* VM CRASHED -- kernel panic or BUG_ON (vng rc=$VNG_RC)"
+ echo "***********************************************************"
+ if [ -s "$CONSOLELOG" ] && \
+ grep -qiE 'kernel BUG|BUG:|Oops:|panic|WARN' "$CONSOLELOG"; then
+ echo ""
+ echo "--- kernel backtrace ($CONSOLELOG) ---"
+ grep -iE -A30 'kernel BUG|BUG:|Oops:|panic|WARN' \
+ "$CONSOLELOG" | head -50
+ else
+ echo ""
+ echo "Re-run with --verbose to see the kernel backtrace:"
+ echo " $0 --verbose ${INNER_ARGS[*]:-}"
+ fi
+ exit 1
+elif [ ! -f "$LOGPATH" ]; then
+ echo "No log file found -- VM may have crashed before writing output"
+ exit 2
+else
+ echo "=== VM finished ==="
+fi
+
+# Show test results from the log
+echo ""
+if grep -q "^Results:" "$LOGPATH"; then
+ grep "^Results:" "$LOGPATH"
+fi
+grep -E "^(PASS|FAIL):" "$LOGPATH" || true
+
+# Scan console log for unexpected kernel warnings (even on clean exit)
+if [ -s "$CONSOLELOG" ]; then
+ WARN_PATTERN='kernel BUG|BUG:|Oops:|WARNING:|WARN_ON|rhashtable'
+ WARN_LINES=$(grep -cE "$WARN_PATTERN" "$CONSOLELOG" 2>/dev/null) || WARN_LINES=0
+ if [ "$WARN_LINES" -gt 0 ]; then
+ echo ""
+ echo "*** kernel warnings in $CONSOLELOG ($WARN_LINES lines) ***"
+ grep -E "$WARN_PATTERN" "$CONSOLELOG" | head -20
+ fi
+fi
+
+# Extract exit code from log
+if grep -q "^EXIT_CODE=" "$LOGPATH"; then
+ INNER_RC=$(grep "^EXIT_CODE=" "$LOGPATH" | tail -1 | cut -d= -f2)
+ exit "$INNER_RC"
+fi
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt b/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
new file mode 100644
index 000000000000..c63105faac03
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
@@ -0,0 +1,178 @@
+#!/usr/bin/env bpftrace
+/*
+ * ipv4_addr_lookup_trace.bt - Trace inet_addr_lst rhltable code paths
+ * SPDX-License-Identifier: GPL-2.0
+ *
+ * Run alongside ipv4_addr_lookup_test.sh to verify that the correct
+ * kernel functions are exercised and to capture per-call overhead.
+ *
+ * Traces:
+ * - inet_lookup_ifaddr_rcu : hot lookup (latency histogram)
+ * - __ip_dev_find : full lookup incl. FIB fallback
+ * - inet_hash_remove : hash remove path
+ * - rhashtable_insert_slow : slow-path insert (fast path is inline)
+ * - rht_deferred_worker : resize worker (expand / shrink)
+ * - bucket_table_alloc : new table allocation (reveals new size)
+ * - rhashtable_rehash_table : actual data migration between tables
+ *
+ * Usage:
+ * bpftrace ipv4_addr_lookup_trace.bt # in one terminal
+ * ./ipv4_addr_lookup_test.sh --num-addrs 500 # in another
+ * # Ctrl-C the bpftrace when test finishes
+ */
+
+BEGIN
+{
+ printf("Tracing inet_addr_lst rhltable paths... Ctrl-C to stop.\n\n");
+ @phase = "idle";
+}
+
+/* ------------------------------------------------------------------ */
+/* Hot lookup path: inet_lookup_ifaddr_rcu (called from __ip_dev_find) */
+/* ------------------------------------------------------------------ */
+
+kprobe:inet_lookup_ifaddr_rcu
+{
+ @lookup_entry[tid] = nsecs;
+}
+
+kretprobe:inet_lookup_ifaddr_rcu
+/@lookup_entry[tid]/
+{
+ $dt = nsecs - @lookup_entry[tid];
+ @lookup_ns = hist($dt);
+ @lookup_count++;
+ delete(@lookup_entry[tid]);
+}
+
+/* __ip_dev_find: full overhead including FIB fallback path */
+
+kprobe:__ip_dev_find
+{
+ @ipdev_entry[tid] = nsecs;
+}
+
+kretprobe:__ip_dev_find
+/@ipdev_entry[tid]/
+{
+ $dt = nsecs - @ipdev_entry[tid];
+ @ipdev_ns = hist($dt);
+ @ipdev_count++;
+ delete(@ipdev_entry[tid]);
+}
+
+/* ------------------------------------------------------------------ */
+/* Insert / Remove */
+/* ------------------------------------------------------------------ */
+
+/* rhashtable_insert_slow is the non-inline slow path called on insert */
+kprobe:rhashtable_insert_slow
+{
+ @insert_slow++;
+}
+
+/* inet_hash_remove is static but not inlined in this build */
+kprobe:inet_hash_remove
+{
+ @remove_count++;
+}
+
+/* ------------------------------------------------------------------ */
+/* Resize events */
+/* ------------------------------------------------------------------ */
+
+/* rht_deferred_worker: the workqueue callback that drives resize */
+kprobe:rht_deferred_worker
+{
+ @resize_wq_entry[tid] = nsecs;
+ @resize_events++;
+ printf(">>> RESIZE #%lld: deferred_worker started\n",
+ @resize_events);
+}
+
+kretprobe:rht_deferred_worker
+/@resize_wq_entry[tid]/
+{
+ $dt = nsecs - @resize_wq_entry[tid];
+ @resize_wq_ns = hist($dt);
+ printf(" RESIZE: deferred_worker done in %lld us\n", $dt / 1000);
+ delete(@resize_wq_entry[tid]);
+}
+
+/* bucket_table_alloc: reveals the NEW table size being allocated.
+ * Signature: bucket_table_alloc(struct rhashtable *ht, size_t nbuckets, gfp_t)
+ * arg1 = nbuckets = new table size.
+ */
+kprobe:bucket_table_alloc*
+{
+ @new_tbl_size = arg1;
+ @bucket_allocs++;
+ printf(" RESIZE: bucket_table_alloc nbuckets=%lld\n", arg1);
+ print(kstack(6));
+}
+
+/* rhashtable_rehash_table: actual entry migration between old/new table */
+kprobe:rhashtable_rehash_table
+{
+ @rehash_entry[tid] = nsecs;
+}
+
+kretprobe:rhashtable_rehash_table
+/@rehash_entry[tid]/
+{
+ $dt = nsecs - @rehash_entry[tid];
+ @rehash_ns = hist($dt);
+ @rehash_count++;
+ printf(" RESIZE: rehash_table done in %lld us\n", $dt / 1000);
+ delete(@rehash_entry[tid]);
+}
+
+/* ------------------------------------------------------------------ */
+/* Summary on Ctrl-C */
+/* ------------------------------------------------------------------ */
+
+END
+{
+ printf("\n");
+ printf("========================================================\n");
+ printf(" inet_addr_lst rhltable trace summary\n");
+ printf("========================================================\n");
+
+ printf("\n--- Call counts ---\n");
+ printf(" inet_lookup_ifaddr_rcu : %8lld (hot lookup)\n",
+ @lookup_count);
+ printf(" __ip_dev_find : %8lld (full lookup)\n",
+ @ipdev_count);
+ printf(" rhashtable_insert_slow : %8lld (insert slow path)\n",
+ @insert_slow);
+ printf(" inet_hash_remove : %8lld (remove)\n",
+ @remove_count);
+
+ printf("\n--- Resize activity ---\n");
+ printf(" rht_deferred_worker : %8lld (resize worker runs)\n",
+ @resize_events);
+ printf(" bucket_table_alloc : %8lld (table allocations)\n",
+ @bucket_allocs);
+ printf(" rhashtable_rehash : %8lld (rehash completions)\n",
+ @rehash_count);
+
+ printf("\n--- inet_lookup_ifaddr_rcu latency (ns) ---\n");
+ print(@lookup_ns);
+
+ printf("\n--- __ip_dev_find latency (ns) ---\n");
+ print(@ipdev_ns);
+
+ printf("\n--- rht_deferred_worker duration (ns) ---\n");
+ print(@resize_wq_ns);
+
+ printf("\n--- rhashtable_rehash_table duration (ns) ---\n");
+ print(@rehash_ns);
+
+ /* clean up maps */
+ clear(@lookup_entry);
+ clear(@ipdev_entry);
+ clear(@resize_wq_entry);
+ clear(@rehash_entry);
+ clear(@new_tbl_size);
+ clear(@phase);
+}
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c b/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
new file mode 100644
index 000000000000..ad1913ebba15
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Fast UDP sender/sink for ipv4_addr_lookup benchmarking.
+ *
+ * Sender mode: sends unconnected UDP packets from many source addresses
+ * to stress __ip_dev_find -> inet_lookup_ifaddr_rcu (rhltable_lookup).
+ * Each sendto() triggers: ip_route_output_key -> __ip_dev_find -> hash lookup.
+ *
+ * Sink mode (--sink): minimal C UDP receiver that counts packets received.
+ * Not used by default -- the test script uses an iptables DROP rule instead
+ * to avoid polluting perf profiles with recv() overhead. Enable with
+ * --sink on the test script command line for packet drop verification.
+ *
+ * Sender design for low-noise measurement:
+ * - Pre-create all sockets during setup (not timed)
+ * - Tight sendto() loop during measurement (no socket lifecycle overhead)
+ * - Clock check only every 1024 packets (avoid paravirt clock overhead)
+ * - 1 second warm-up to stabilize caches and hash table
+ * - Multiple rounds with per-round statistics (median, min, max, stdev)
+ *
+ * Usage:
+ * ipv4_addr_lookup_udp_sender <num_addrs> <rounds> <duration_sec>
+ * ipv4_addr_lookup_udp_sender --sink [port]
+ *
+ * Example: ipv4_addr_lookup_udp_sender 1000 10 3
+ * -> 10 rounds of 3s each (+ 1s warm-up) = ~31s total
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+#define DST_ADDR "192.168.1.2"
+#define DST_PORT 9000
+#define SINK_PORT DST_PORT
+#define SINK_BUF 4096
+#define WARMUP_SEC 1
+#define CLOCK_INTERVAL 1024 /* check clock every N packets */
+#define MAX_ROUNDS 100
+#define PAYLOAD_LEN 64
+
+static double ts_diff(struct timespec *a, struct timespec *b)
+{
+ return (b->tv_sec - a->tv_sec) +
+ (b->tv_nsec - a->tv_nsec) * 1e-9;
+}
+
+static int cmp_double(const void *a, const void *b)
+{
+ double da = *(const double *)a;
+ double db = *(const double *)b;
+
+ return (da > db) - (da < db);
+}
+
+static void run_round(int *fds, int num_addrs, int duration,
+ struct sockaddr_in *dst, char *payload, int payload_len,
+ long long *out_sent, long long *out_errors,
+ double *out_rate)
+{
+ struct timespec ts_start, ts_now;
+ long long sent = 0, errors = 0;
+ double elapsed;
+ int i = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_start);
+ for (;;) {
+ if (fds[i] >= 0) {
+ if (sendto(fds[i], payload, payload_len, 0,
+ (struct sockaddr *)dst,
+ sizeof(*dst)) < 0)
+ errors++;
+ else
+ sent++;
+ }
+ i = (i + 1) % num_addrs;
+ if ((sent & (CLOCK_INTERVAL - 1)) == 0) {
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ if (ts_diff(&ts_start, &ts_now) >= duration)
+ break;
+ }
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ elapsed = ts_diff(&ts_start, &ts_now);
+
+ *out_sent = sent;
+ *out_errors = errors;
+ *out_rate = elapsed > 0 ? sent / elapsed : 0;
+}
+
+static volatile int sink_running = 1;
+
+static void sink_stop(int sig)
+{
+ sink_running = 0;
+}
+
+/* Not used by default -- the test script uses iptables DROP instead to keep
+ * perf profiles clean. Enable with: test_script --sink
+ */
+static int run_sink(int port)
+{
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 }; /* 100ms */
+ int rcvbuf = 4 * 1024 * 1024; /* 4 MB - prevent drops during bursts */
+ struct sigaction sa = { };
+ struct sockaddr_in addr;
+ long long received = 0;
+ char buf[SINK_BUF];
+ int fd;
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return 1;
+ }
+
+ /* SO_RCVBUFFORCE bypasses net.core.rmem_max (requires root) */
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf)))
+ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ perror("bind");
+ close(fd);
+ return 1;
+ }
+
+ /* Use sigaction without SA_RESTART so recv() returns -EINTR
+ * immediately on signal, rather than being silently restarted.
+ */
+ sa.sa_handler = sink_stop;
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ fprintf(stderr, "sink: listening on port %d\n", port);
+
+ while (sink_running) {
+ if (recv(fd, buf, sizeof(buf), 0) > 0)
+ received++;
+ }
+
+ /* Drain in-flight packets (e.g. still traversing veth pipe).
+ * SO_RCVTIMEO (100ms) ensures we exit once the queue is idle.
+ */
+ while (recv(fd, buf, sizeof(buf), 0) > 0)
+ received++;
+
+ close(fd);
+
+ fprintf(stderr, "sink: received %lld packets\n", received);
+ /* Parseable output for test script */
+ printf("received=%lld\n", received);
+ fflush(stdout);
+ return 0;
+}
+
+/* Create and bind one UDP socket per source address: 10.B2.B3.1
+ * Returns the number of successfully bound sockets.
+ */
+static int setup_sockets(int *fds, int num_addrs, int sndbuf)
+{
+ struct sockaddr_in src;
+ int i, n_ok = 0;
+
+ for (i = 0; i < num_addrs; i++) {
+ int idx = i + 1;
+
+ fds[i] = -1;
+ memset(&src, 0, sizeof(src));
+ src.sin_family = AF_INET;
+ /* 10.<high byte>.<low byte>.1 */
+ src.sin_addr.s_addr = htonl(0x0a000001 |
+ ((idx & 0xff) << 8) |
+ (((idx >> 8) & 0xff) << 16));
+
+ fds[i] = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fds[i] < 0)
+ continue;
+ if (sndbuf > 0) {
+ if (setsockopt(fds[i], SOL_SOCKET, SO_SNDBUFFORCE,
+ &sndbuf, sizeof(sndbuf)))
+ setsockopt(fds[i], SOL_SOCKET, SO_SNDBUF,
+ &sndbuf, sizeof(sndbuf));
+ }
+ if (bind(fds[i], (struct sockaddr *)&src, sizeof(src)) < 0) {
+ close(fds[i]);
+ fds[i] = -1;
+ continue;
+ }
+ n_ok++;
+ }
+ return n_ok;
+}
+
+/* Warm-up: send for WARMUP_SEC to stabilize caches, hash table, softirq */
+static long long run_warmup(int *fds, int num_addrs, struct sockaddr_in *dst,
+ char *payload)
+{
+ struct timespec ts_start, ts_now;
+ long long sent = 0;
+ int i = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_start);
+ for (;;) {
+ if (fds[i] >= 0) {
+ if (sendto(fds[i], payload, PAYLOAD_LEN, 0,
+ (struct sockaddr *)dst, sizeof(*dst)) >= 0)
+ sent++;
+ }
+ i = (i + 1) % num_addrs;
+ if ((sent & (CLOCK_INTERVAL - 1)) == 0) {
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ if (ts_diff(&ts_start, &ts_now) >= WARMUP_SEC)
+ break;
+ }
+ }
+ return sent;
+}
+
+/* Compute and print summary statistics (parseable by test script).
+ * sent= includes warmup so it matches the sink's received count.
+ */
+static void print_summary(double *rates, int rounds,
+ long long total_sent, long long warmup_sent,
+ long long total_errors)
+{
+ double median, mean, stdev, sum, sumsq;
+ int i;
+
+ qsort(rates, rounds, sizeof(double), cmp_double);
+
+ if (rounds % 2 == 0)
+ median = (rates[rounds / 2 - 1] + rates[rounds / 2]) / 2.0;
+ else
+ median = rates[rounds / 2];
+
+ sum = 0;
+ sumsq = 0;
+ for (i = 0; i < rounds; i++) {
+ sum += rates[i];
+ sumsq += rates[i] * rates[i];
+ }
+ mean = sum / rounds;
+
+ if (rounds > 1) {
+ double variance = (sumsq - sum * sum / rounds) /
+ (rounds - 1);
+
+ /* Sqrt via Newton's method (avoids -lm) */
+ stdev = variance;
+ if (stdev > 0) {
+ double s = stdev / 2;
+
+ for (i = 0; i < 20; i++)
+ s = (s + variance / s) / 2;
+ stdev = s;
+ }
+ } else {
+ stdev = 0;
+ }
+
+ printf("sent=%lld warmup=%lld errors=%lld rounds=%d "
+ "rate=%.0f pkt/s median=%.0f min=%.0f max=%.0f stdev=%.0f\n",
+ total_sent + warmup_sent, warmup_sent, total_errors, rounds,
+ mean, median, rates[0], rates[rounds - 1], stdev);
+}
+
+/* Prevent CPU C-state transitions for stable benchmark results.
+ * Holds /dev/cpu_dma_latency open with value 0 (lowest latency).
+ * Returns fd (caller must close), or -1 on failure (non-fatal).
+ */
+static int set_cpu_dma_latency(void)
+{
+ int32_t lat = 0;
+ int fd;
+
+ fd = open("/dev/cpu_dma_latency", O_WRONLY);
+ if (fd < 0)
+ return -1;
+ if (write(fd, &lat, sizeof(lat)) != sizeof(lat)) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int run_sender(int num_addrs, int rounds, int duration, int sndbuf)
+{
+ long long total_sent = 0, total_errors = 0, warmup_sent;
+ long long round_sent, round_errors;
+ int *fds, n_ok, i, dma_fd;
+ double rates[MAX_ROUNDS];
+ char payload[PAYLOAD_LEN];
+ struct sockaddr_in dst;
+ double round_rate;
+ struct rlimit rl;
+
+ if (rounds < 1)
+ rounds = 1;
+ if (rounds > MAX_ROUNDS)
+ rounds = MAX_ROUNDS;
+
+ /* Raise fd limit for high address counts */
+ if (num_addrs + 64 > 1024) {
+ rl.rlim_cur = num_addrs + 256;
+ rl.rlim_max = num_addrs + 256;
+ setrlimit(RLIMIT_NOFILE, &rl);
+ }
+
+ memset(payload, 'X', sizeof(payload));
+ memset(&dst, 0, sizeof(dst));
+ dst.sin_family = AF_INET;
+ dst.sin_port = htons(DST_PORT);
+ inet_pton(AF_INET, DST_ADDR, &dst.sin_addr);
+
+ /* Phase 1: Pre-create and bind all sockets (not timed) */
+ fds = calloc(num_addrs, sizeof(int));
+ if (!fds) {
+ perror("calloc");
+ return 1;
+ }
+
+ n_ok = setup_sockets(fds, num_addrs, sndbuf);
+ fprintf(stderr, "setup: %d/%d sockets bound\n", n_ok, num_addrs);
+
+ dma_fd = set_cpu_dma_latency();
+ if (dma_fd >= 0)
+ fprintf(stderr, "setup: cpu_dma_latency=0 (C-states disabled)\n");
+ if (n_ok == 0) {
+ fprintf(stderr, "no sockets created\n");
+ free(fds);
+ return 1;
+ }
+
+ /* Phase 2: Warm-up */
+ warmup_sent = run_warmup(fds, num_addrs, &dst, payload);
+
+ /* Phase 3: Measurement rounds */
+ for (i = 0; i < rounds; i++) {
+ run_round(fds, num_addrs, duration, &dst, payload,
+ PAYLOAD_LEN, &round_sent, &round_errors, &round_rate);
+ rates[i] = round_rate;
+ total_sent += round_sent;
+ total_errors += round_errors;
+ fprintf(stderr, " round %2d: %8.0f pkt/s\n",
+ i + 1, round_rate);
+ }
+
+ print_summary(rates, rounds, total_sent, warmup_sent, total_errors);
+
+ /* Cleanup */
+ if (dma_fd >= 0)
+ close(dma_fd);
+ for (i = 0; i < num_addrs; i++) {
+ if (fds[i] >= 0)
+ close(fds[i]);
+ }
+ free(fds);
+
+ return (total_errors > num_addrs / 10) ? 1 : 0;
+}
+
+int main(int argc, char **argv)
+{
+ int sndbuf = 0;
+ int port;
+
+ if (argc >= 2 && strcmp(argv[1], "--sink") == 0) {
+ port = (argc >= 3) ? atoi(argv[2]) : SINK_PORT;
+
+ return run_sink(port);
+ }
+
+ if (argc < 4) {
+ fprintf(stderr,
+ "Usage: %s <num_addrs> <rounds> <duration_sec> [--sndbuf bytes]\n"
+ " %s --sink [port]\n",
+ argv[0], argv[0]);
+ return 1;
+ }
+
+ if (argc >= 6 && strcmp(argv[4], "--sndbuf") == 0)
+ sndbuf = atoi(argv[5]);
+
+ return run_sender(atoi(argv[1]), atoi(argv[2]), atoi(argv[3]), sndbuf);
+}
--
2.43.0
^ permalink raw reply related
* [RFC PATCH net-next 3/4] ipv4: convert inet_addr_lst to rhltable for dynamic resizing
From: hawk @ 2026-03-31 21:07 UTC (permalink / raw)
To: netdev
Cc: davem, dsahern, edumazet, kuba, pabeni, horms, shuah,
linux-kselftest, hawk, ivan, kernel-team
In-Reply-To: <20260331210739.3998753-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
The per-netns IPv4 local address hash table (inet_addr_lst) is a
fixed-size hlist with 256 buckets (IN4_ADDR_HSIZE). On hosts with many
addresses -- e.g. ~700 on Cloudflare edge nodes -- the average chain
length reaches ~2.8, making inet_lookup_ifaddr_rcu() visible in perf
profiles on the unconnected UDP sendmsg path via __ip_dev_find().
Replace the fixed hlist with an rhltable (resizable hash linked table)
that grows and shrinks automatically as addresses are added or removed.
The rhl variant is needed because the same IP can exist on multiple
interfaces. A plain rhashtable would reject the second insert with
-EEXIST, and removing one interface's address would silently drop the
other from the table. All current callers only need first-match
semantics, which rhltable_lookup() provides.
The rhashtable_params are tuned for this use case:
- No explicit .hashfn: with key_len = sizeof(__be32), the default
path calls jhash2(key, 1, seed) which the compiler fully inlines.
- .obj_cmpfn: a direct __be32 comparison replacing the generic
memcmp() in the default rhashtable_compare(). The compiler inlines
this to a single cmp instruction.
- .min_size = 32: most network namespaces only have loopback, so 32
buckets (256 bytes) is sufficient and saves memory compared to the
old fixed 256-bucket table (2048 bytes per netns).
With these settings, objdump confirms zero indirect calls and zero
function calls to hashfn or cmpfn in the lookup path.
The check_lifetime() work function previously iterated all hash buckets
directly. Convert it to walk for_each_netdev -> in_dev->ifa_list, which
is the natural way to enumerate all addresses and avoids coupling the
lifetime logic to hash table internals.
The rhltable serves as a lookup cache for __ip_dev_find(). If
rhltable_insert() fails (e.g. -ENOMEM during table resize), the address
remains on in_dev->ifa_list and lookups fall back to the slower but
always-correct fib_table_lookup() path. A pr_warn is emitted on insert
failure for diagnostics. On remove, -ENOENT is tolerated since the
preceding insert may have failed.
Reported-by: Ivan Babrou <ivan@cloudflare.com>
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
---
include/linux/inetdevice.h | 3 +-
include/net/ip.h | 5 --
include/net/netns/ipv4.h | 4 +-
net/ipv4/Kconfig | 16 ----
net/ipv4/devinet.c | 149 +++++++++++++++++++++----------------
5 files changed, 88 insertions(+), 89 deletions(-)
diff --git a/include/linux/inetdevice.h b/include/linux/inetdevice.h
index dccbeb25f701..e2f7a2f721c9 100644
--- a/include/linux/inetdevice.h
+++ b/include/linux/inetdevice.h
@@ -13,6 +13,7 @@
#include <linux/sysctl.h>
#include <linux/rtnetlink.h>
#include <linux/refcount.h>
+#include <linux/rhashtable-types.h>
struct ipv4_devconf {
void *sysctl;
@@ -141,7 +142,7 @@ static inline void ipv4_devconf_setall(struct in_device *in_dev)
ARP_EVICT_NOCARRIER)
struct in_ifaddr {
- struct hlist_node addr_lst;
+ struct rhlist_head addr_lst;
struct in_ifaddr __rcu *ifa_next;
struct in_device *ifa_dev;
struct rcu_head rcu_head;
diff --git a/include/net/ip.h b/include/net/ip.h
index f39a3787fedd..03932ec93d67 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -705,11 +705,6 @@ static inline unsigned int ipv4_addr_hash(__be32 ip)
return (__force unsigned int) ip;
}
-static inline u32 __ipv4_addr_hash(const __be32 ip, const u32 initval)
-{
- return jhash_1word((__force u32)ip, initval);
-}
-
static inline u32 ipv4_portaddr_hash(const struct net *net,
__be32 saddr,
unsigned int port)
diff --git a/include/net/netns/ipv4.h b/include/net/netns/ipv4.h
index 80ccd4dda8e0..f956ea1b23ca 100644
--- a/include/net/netns/ipv4.h
+++ b/include/net/netns/ipv4.h
@@ -11,11 +11,11 @@
#include <linux/rcupdate.h>
#include <linux/seqlock.h>
#include <linux/siphash.h>
+#include <linux/rhashtable-types.h>
struct ctl_table_header;
struct ipv4_devconf;
struct fib_rules_ops;
-struct hlist_head;
struct fib_table;
struct sock;
struct local_ports {
@@ -296,7 +296,7 @@ struct netns_ipv4 {
atomic_t rt_genid;
siphash_key_t ip_id_key;
- struct hlist_head *inet_addr_lst;
+ struct rhltable inet_addr_lst;
struct delayed_work addr_chk_work;
};
diff --git a/net/ipv4/Kconfig b/net/ipv4/Kconfig
index 3c5e5e74b3e4..df922f9f5289 100644
--- a/net/ipv4/Kconfig
+++ b/net/ipv4/Kconfig
@@ -402,22 +402,6 @@ config INET_IPCOMP
If unsure, say Y.
-config INET_ADDR_HASH_BUCKETS
- int "IPv4 address hash table size" if EXPERT
- range 64 16384
- default 256
- help
- Number of hash buckets for looking up local IPv4 addresses,
- e.g. during route output to validate the source address via
- __ip_dev_find(). Rounded up to the nearest power of 2.
-
- Hosts with many IPv4 addresses benefit from a larger table to reduce
- hash chain lengths. This is particularly relevant when sending using
- unconnected UDP sockets.
-
- The default of 256 is fine for most systems. A value of 1024
- suits hosts with ~500+ addresses.
-
config INET_TABLE_PERTURB_ORDER
int "INET: Source port perturbation table size (as power of 2)" if EXPERT
default 16
diff --git a/net/ipv4/devinet.c b/net/ipv4/devinet.c
index 9e3da06fb618..a02a31d68b2f 100644
--- a/net/ipv4/devinet.c
+++ b/net/ipv4/devinet.c
@@ -49,6 +49,7 @@
#include "igmp_internal.h"
#include <linux/slab.h>
#include <linux/hash.h>
+#include <linux/rhashtable.h>
#ifdef CONFIG_SYSCTL
#include <linux/sysctl.h>
#endif
@@ -108,28 +109,45 @@ static const struct nla_policy ifa_ipv4_policy[IFA_MAX+1] = {
[IFA_PROTO] = { .type = NLA_U8 },
};
-#define IN4_ADDR_HSIZE_SHIFT order_base_2(CONFIG_INET_ADDR_HASH_BUCKETS)
-#define IN4_ADDR_HSIZE (1U << IN4_ADDR_HSIZE_SHIFT)
-
-static u32 inet_addr_hash(const struct net *net, __be32 addr)
+static int inet_addr_cmpfn(struct rhashtable_compare_arg *arg, const void *obj)
{
- u32 val = __ipv4_addr_hash(addr, net_hash_mix(net));
+ const struct in_ifaddr *ifa = obj;
+ const __be32 *key = arg->key;
- return hash_32(val, IN4_ADDR_HSIZE_SHIFT);
+ return *key != ifa->ifa_local;
}
+static const struct rhashtable_params inet_addr_rht_params = {
+ .head_offset = offsetof(struct in_ifaddr, addr_lst),
+ .key_offset = offsetof(struct in_ifaddr, ifa_local),
+ .key_len = sizeof(__be32),
+ .min_size = 32,
+ .obj_cmpfn = inet_addr_cmpfn,
+ .automatic_shrinking = true,
+};
+
static void inet_hash_insert(struct net *net, struct in_ifaddr *ifa)
{
- u32 hash = inet_addr_hash(net, ifa->ifa_local);
+ int err;
ASSERT_RTNL();
- hlist_add_head_rcu(&ifa->addr_lst, &net->ipv4.inet_addr_lst[hash]);
+ err = rhltable_insert(&net->ipv4.inet_addr_lst, &ifa->addr_lst,
+ inet_addr_rht_params);
+ /* Non-fatal: lookups fall back to fib_table_lookup() */
+ if (unlikely(err))
+ pr_warn("%s() failed for %pI4: %d\n",
+ __func__, &ifa->ifa_local, err);
}
-static void inet_hash_remove(struct in_ifaddr *ifa)
+static void inet_hash_remove(struct net *net, struct in_ifaddr *ifa)
{
+ int err;
+
ASSERT_RTNL();
- hlist_del_init_rcu(&ifa->addr_lst);
+ err = rhltable_remove(&net->ipv4.inet_addr_lst, &ifa->addr_lst,
+ inet_addr_rht_params);
+ /* -ENOENT is fine: insert may have failed earlier (e.g. -ENOMEM) */
+ WARN_ON_ONCE(err && err != -ENOENT);
}
/**
@@ -173,12 +191,12 @@ EXPORT_SYMBOL(__ip_dev_find);
/* called under RCU lock */
struct in_ifaddr *inet_lookup_ifaddr_rcu(struct net *net, __be32 addr)
{
- u32 hash = inet_addr_hash(net, addr);
- struct in_ifaddr *ifa;
+ struct rhlist_head *rhl;
- hlist_for_each_entry_rcu(ifa, &net->ipv4.inet_addr_lst[hash], addr_lst)
- if (ifa->ifa_local == addr)
- return ifa;
+ rhl = rhltable_lookup(&net->ipv4.inet_addr_lst, &addr,
+ inet_addr_rht_params);
+ if (rhl)
+ return container_of(rhl, struct in_ifaddr, addr_lst);
return NULL;
}
@@ -216,7 +234,7 @@ static struct in_ifaddr *inet_alloc_ifa(struct in_device *in_dev)
in_dev_hold(in_dev);
ifa->ifa_dev = in_dev;
- INIT_HLIST_NODE(&ifa->addr_lst);
+ memset(&ifa->addr_lst, 0, sizeof(ifa->addr_lst));
return ifa;
}
@@ -405,7 +423,7 @@ static void __inet_del_ifa(struct in_device *in_dev,
}
if (!do_promote) {
- inet_hash_remove(ifa);
+ inet_hash_remove(dev_net(in_dev->dev), ifa);
*ifap1 = ifa->ifa_next;
rtmsg_ifa(RTM_DELADDR, ifa, nlh, portid);
@@ -434,7 +452,7 @@ static void __inet_del_ifa(struct in_device *in_dev,
/* 2. Unlink it */
*ifap = ifa1->ifa_next;
- inet_hash_remove(ifa1);
+ inet_hash_remove(dev_net(in_dev->dev), ifa1);
/* 3. Announce address deletion */
@@ -709,21 +727,24 @@ static int inet_rtm_deladdr(struct sk_buff *skb, struct nlmsghdr *nlh,
static void check_lifetime(struct work_struct *work)
{
unsigned long now, next, next_sec, next_sched;
+ bool change_needed = false;
+ struct in_device *in_dev;
+ struct net_device *dev;
struct in_ifaddr *ifa;
- struct hlist_node *n;
struct net *net;
- int i;
net = container_of(to_delayed_work(work), struct net, ipv4.addr_chk_work);
now = jiffies;
next = round_jiffies_up(now + ADDR_CHECK_FREQUENCY);
- for (i = 0; i < IN4_ADDR_HSIZE; i++) {
- struct hlist_head *head = &net->ipv4.inet_addr_lst[i];
- bool change_needed = false;
+ rcu_read_lock();
+ for_each_netdev_rcu(net, dev) {
+ in_dev = __in_dev_get_rcu(dev);
+ if (!in_dev)
+ continue;
- rcu_read_lock();
- hlist_for_each_entry_rcu(ifa, head, addr_lst) {
+ for (ifa = rcu_dereference(in_dev->ifa_list); ifa;
+ ifa = rcu_dereference(ifa->ifa_next)) {
unsigned long age, tstamp;
u32 preferred_lft;
u32 valid_lft;
@@ -757,43 +778,47 @@ static void check_lifetime(struct work_struct *work)
next = tstamp + preferred_lft * HZ;
}
}
- rcu_read_unlock();
- if (!change_needed)
- continue;
+ }
+ rcu_read_unlock();
+ if (change_needed) {
rtnl_net_lock(net);
- hlist_for_each_entry_safe(ifa, n, head, addr_lst) {
- unsigned long age;
+ for_each_netdev(net, dev) {
+ struct in_ifaddr __rcu **ifap;
- if (ifa->ifa_flags & IFA_F_PERMANENT)
+ in_dev = __in_dev_get_rtnl_net(dev);
+ if (!in_dev)
continue;
- /* We try to batch several events at once. */
- age = (now - ifa->ifa_tstamp +
- ADDRCONF_TIMER_FUZZ_MINUS) / HZ;
+ ifap = &in_dev->ifa_list;
+ ifa = rtnl_net_dereference(net, *ifap);
+ while (ifa) {
+ unsigned long age;
- if (ifa->ifa_valid_lft != INFINITY_LIFE_TIME &&
- age >= ifa->ifa_valid_lft) {
- struct in_ifaddr __rcu **ifap;
- struct in_ifaddr *tmp;
-
- ifap = &ifa->ifa_dev->ifa_list;
- tmp = rtnl_net_dereference(net, *ifap);
- while (tmp) {
- if (tmp == ifa) {
- inet_del_ifa(ifa->ifa_dev,
- ifap, 1);
- break;
- }
- ifap = &tmp->ifa_next;
- tmp = rtnl_net_dereference(net, *ifap);
+ if (ifa->ifa_flags & IFA_F_PERMANENT) {
+ ifap = &ifa->ifa_next;
+ ifa = rtnl_net_dereference(net, *ifap);
+ continue;
}
- } else if (ifa->ifa_preferred_lft !=
- INFINITY_LIFE_TIME &&
- age >= ifa->ifa_preferred_lft &&
- !(ifa->ifa_flags & IFA_F_DEPRECATED)) {
- ifa->ifa_flags |= IFA_F_DEPRECATED;
- rtmsg_ifa(RTM_NEWADDR, ifa, NULL, 0);
+
+ /* We try to batch several events at once. */
+ age = (now - ifa->ifa_tstamp +
+ ADDRCONF_TIMER_FUZZ_MINUS) / HZ;
+
+ if (ifa->ifa_valid_lft != INFINITY_LIFE_TIME &&
+ age >= ifa->ifa_valid_lft) {
+ inet_del_ifa(in_dev, ifap, 1);
+ ifa = rtnl_net_dereference(net, *ifap);
+ continue;
+ } else if (ifa->ifa_preferred_lft !=
+ INFINITY_LIFE_TIME &&
+ age >= ifa->ifa_preferred_lft &&
+ !(ifa->ifa_flags & IFA_F_DEPRECATED)) {
+ ifa->ifa_flags |= IFA_F_DEPRECATED;
+ rtmsg_ifa(RTM_NEWADDR, ifa, NULL, 0);
+ }
+ ifap = &ifa->ifa_next;
+ ifa = rtnl_net_dereference(net, *ifap);
}
}
rtnl_net_unlock(net);
@@ -2786,12 +2811,9 @@ static __net_init int devinet_init_net(struct net *net)
#endif
struct ipv4_devconf *all, *dflt;
int err;
- int i;
- err = -ENOMEM;
- net->ipv4.inet_addr_lst = kmalloc_objs(struct hlist_head,
- IN4_ADDR_HSIZE);
- if (!net->ipv4.inet_addr_lst)
+ err = rhltable_init(&net->ipv4.inet_addr_lst, &inet_addr_rht_params);
+ if (err)
goto err_alloc_hash;
all = kmemdup(&ipv4_devconf, sizeof(ipv4_devconf), GFP_KERNEL);
@@ -2854,9 +2876,6 @@ static __net_init int devinet_init_net(struct net *net)
net->ipv4.forw_hdr = forw_hdr;
#endif
- for (i = 0; i < IN4_ADDR_HSIZE; i++)
- INIT_HLIST_HEAD(&net->ipv4.inet_addr_lst[i]);
-
INIT_DEFERRABLE_WORK(&net->ipv4.addr_chk_work, check_lifetime);
net->ipv4.devconf_all = all;
@@ -2876,7 +2895,7 @@ static __net_init int devinet_init_net(struct net *net)
err_alloc_dflt:
kfree(all);
err_alloc_all:
- kfree(net->ipv4.inet_addr_lst);
+ rhltable_destroy(&net->ipv4.inet_addr_lst);
err_alloc_hash:
return err;
}
@@ -2900,7 +2919,7 @@ static __net_exit void devinet_exit_net(struct net *net)
#endif
kfree(net->ipv4.devconf_dflt);
kfree(net->ipv4.devconf_all);
- kfree(net->ipv4.inet_addr_lst);
+ rhltable_destroy(&net->ipv4.inet_addr_lst);
}
static __net_initdata struct pernet_operations devinet_ops = {
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox