Netdev List
 help / color / mirror / Atom feed
* [PATCH net 0/4] mptcp: misc fixes for v7.1-rc3
From: Matthieu Baerts (NGI0) @ 2026-05-01 19:35 UTC (permalink / raw)
  To: Mat Martineau, Geliang Tang, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Florian Westphal,
	Gang Yan, Dmytro Shytyi
  Cc: netdev, mptcp, linux-kernel, Matthieu Baerts (NGI0),
	Shardul Bankar, stable

Here are various unrelated fixes:

- Patch 1: increment the right MIB counter. A fix for v5.7.

- Patch 2: set the right MPTCP reset reason. A fix for v5.9.

- Patch 3: fix rx timestamp corruption when on MPTCP passive fastopen. A
  fix for v6.2.

- Patch 4: increase sockopt seq after having set TCP_MAXSEG to propagate
  it to newer subflows later. A fix for 6.17.

Signed-off-by: Matthieu Baerts (NGI0) <matttbe@kernel.org>
---
Matthieu Baerts (NGI0) (1):
      mptcp: sockopt: increase seq in mptcp_setsockopt_all_sf

Paolo Abeni (1):
      mptcp: fix rx timestamp corruption on fastopen

Shardul Bankar (2):
      mptcp: use MPJoinSynAckHMacFailure for SynAck HMAC failure
      mptcp: use MPTCP_RST_EMPTCP for ACK HMAC validation failure

 net/mptcp/fastopen.c | 4 +++-
 net/mptcp/sockopt.c  | 4 ++++
 net/mptcp/subflow.c  | 4 ++--
 3 files changed, 9 insertions(+), 3 deletions(-)
---
base-commit: 85da3965df5e6f1e1c48d2c435e140c5b66625ef
change-id: 20260501-net-mptcp-misc-fixes-7-1-rc3-902fc2a5d499

Best regards,
--  
Matthieu Baerts (NGI0) <matttbe@kernel.org>


^ permalink raw reply

* [PATCH net-next 5/5] Documentation: networking: add OPEN Alliance 10BASE-T1x MAC-PHY serial interface
From: Selvamani Rajagopal @ 2026-05-01 19:15 UTC (permalink / raw)
  To: Piergiorgio Beruto, parthiban.veerasooran@microchip.com,
	davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
	pabeni@redhat.com, horms@kernel.org, corbet@lwn.net,
	skhan@linuxfoundation.org, netdev@vger.kernel.org,
	linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org

Added the changes to API to support onsemi devices and
new APIs introduced to support hardware timestamp.

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
 Documentation/networking/oa-tc6-framework.rst | 32 ++++++++++++++++---
 1 file changed, 28 insertions(+), 4 deletions(-)

diff --git a/Documentation/networking/oa-tc6-framework.rst b/Documentation/networking/oa-tc6-framework.rst
index fe2aabde9..cae16e4bf 100644
--- a/Documentation/networking/oa-tc6-framework.rst
+++ b/Documentation/networking/oa-tc6-framework.rst
@@ -453,8 +453,9 @@ Device drivers API

 The include/linux/oa_tc6.h defines the following functions:

-.. c:function:: struct oa_tc6 *oa_tc6_init(struct spi_device *spi, \
-                                           struct net_device *netdev)
+.. c:function:: struct oa_tc6 *oa_tc6_init(void *priv, struct spi_device *spi, \
+                                           struct net_device *netdev, \
+                                           struct mii_bus *bus)

 Initialize OA TC6 lib.

@@ -485,13 +486,36 @@ Reading multiple consecutive registers starting from @address in the MAC-PHY.
 Maximum of 128 consecutive registers can be read starting at @address.

 .. c:function:: netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, \
-                                              struct sk_buff *skb);
+                                              struct sk_buff *skb)

 The transmit Ethernet frame in the skb is or going to be transmitted through
 the MAC-PHY.

-.. c:function:: int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6);
+.. c:function:: int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6)

 Zero align receive frame feature can be enabled to align all receive ethernet
 frames data to start at the beginning of any receive data chunk payload with a
 start word offset (SWO) of zero.
+
+.. c:function:: int oa_tc6_hwtstamp_ioctl(struct oa_tc6 *tc6, \
+                                          struct ifreq *rq, int cmd)
+Legacy ioctl interface for supporting hardware timestmp.
+
+frames data to start at the beginning of any receive data chunk payload with a
+
+.. c:function:: int oa_tc6_hwtstamp_set(struct oa_tc6 *tc6, \
+                                        struct kernel_hwtstamp_config *cfg)
+Interface to set hardware timestmp configuration through ndo_hwtstamp_set.
+This API is used by legacy ioctl interface as well.
+
+.. c:function:: void oa_tc6_hwtstamp_get(struct oa_tc6 *tc6, \
+                                         struct kernel_hwtstamp_config *cfg)
+
+Interface to get the hardware timestmp configuration through ndo_hwtstamp_get.
+This API is used by legacy ioctl interface as well.
+
+.. c:function:: void *oa_tc6_priv(struct oa_tc6 *tc6)
+Interface to get vendor's private data structure from oa_tc6 structure. This
+is needed for vendor implemented mii_bus APIs, as mii_bus APIs priv
+pointer carries oa_tc6 structure.
+
--
2.43.0


Public Information


^ permalink raw reply related

* [PATCH net-next 4/5] onsemi: ncn260xx: Add driver support for NCN26010 and TS2500 MAC-PHY
From: Selvamani Rajagopal @ 2026-05-01 19:15 UTC (permalink / raw)
  To: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	netdev@vger.kernel.org, linux-kernel@vger.kernel.org

Support for onsemi's 10Base-T1S MAC-PHY products. Works with
Open Alliance TC6 framework in the kernel.

Hardware timestamp support added only for TS2500.

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
 drivers/net/ethernet/Kconfig                  |   1 +
 drivers/net/ethernet/Makefile                 |   1 +
 .../net/ethernet/microchip/lan865x/lan865x.c  |   2 +-
 drivers/net/ethernet/onsemi/Kconfig           |  21 +
 drivers/net/ethernet/onsemi/Makefile          |   7 +
 drivers/net/ethernet/onsemi/ncn260xx/Kconfig  |  22 +
 drivers/net/ethernet/onsemi/ncn260xx/Makefile |   7 +
 .../onsemi/ncn260xx/ncn260xx_ethtool.c        | 259 +++++
 .../onsemi/ncn260xx/ncn260xx_macphy.c         | 927 ++++++++++++++++++
 .../onsemi/ncn260xx/ncn260xx_macphy.h         | 283 ++++++
 .../ethernet/onsemi/ncn260xx/ncn260xx_ptp.c   | 253 +++++
 11 files changed, 1782 insertions(+), 1 deletion(-)
 create mode 100644 drivers/net/ethernet/onsemi/Kconfig
 create mode 100644 drivers/net/ethernet/onsemi/Makefile
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/Kconfig
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/Makefile
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ethtool.c
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.c
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.h
 create mode 100644 drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ptp.c

diff --git a/drivers/net/ethernet/Kconfig b/drivers/net/ethernet/Kconfig
index b8f70e2a1..a42656120 100644
--- a/drivers/net/ethernet/Kconfig
+++ b/drivers/net/ethernet/Kconfig
@@ -134,6 +134,7 @@ source "drivers/net/ethernet/8390/Kconfig"
 source "drivers/net/ethernet/nvidia/Kconfig"
 source "drivers/net/ethernet/nxp/Kconfig"
 source "drivers/net/ethernet/oki-semi/Kconfig"
+source "drivers/net/ethernet/onsemi/Kconfig"

 config ETHOC
        tristate "OpenCores 10/100 Mbps Ethernet MAC support"
diff --git a/drivers/net/ethernet/Makefile b/drivers/net/ethernet/Makefile
index 57344fec6..38527c249 100644
--- a/drivers/net/ethernet/Makefile
+++ b/drivers/net/ethernet/Makefile
@@ -71,6 +71,7 @@ obj-$(CONFIG_NET_VENDOR_NI) += ni/
 obj-$(CONFIG_NET_VENDOR_NVIDIA) += nvidia/
 obj-$(CONFIG_LPC_ENET) += nxp/
 obj-$(CONFIG_NET_VENDOR_OKI) += oki-semi/
+obj-$(CONFIG_NET_VENDOR_ONSEMI) += onsemi/
 obj-$(CONFIG_ETHOC) += ethoc.o
 obj-$(CONFIG_NET_VENDOR_PASEMI) += pasemi/
 obj-$(CONFIG_NET_VENDOR_QLOGIC) += qlogic/
diff --git a/drivers/net/ethernet/microchip/lan865x/lan865x.c b/drivers/net/ethernet/microchip/lan865x/lan865x.c
index 0277d9737..fb1ef0855 100644
--- a/drivers/net/ethernet/microchip/lan865x/lan865x.c
+++ b/drivers/net/ethernet/microchip/lan865x/lan865x.c
@@ -346,7 +346,7 @@ static int lan865x_probe(struct spi_device *spi)
        spi_set_drvdata(spi, priv);
        INIT_WORK(&priv->multicast_work, lan865x_multicast_work_handler);

-       priv->tc6 = oa_tc6_init(spi, netdev);
+       priv->tc6 = oa_tc6_init(priv, spi, netdev, NULL);
        if (!priv->tc6) {
                ret = -ENODEV;
                goto free_netdev;
diff --git a/drivers/net/ethernet/onsemi/Kconfig b/drivers/net/ethernet/onsemi/Kconfig
new file mode 100644
index 000000000..43c778a55
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Kconfig
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# onsemi network device configuration
+#
+
+config NET_VENDOR_ONSEMI
+       bool "onsemi network devices"
+       help
+         If you have a network card belonging to this class, say Y.
+
+         Note that the answer to this question doesn't directly affect the
+         kernel: saying N will just cause the configurator to skip all
+         the questions about onsemi ethernet devices. If you say Y, you
+          will be asked for your specific card in the following questions.
+
+if NET_VENDOR_ONSEMI
+
+source "drivers/net/ethernet/onsemi/ncn260xx/Kconfig"
+
+endif # NET_VENDOR_ONSEMI
+
diff --git a/drivers/net/ethernet/onsemi/Makefile b/drivers/net/ethernet/onsemi/Makefile
new file mode 100644
index 000000000..02f6f88a0
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the onsemi network device drivers.
+#
+
+obj-$(CONFIG_NCN260XX_MACPHY) += ncn260xx/
+
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/Kconfig b/drivers/net/ethernet/onsemi/ncn260xx/Kconfig
new file mode 100644
index 000000000..350d3e82f
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/Kconfig
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# onsemi NCN260xx Driver Support
+#
+
+if NET_VENDOR_ONSEMI
+
+config NCN260XX_MACPHY
+       tristate "NCN260xx support"
+       depends on SPI
+       select NCN26000_PHY
+       select OA_TC6
+       select NET_DEVLINK
+       help
+         Support for the onsemi NCN26010/TS2500 MACPHY Ethernet chip.
+          It works under the framework that conform to OPEN Alliance
+          10BASE-T1x Serial Interface specification.
+
+          To compile this driver as a module, choose M here. The module will be
+          called ncn26xx.
+
+endif # NET_VENDOR_ONSEMI
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/Makefile b/drivers/net/ethernet/onsemi/ncn260xx/Makefile
new file mode 100644
index 000000000..7d392bb90
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0-only
+#
+# Makefile for the onsemi network device drivers.
+#
+obj-$(CONFIG_NCN260XX_MACPHY) := ncn260xx.o
+ncn260xx-objs := ncn260xx_macphy.o ncn260xx_ethtool.o ncn260xx_ptp.o
+
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ethtool.c b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ethtool.c
new file mode 100644
index 000000000..fe117fed0
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ethtool.c
@@ -0,0 +1,259 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's NCN260xx/TS2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "ncn260xx_macphy.h"
+#include <linux/ethtool.h>
+
+#define ONMPH_NUM_REGS                 38
+#define ONMPH_REGDUMP_LEN              (sizeof(u32) * (ONMPH_NUM_REGS * 2))
+
+static u32 phy_addr_list[ONMPH_NUM_REGS] = {
+       (0x4 << 16) | 0x8000,
+       (0x4 << 16) | 0x8001,
+               (0x4 << 16) | 0x8002,
+               (0x4 << 16) | 0x8003,
+               (0x4 << 16) | 0x8004,
+               (0x4 << 16) | 0x8007,
+               (0x4 << 16) | 0xCC01,
+               (0x4 << 16) | 0xCC02,
+               (0x4 << 16) | 0xCC03,
+               (0x4 << 16) | 0xCC04,
+               (0x4 << 16) | 0xCD00,
+               (0x4 << 16) | 0xCD01,
+               (0x4 << 16) | 0xCD02,
+               (0x4 << 16) | 0xD000,
+               (0x4 << 16) | 0xD001,
+               (0x4 << 16) | 0xD100,
+               (0x4 << 16) | 0xD101,
+               (0xC << 16) | 0x10,
+               (0xC << 16) | 0x11,
+               (0xC << 16) | 0x12,
+               (0xC << 16) | 0x1000,
+               (0xC << 16) | 0x1001,
+               (0xC << 16) | 0x1002,
+               (0xC << 16) | 0x1003,
+               (0xC << 16) | 0x1005,
+               (0xC << 16) | 0x1010,
+               (0xC << 16) | 0x1011,
+               (0xC << 16) | 0x1012,
+               (0xC << 16) | 0x1013,
+               (0xC << 16) | 0x1014,
+               (0xC << 16) | 0x1015,
+               (0xC << 16) | 0x1016,
+               (0xC << 16) | 0x1017,
+               (0xC << 16) | 0x1018,
+               (0xC << 16) | 0x1019,
+               (0xC << 16) | 0x101A,
+               (0xC << 16) | 0x101B,
+               (0xC << 16) | 0x101C,
+       };
+
+static const char onmph_stat_strings[][ETH_GSTRING_LEN] = {
+       "tx-bytes-ok",
+       "tx-frames",
+       "tx-broadcast-frames",
+       "tx-multicast-frames",
+       "tx-64-frames",
+       "tx-65-127-frames",
+       "tx-128-255-frames",
+       "tx-256-511-frames",
+       "tx-512-1023-frames",
+       "tx-1024-1518-frames",
+       "tx-underrun-errors",
+       "tx-single-collision",
+       "tx-multiple-collision",
+       "tx-excessive-collision",
+       "tx-deferred-frames",
+       "tx-carrier-sense-errors",
+       "rx-bytes-ok",
+       "rx-frames",
+       "rx-broadcast-frames",
+       "rx-multicast-frames",
+       "rx-64-frames",
+       "rx-65-127-frames",
+       "rx-128-255-frames",
+       "rx-256-511-frames",
+       "rx-512-1023-frames",
+       "rx-1024-1518-frames",
+       "rx-runt",
+       "rx-too-long-frames",
+       "rx-crc-errors",
+       "rx-symbol-errors",
+       "rx-alignment-errors",
+       "rx-busy-drop-frames",
+       "rx-mismatch-drop-frames",
+       "ts_frames",
+};
+
+#define ONMPH_STATS_LEN ARRAY_SIZE(onmph_stat_strings)
+static_assert(ONMPH_STATS_LEN == ONMPH_STATS_NUM);
+
+#define STAT_REG_OFFSET(x) ((ONMPH_REG_MAC_ST##x) - ONMPH_REG_MAC_FIRST_STAT)
+
+#define STAT_OFF_TX_BYTES_OK 0
+#define STAT_OFF_TX_FRAMES 1
+
+static void onmph_update_mac_stats(struct onmph_info *priv)
+{
+       u64 *data = priv->stats_data;
+       u64 tx_frames_before;
+       u32 *regs;
+       u32 *rptr;
+       int ret;
+
+       regs = kmalloc_array(ONMPH_NUMBER_OF_STAT_REGS, sizeof(u32), GFP_KERNEL);
+       if (!regs)
+               return;
+
+       ret = oa_tc6_read_registers(priv->tc6, ONMPH_REG_MAC_STOCTECTSTXL,
+                                   regs, ONMPH_NUMBER_OF_STAT_REGS);
+       if (ret)
+               goto out;
+
+       rptr = regs;
+
+       /* Workaround for NCN26010 version 0x01 */
+       if (priv->model == ONMPH_MODEL_NCN26010 &&
+           priv->version == ONMPH_NCN26010_V0) {
+               tx_frames_before = priv->stats_data[STAT_OFF_TX_FRAMES];
+       }
+
+       /* TX bytes is a 64-bit register that spans over two 32-bit regs
+        * note: HW does auto-freeze when reading LSB and un-freeze on MSB
+        */
+       *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+       /* run until the next 64-bit register */
+       for (rptr += 2; (rptr - regs) < STAT_REG_OFFSET(OCTECTSRXL); ++rptr)
+               *(data++) += *rptr;
+
+       /* RX bytes is a 64-bit register that spans over two 32-bit regs
+        * note: HW does auto-freeze when reading LSB and un-freeze on MSB
+        */
+       *(data++) += ((u64)*rptr) | (((u64)*(rptr + 1)) << 32);
+
+       for (rptr += 2; (rptr - regs) < ONMPH_NUMBER_OF_STAT_REGS; ++rptr)
+               *(data++) += *rptr;
+
+       /* model-specific fixes */
+       if (priv->model == ONMPH_MODEL_NCN26010 &&
+           priv->version == ONMPH_NCN26010_V0) {
+               /* Add 4 to transmitted bytes for each transmitted packet
+                * because the HW is not counting the FCS
+                */
+               priv->stats_data[STAT_OFF_TX_BYTES_OK] +=
+                       4 * (priv->stats_data[STAT_OFF_TX_FRAMES] -
+                            tx_frames_before);
+       }
+       priv->stats_data[ONMPH_STATS_NUM - 1] = priv->ts_frames;
+out:
+       kfree(regs);
+}
+
+static void onmph_get_drvinfo(struct net_device *ndev,
+                             struct ethtool_drvinfo *info)
+{
+       strscpy(info->driver, DRV_NAME, sizeof(info->driver));
+       strscpy(info->bus_info, dev_name(&ndev->dev), sizeof(info->bus_info));
+       strscpy(info->version, DRV_VERSION, sizeof(info->version));
+}
+
+static int onmph_ethtool_set_link_ksettings(struct net_device *ndev,
+                                           const struct ethtool_link_ksettings *cmd)
+{
+       phy_ethtool_ksettings_set(ndev->phydev, cmd);
+       return 0;
+}
+
+static int onmph_ethtool_get_link_ksettings(struct net_device *ndev,
+                                           struct ethtool_link_ksettings *cmd)
+{
+       phy_ethtool_ksettings_get(ndev->phydev, cmd);
+       return 0;
+}
+
+static int onmph_get_sset_count(struct net_device *ndev, int sset)
+{
+       if (sset == ETH_SS_STATS)
+               return ONMPH_STATS_LEN;
+       else
+               return -EOPNOTSUPP;
+}
+
+static void onmph_get_strings(struct net_device *ndev, u32 stringset, u8 *buf)
+{
+       memcpy(buf, onmph_stat_strings, ONMPH_STATS_LEN * ETH_GSTRING_LEN);
+}
+
+static void onmph_get_ethtool_stats(struct net_device *ndev,
+                                   struct ethtool_stats *stats, u64 *data)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       onmph_update_mac_stats(priv);
+       memcpy(data, priv->stats_data, sizeof(priv->stats_data));
+}
+
+static int onmph_get_ts_info(struct net_device *ndev,
+                            struct kernel_ethtool_ts_info *ts_info)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       if (!priv->ptp_clock)
+               return ethtool_op_get_ts_info(ndev, ts_info);
+
+       ts_info->so_timestamping = SOF_TIMESTAMPING_RAW_HARDWARE |
+                                  SOF_TIMESTAMPING_TX_HARDWARE |
+                                  SOF_TIMESTAMPING_RX_HARDWARE;
+       ts_info->phc_index = ptp_clock_index(priv->ptp_clock);
+       ts_info->tx_types = BIT(HWTSTAMP_TX_ON);
+       ts_info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
+       return 0;
+}
+
+static int onmph_get_regs_len(struct net_device *dev)
+{
+       return ONMPH_REGDUMP_LEN;
+}
+
+static void onmph_get_regs(struct net_device *ndev, struct ethtool_regs *regs, void *p)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+       u32 *pbuff = (u32 *)p;
+       u32 val, reg;
+       int ret = 0;
+       int i;
+
+       regs->version = 0;
+       memset(p, 0, ONMPH_REGDUMP_LEN);
+
+       if (!netif_running(ndev))
+               return;
+
+       for (i = 0; i < ONMPH_NUM_REGS; i++) {
+               val = 0;
+               reg = phy_addr_list[i];
+               ret = oa_tc6_read_register(priv->tc6, reg, &val);
+               if (ret)
+                       continue;
+               *pbuff++ = cpu_to_be32(reg);
+               *pbuff++ = cpu_to_be32(val);
+       }
+}
+
+const struct ethtool_ops onmph_ethtool_ops = {
+       .get_drvinfo        = onmph_get_drvinfo,
+       .get_link           = ethtool_op_get_link,
+       .get_link_ksettings = onmph_ethtool_get_link_ksettings,
+       .set_link_ksettings = onmph_ethtool_set_link_ksettings,
+       .get_sset_count     = onmph_get_sset_count,
+       .get_strings        = onmph_get_strings,
+       .get_ethtool_stats  = onmph_get_ethtool_stats,
+       .get_ts_info        = onmph_get_ts_info,
+       .get_regs_len       = onmph_get_regs_len,
+       .get_regs           = onmph_get_regs,
+};
+
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.c b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.c
new file mode 100644
index 000000000..4c033d65f
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.c
@@ -0,0 +1,927 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's NCN260xx/TS2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "ncn260xx_macphy.h"
+
+#include <linux/etherdevice.h>
+#include <linux/if_ether.h>
+#include <linux/irqchip.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/phy.h>
+
+/* ONMPH functions & definitions */
+
+#define ONMPH_STATUS0_MASK     (ONMPH_SPI_ST0_CDPE_BIT | \
+                               ONMPH_SPI_ST0_TXFCSE_BIT | \
+                               ONMPH_SPI_ST0_TTSCAC_BIT | \
+                               ONMPH_SPI_ST0_TTSCAB_BIT | \
+                               ONMPH_SPI_ST0_TTSCAA_BIT | \
+                               ONMPH_SPI_ST0_RESETC_BIT | \
+                               ONMPH_SPI_ST0_HDRE_BIT | \
+                               ONMPH_SPI_ST0_LOFE_BIT | \
+                               ONMPH_SPI_ST0_RXBOE_BIT | \
+                               ONMPH_SPI_ST0_TXBUE_BIT | \
+                               ONMPH_SPI_ST0_TXBOE_BIT | \
+                               ONMPH_SPI_ST0_TXPE_BIT)
+
+/* Converts a MACPHY ID to a device name */
+static inline const char *onmph_id_to_name(u32 id)
+{
+       if (id == ONMPH_MODEL_NCN26010)
+               return "NCN26010";
+       if (id == ONMPH_MODEL_TS2500)
+               return "TS2500";
+       return "unknown";
+}
+
+/* Initializes the net device MAC address by reading the UID stored into the
+ * device internal non-volatile memory.
+ */
+static int onmph_read_mac_from_nvmem(struct onmph_info *priv)
+{
+       u8 addr[ETH_ALEN];
+       u32 mac1 = 0;
+       u32 mac0 = 0;
+       int i, j;
+       u32 val;
+       int ret;
+
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_SPI_PHYID, &val);
+       if (ret)
+               return ret;
+
+       val = (val & ONMPH_SPI_PHYID_OUI_MASK) >> ONMPH_SPI_PHYID_OUI_SHIFT;
+
+       /* Convert the OID in host byte order */
+       for (i = 2; i >= 0; --i) {
+               addr[i] = 0;
+               for (j = 0; j < 8; ++j) {
+                       addr[i] |= (val & 1) << (7 - j);
+                       val >>= 1;
+               }
+       }
+
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_VS_MACID1, &mac1);
+       if (ret)
+               return ret;
+
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_VS_MACID0, &mac0);
+       if (ret)
+               return ret;
+
+       /* Pre-production parts may have 0 */
+       if (mac0 == 0 && mac1 == 0)
+               return -ENXIO;
+
+       addr[3] = mac1 & 0xff;
+       addr[4] = (mac0 >> 8) & 0xff;
+       addr[5] = mac0 & 0xff;
+
+       __dev_addr_set(priv->ndev, addr, ETH_ALEN);
+       priv->ndev->addr_assign_type = NET_ADDR_PERM;
+
+       return ret;
+}
+
+/* Writes MAC address to macphy registers */
+static int onmph_set_mac_filter(struct net_device *ndev, const u8 *mac)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+       u32 val;
+       int ret;
+
+       /* Set unicast address filter */
+       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKL(0),
+                                   0xffffffff);
+       if (ret)
+               return ret;
+
+       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKH(0),
+                                   0xffff);
+       if (ret)
+               return ret;
+
+       val = ((u32)mac[2] << 24) |
+              ((u32)mac[3] << 16) |
+              ((u32)mac[4] << 8) |
+              ((u32)mac[5]);
+
+       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTL(0), val);
+       if (ret)
+               return ret;
+
+       val = ONMPH_MAC_ADDRFILT_EN_BIT | ((u32)mac[0] << 8) | ((u32)mac[1]);
+
+       return oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTH(0), val);
+}
+
+static int onmph_mac_ctrl_clear_bits(struct onmph_info *priv, u32 in_bits, bool clr)
+{
+       u32 reg = ONMPH_REG_MAC_CONTROL;
+       u32 rval = 0;
+       int ret;
+
+       ret = oa_tc6_read_register(priv->tc6, reg, &rval);
+       if (!ret) {
+               u32 wval = 0;
+
+               if (clr)
+                       wval = rval & ~in_bits;
+               else
+                       wval = rval | in_bits;
+               if (rval != wval)
+                       ret = oa_tc6_write_register(priv->tc6, reg, wval);
+       }
+       return ret;
+}
+
+static int onmph_init(struct onmph_info *priv)
+{
+       u32 val;
+       int ret;
+
+       /* Configure the SPI protocol */
+       val = (ONMPH_SPI_CFG0_SYNC_BIT) | ONMPH_SPI_CFG0_RXCTE_BIT |
+             (ONMPH_TXCTHRESH_8 << ONMPH_SPI_CFG0_TXCTHRESH_SHIFT) |
+             (ONMPH_CPS_64 << ONMPH_SPI_CFG0_CPS_SHIFT);
+
+       if (priv->tx_fcs_calc)
+               val |= ONMPH_SPI_CFG0_TXFCSVE_BIT;
+
+       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_SPI_CFG0, val);
+       if (ret)
+               return ret;
+
+       val = (u32)~(ONMPH_SPI_ST0_RESETC_BIT |
+                    ONMPH_SPI_ST0_HDRE_BIT | ONMPH_SPI_ST0_LOFE_BIT |
+                    ONMPH_SPI_ST0_RXBOE_BIT | ONMPH_SPI_ST0_TXBOE_BIT |
+                    ONMPH_SPI_ST0_TXPE_BIT);
+
+       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_SPI_IRQM0, val);
+       if (ret)
+               return ret;
+
+       /* Read the initial value of TX credits */
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_SPI_BUFST, &val);
+       if (ret)
+               return ret;
+
+       /* Program the source MAC address into the device */
+       ret = onmph_set_mac_filter(priv->ndev, priv->ndev->dev_addr);
+
+       val = ONMPH_MAC_CONTROL_ADRF_BIT;
+       if (!priv->tx_fcs_calc)
+               val |= ONMPH_MAC_CONTROL_FCSA_BIT;
+
+       return onmph_mac_ctrl_clear_bits(priv, val, false);
+}
+
+static void onmph_shutdown(struct onmph_info *priv)
+{
+       u32 val = ONMPH_MAC_CONTROL_TXEN_BIT | ONMPH_MAC_CONTROL_RXEN_BIT;
+       struct net_device *ndev = priv->ndev;
+
+       netif_stop_queue(ndev);
+       phy_stop(ndev->phydev);
+
+       onmph_mac_ctrl_clear_bits(priv, val, true);
+}
+
+static int onmph_set_promiscuous_mode(struct onmph_info *priv, unsigned int rx_flags)
+{
+       u32 val = ONMPH_MAC_CONTROL_ADRF_BIT;
+       bool clr = false;
+
+       if (rx_flags & IFF_PROMISC)
+               clr = true;
+       return onmph_mac_ctrl_clear_bits(priv, val, clr);
+}
+
+static int onmph_set_multicast_mode(struct onmph_info *priv, unsigned int rx_flags)
+{
+       int i, ret = 0;
+       u32 val;
+
+       if ((rx_flags & IFF_ALLMULTI) ||
+           (netdev_mc_count(priv->ndev) > ONMPH_N_MCAST_FILTERS)) {
+               /* Disable multicast filter */
+               ret = onmph_mac_ctrl_clear_bits(priv, ONMPH_MAC_CONTROL_MCSF_BIT, true);
+               if (ret)
+                       return ret;
+
+               /* Accept all multicasts (any address with the LSB = 1 in the first byte) */
+               ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKL(1), 0);
+               if (ret)
+                       return ret;
+
+               ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKH(1), 0x100);
+               if (ret)
+                       return ret;
+
+               ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTL(1), 0);
+               if (ret)
+                       return ret;
+
+               val = ONMPH_MAC_ADDRFILT_EN_BIT | 0x00000100;
+               ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTH(1), val);
+       } else if (netdev_mc_count(priv->ndev) == 0) {
+               /* Enable multicast filter */
+               ret = onmph_mac_ctrl_clear_bits(priv, ONMPH_MAC_CONTROL_MCSF_BIT, false);
+               if (ret)
+                       return ret;
+
+               /* Disable filters */
+               for (i = 1; i <= ONMPH_N_MCAST_FILTERS; i++) {
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTH(i), 0);
+                       if (ret)
+                               return ret;
+               }
+       } else {
+               struct netdev_hw_addr *ha;
+               u32 addrh;
+               u32 addrl;
+
+               /* Disable multicast filter */
+               ret = onmph_mac_ctrl_clear_bits(priv, ONMPH_MAC_CONTROL_MCSF_BIT, true);
+               if (ret)
+                       return ret;
+
+               /* Disable filters */
+               for (i = 1; i <= ONMPH_N_MCAST_FILTERS; i++) {
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTH(i), 0);
+                       if (ret)
+                               return ret;
+               }
+
+               i = 1;
+               netdev_for_each_mc_addr(ha, priv->ndev) {
+                       if (i > ONMPH_N_MCAST_FILTERS)
+                               break;
+
+                       addrh = ((ha->addr[0] << 8) | ha->addr[1] |
+                                ONMPH_MAC_ADDRFILT_EN_BIT);
+                       addrl = ((ha->addr[2] << 24) | (ha->addr[3] << 16) |
+                                (ha->addr[4] << 8) | (ha->addr[5]));
+
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTH(i),
+                                                   addrh);
+                       if (ret)
+                               return ret;
+
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRFILTL(i),
+                                                   addrl);
+                       if (ret)
+                               return ret;
+
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKL(i),
+                                                   0xffffffff);
+                       if (ret)
+                               return ret;
+
+                       ret = oa_tc6_write_register(priv->tc6, ONMPH_REG_MAC_ADDRMASKH(i),
+                                                   0xffff);
+                       if (ret)
+                               return ret;
+                       i++;
+               }
+       }
+       return ret;
+}
+
+/* Deferred function for applying RX mode flags in non-atomic context */
+static int onmph_rx_mode_update(struct onmph_info *priv)
+{
+       unsigned int rx_flags;
+       unsigned long flags;
+       int ret;
+
+       spin_lock_irqsave(&priv->lock, flags);
+
+       rx_flags = priv->ndev_flags;
+       priv->rx_flags_upd = false;
+
+       spin_unlock_irqrestore(&priv->lock, flags);
+
+       ret = onmph_set_promiscuous_mode(priv, rx_flags);
+       if (ret)
+               goto out;
+
+       ret = onmph_set_multicast_mode(priv, rx_flags);
+out:
+       return ret;
+}
+
+static int onmph_ioctl(struct net_device *ndev, struct ifreq *rq, int cmd)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       if (!netif_running(ndev))
+               return -EINVAL;
+
+       /* Shouldn't pass hardware timestamp related command to
+        * PHY as it doesn't support natively
+        */
+       if (cmd == SIOCSHWTSTAMP || cmd == SIOCGHWTSTAMP)
+               return onmph_ioctl_timestamp(priv, rq, cmd);
+
+       return phy_do_ioctl(ndev, rq, cmd);
+}
+
+static void onmph_set_rx_mode(struct net_device *ndev)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+       unsigned long flags;
+
+       spin_lock_irqsave(&priv->lock, flags);
+
+       priv->rx_flags_upd = true;
+       priv->ndev_flags = ndev->flags;
+
+       spin_unlock_irqrestore(&priv->lock, flags);
+
+       if (priv->thread)
+               wake_up_process(priv->thread);
+}
+
+static int onmph_set_mac_address(struct net_device *ndev, void *p)
+{
+       struct sockaddr *addr = p;
+
+       if (!is_valid_ether_addr(addr->sa_data))
+               return -EADDRNOTAVAIL;
+
+       eth_hw_addr_set(ndev, addr->sa_data);
+       return onmph_set_mac_filter(ndev, addr->sa_data);
+}
+
+static netdev_tx_t onmph_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       if (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP)
+               priv->ts_frames++;
+
+       return oa_tc6_start_xmit(priv->tc6, skb);
+}
+
+static void onmph_process_events(struct onmph_info *priv)
+{
+       u32 val;
+       int ret;
+
+       if (!priv->event_pending)
+               return;
+
+       priv->event_pending = false;
+
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_SPI_ST0, &val);
+       if (ret) {
+               dev_err(&priv->spi->dev, "Error reading ST0 register");
+               return;
+       }
+}
+
+static int onmph_thread_fun(void *data)
+{
+       struct onmph_info *priv = data;
+       bool update_rx_mode = false;
+       unsigned long flags;
+       signed long tout;
+       int ret = 0;
+
+       tout = priv->poll_jiff;
+
+       do {
+               if (update_rx_mode) {
+                       ret = onmph_rx_mode_update(priv);
+                       if (unlikely(ret)) {
+                               dev_err(&priv->spi->dev, "Failed to set new RX mode");
+                               break;
+                       }
+               }
+
+               if (tout == 0) {
+                       tout = priv->poll_jiff;
+
+                       /* Force checking the status register */
+                       priv->event_pending = true;
+               }
+
+               onmph_process_events(priv);
+
+               spin_lock_irqsave(&priv->lock, flags);
+               __set_current_state(TASK_INTERRUPTIBLE);
+
+               update_rx_mode = priv->rx_flags_upd;
+               ret = update_rx_mode;
+
+               spin_unlock_irqrestore(&priv->lock, flags);
+
+               if (!ret)
+                       tout = schedule_timeout(tout);
+               else
+                       set_current_state(TASK_RUNNING);
+       } while (!kthread_should_stop());
+       return 0;
+}
+
+static int onmph_open(struct net_device *ndev)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+       int ret = 0;
+       u32 val;
+
+       dev_info(&priv->spi->dev, "%s", "onmph_open");
+       phy_start(priv->ndev->phydev);
+
+       priv->thread = kthread_run(onmph_thread_fun, priv, DRV_NAME "/%s:%d",
+                                  dev_name(&priv->spi->dev),
+                                  spi_get_chipselect(priv->spi, 0));
+
+       if (IS_ERR(priv->thread)) {
+               ret = PTR_ERR(priv->thread);
+       } else {
+               val = ONMPH_MAC_CONTROL_TXEN_BIT | ONMPH_MAC_CONTROL_RXEN_BIT;
+               ret = onmph_mac_ctrl_clear_bits(priv, val, false);
+
+               netif_start_queue(priv->ndev);
+       }
+       return ret;
+}
+
+static int onmph_stop(struct net_device *ndev)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       dev_info(&priv->spi->dev, "%s", "onmph_stop");
+
+       onmph_shutdown(priv);
+
+       kthread_stop(priv->thread);
+       priv->thread = NULL;
+
+       return 0;
+}
+
+static int onmph_hwtstamp_get(struct net_device *ndev,
+                             struct kernel_hwtstamp_config *cfg)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       if (!priv->ptp_clock) {
+               cfg->tx_type = 0;
+               cfg->rx_filter = 0;
+       } else {
+               oa_tc6_hwtstamp_get(priv->tc6, cfg);
+       }
+       return 0;
+}
+
+static int onmph_hwtstamp_set(struct net_device *ndev,
+                             struct kernel_hwtstamp_config *cfg,
+                             struct netlink_ext_ack *extack)
+{
+       struct onmph_info *priv = netdev_priv(ndev);
+
+       if (!netif_running(ndev))
+               return -EIO;
+       if (!priv->ptp_clock)
+               return -EPROTONOSUPPORT;
+       return oa_tc6_hwtstamp_set(priv->tc6, cfg);
+}
+
+static const struct net_device_ops onmph_netdev_ops = {
+       .ndo_open            = onmph_open,
+       .ndo_stop            = onmph_stop,
+       .ndo_start_xmit      = onmph_start_xmit,
+       .ndo_set_mac_address = onmph_set_mac_address,
+       .ndo_set_rx_mode     = onmph_set_rx_mode,
+       .ndo_eth_ioctl       = onmph_ioctl,
+       .ndo_hwtstamp_get    = onmph_hwtstamp_get,
+       .ndo_hwtstamp_set    = onmph_hwtstamp_set,
+};
+
+static int mmd2mms(int mmd)
+{
+       int ret = -EOPNOTSUPP;
+
+       switch (mmd) {
+       case MDIO_MMD_PCS:
+               ret = OA_TC6_PHY_C45_PCS_MMS2;
+               break;
+       case MDIO_MMD_PMAPMD:
+               ret = OA_TC6_PHY_C45_PMA_PMD_MMS3;
+               break;
+       case MDIO_MMD_VEND2:
+               ret = OA_TC6_PHY_C45_VS_PLCA_MMS4;
+               break;
+       case MDIO_MMD_VEND1:
+               ret = ONMPH_OA_TC6_VEND1_MMS12;
+               break;
+       default:
+               break;
+       }
+       return ret;
+}
+
+static int onmph_update_model(struct onmph_info *priv, void *tc6_handle)
+{
+       u32 val = 0;
+       int ret;
+
+       if (priv->model != -1)
+               return 0;
+       ret = oa_tc6_read_register(tc6_handle, ONMPH_REG_VS_CHIPID, &val);
+       if (ret)
+               return ret;
+
+       priv->capabilities = ONMPH_CAP_MACADDR;
+       priv->version = (val & ONMPH_CHIPID_REVISION_MASK) >> ONMPH_CHIPID_REVISION_SHIFT;
+
+       priv->model = (val & ONMPH_CHIPID_MODEL_MASK) >> ONMPH_CHIPID_MODEL_SHIFT;
+       if (priv->model == ONMPH_MODEL_TS2500) {
+               priv->capabilities |= ONMPH_CAP_PTP;
+       } else if (priv->model != ONMPH_MODEL_NCN26010) {
+               dev_err(&priv->spi->dev, "Unrecognized macphy. 0x%x\n", val);
+               return -ENODEV;
+       }
+       dev_info(&priv->spi->dev, "Macphy model %s, version %u\n",
+                onmph_id_to_name(priv->model), priv->version);
+       return ret;
+}
+
+static int onmph_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
+                                 int regnum)
+{
+       u32 address, val = 0;
+       int mms, ret;
+
+       /* Only PHY #0 is supported. */
+       if (addr != 0)
+               return 0;
+
+       mms = mmd2mms(devnum);
+       if (mms < 0)
+               return mms;
+
+       address = mms << 16 | regnum;
+       ret = oa_tc6_read_register(bus->priv, address, &val);
+       if (ret)
+               return ret;
+       return val;
+}
+
+static int onmph_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
+                                  int regnum, u16 val)
+{
+       u32 address;
+       int mms;
+
+       if (regnum < 0 || regnum > 0xffff)
+               return -ENXIO;
+
+       /* We support only PHY #0 at this time */
+       if (addr != 0)
+               return 0;
+
+       mms = mmd2mms(devnum);
+       if (mms < 0)
+               return mms;
+
+       address = (u32)regnum | (u32)mms << 16;
+
+       return oa_tc6_write_register(bus->priv, address, val);
+}
+
+static int onmph_mdiobus_read(struct mii_bus *bus, int addr, int reg)
+{
+       struct onmph_info *priv = oa_tc6_priv(bus->priv);
+       u32 val, mms;
+       int ret = 0;
+       u32 address;
+
+       if (addr != 0)
+               return ret;
+
+       if (reg < 0 || reg > 31)
+               return -ENXIO;
+
+       if (reg == MII_MMD_CTRL || reg == MII_MMD_DATA) {
+               dev_err(&priv->spi->dev, "MMD_CTRL/DATA read not supported\n");
+               return -EOPNOTSUPP;
+       }
+
+       onmph_update_model(priv, bus->priv);
+
+       address = reg;
+       mms = ONMPH_OA_TC6_MACPHY_MMS0;
+       if (address < 16)
+               address |= 0xFF00;
+       else
+               mms = ONMPH_OA_TC6_VEND1_MMS12;
+
+       val = 0;
+       ret = oa_tc6_read_register(bus->priv, mms << 16 | address, &val);
+       if (ret != 0)
+               return ret;
+
+       /* By mistake, NCN26010's PHY ID tied to OUI. For NCN26010,
+        * return a consistent value for DEVID1/2, which is the same
+        * value that is used in PHY driver of NCN26000.
+        *
+        * Please note that control comes here from oa_tc6_init. Which
+        * means probe didn't have chance to read the chip ID to figure
+        * out the model.
+        */
+       if (priv->model == ONMPH_MODEL_NCN26010) {
+               if (reg == MDIO_DEVID1)
+                       val = 0x180F;
+               else if (reg == MDIO_DEVID2)
+                       val = 0xF5A1;
+       }
+       return val;
+}
+
+static int onmph_mdiobus_write(struct mii_bus *bus, int addr, int reg, u16 val)
+{
+       struct onmph_info *priv = oa_tc6_priv(bus->priv);
+       u32 mms;
+
+       if (addr != 0)
+               return 0;
+
+       if (reg < 0 || reg > 31)
+               return -ENXIO;
+
+       if (reg == MII_MMD_CTRL || reg == MII_MMD_DATA) {
+               dev_err(&priv->spi->dev, "MMD_CTRL/DATA write not supported\n");
+               return -EOPNOTSUPP;
+       }
+
+       /* Prevent the PHY from being reset from the control register. The
+        * NCN26010 triggers a global soft-reset when resetting the PHY.
+        */
+       if (reg == MDIO_CTRL1 && (val & MDIO_CTRL1_RESET) != 0 &&
+           priv->model == ONMPH_MODEL_NCN26010 &&
+           priv->version == ONMPH_NCN26010_V0)
+               val &= ~MDIO_CTRL1_RESET;
+
+       mms = ONMPH_OA_TC6_MACPHY_MMS0;
+       if (reg < 16)
+               reg |= 0xFF00;
+       else
+               mms = ONMPH_OA_TC6_VEND1_MMS12;
+       return oa_tc6_write_register(bus->priv, mms << 16 | reg, val);
+}
+
+/* Rest will be initialized by oa_tc6.c */
+static void onmph_init_mii_bus_info(struct onmph_info *priv, struct mii_bus *bus)
+{
+       bus->read = onmph_mdiobus_read;
+       bus->write = onmph_mdiobus_write;
+       bus->read_c45 = onmph_mdiobus_read_c45;
+       bus->write_c45 = onmph_mdiobus_write_c45;
+       bus->name = "onmph-mdiobus";
+
+       /* Only phy#0 supported */
+       bus->phy_mask = ~1U;
+}
+
+/* sudo devlink dev param show spi/spi0.0 name plca */
+static int onmph_get_plca_status(struct onmph_info *priv, struct devlink_param_gset_ctx *ctx)
+{
+       u32 addr = ONMPH_REG_PLCADIAG;
+       u32 val = 0;
+       int ret;
+
+       ret = oa_tc6_read_register(priv->tc6, addr, &val);
+       if (ret)
+               return ret;
+       if ((val & ONMPH_PLCADIAG_ERR) == 0) {
+               addr = ONMPH_REG_BCNCNT;
+               val = 0;
+               ret = oa_tc6_read_register(priv->tc6, addr, &val);
+               if (ret)
+                       return ret;
+               snprintf(ctx->val.vstr, sizeof(ctx->val.vstr),
+                        "No error. BCNCNT = 0x%x\n", val);
+       } else {
+               snprintf(ctx->val.vstr, sizeof(ctx->val.vstr),
+                        "Error: %s%s%s",
+                        (val & ONMPH_PLCADIAG_RXINTO) ? "RXINFO " : "",
+                        (val & ONMPH_PLCADIAG_UNEXPB) ? "UNEXPB " : "",
+                        (val & ONMPH_PLCADIAG_BCNBFTO) ? "BCNBFTO" : "");
+       }
+       return 0;
+}
+
+static int onmph_param_get(struct devlink *dl, u32 id,
+                          struct devlink_param_gset_ctx *ctx,
+                          struct netlink_ext_ack *extack)
+{
+       struct onmph_devlink_priv *devl_priv = devlink_priv(dl);
+       struct onmph_info *priv = devl_priv->onmph_priv;
+
+       if (id == ONMPH_PARAM_ID_PLCA)
+               return onmph_get_plca_status(priv, ctx);
+       return -EINVAL;
+}
+
+/* Nothing to set */
+static int onmph_param_set(struct devlink *dl, u32 id, struct devlink_param_gset_ctx *ctx,
+                          struct netlink_ext_ack *extack)
+{
+       return -EIO;
+}
+
+static const struct devlink_param onmph_params[] = {
+       DEVLINK_PARAM_DRIVER(ONMPH_PARAM_ID_PLCA,
+                            "plca", DEVLINK_PARAM_TYPE_STRING,
+                            BIT(DEVLINK_PARAM_CMODE_RUNTIME),
+                            onmph_param_get, onmph_param_set, NULL),
+};
+
+static int onmph_probe(struct spi_device *spi)
+{
+       struct device *dev = &spi->dev;
+       struct net_device *ndev;
+       struct onmph_info *priv;
+       struct mii_bus *bus;
+       u32 val;
+       int ret;
+
+       if (spi->irq < 0)
+               return -ENODEV;
+
+       ndev = devm_alloc_etherdev(dev, sizeof(struct onmph_info));
+       if (!ndev)
+               return -ENOMEM;
+
+       priv = netdev_priv(ndev);
+       priv->model = -1; /* 0 is a valid number for NCN26010. */
+       priv->ndev = ndev;
+       priv->spi = spi;
+       priv->dev = dev;
+
+       SET_NETDEV_DEV(ndev, dev);
+
+       spin_lock_init(&priv->lock);
+       ndev->irq = spi->irq;
+
+       spi->dev.platform_data = priv;
+       spi_set_drvdata(spi, priv);
+
+       ndev->netdev_ops = &onmph_netdev_ops;
+       ndev->ethtool_ops = &onmph_ethtool_ops;
+       ndev->if_port = IF_PORT_10BASET;
+       ndev->priv_flags |= IFF_UNICAST_FLT;
+       ndev->hw_features = NETIF_F_RXALL;
+
+       priv->devlink = devlink_alloc(&priv->devlink_ops,
+                                     sizeof(struct onmph_devlink_priv),
+                                     &spi->dev);
+       if (!priv->devlink)
+               return -ENOMEM;
+
+       ret = devlink_params_register(priv->devlink, onmph_params, ARRAY_SIZE(onmph_params));
+       if (ret)
+               goto devl_reg_err;
+       priv->devlink_priv = devlink_priv(priv->devlink);
+       priv->devlink_priv->onmph_priv = priv;
+       devlink_register(priv->devlink);
+
+       priv->tx_fcs_calc = false;
+       priv->poll_jiff = HZ * 5; /* Poll interval */
+
+       if (!priv->tx_fcs_calc)
+               priv->ndev->hw_features |= NETIF_F_RXFCS;
+
+       /* Pointer "mii_bus" is not saved in anywhere in vendor's code as
+        * oa_tc6.c owns it including the responsibilty to release it.
+        */
+       bus = mdiobus_alloc();
+       if (!bus)
+               return -ENOMEM;
+       onmph_init_mii_bus_info(priv, bus);
+
+       priv->tc6 = oa_tc6_init(priv, spi, ndev, bus);
+
+       /* oa_tc6_init may release mdiobus on error */
+       if (!priv->tc6) {
+               dev_err(&spi->dev, "OA TC6 init failed");
+               return -ENODEV;
+       }
+
+       /* Clear RSTS, if set */
+       oa_tc6_read_register(priv->tc6, ONMPH_REG_MIIM_IRQ_STATUS, &val);
+       val &= MIIM_IRQ_STATUS_RSTS;
+       if (val != 0)
+               oa_tc6_write_register(priv->tc6, ONMPH_REG_MIIM_IRQ_STATUS,
+                                     MIIM_IRQ_STATUS_RSTS);
+
+       /* Acknowledge all IRQ status bits */
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_SPI_ST0, &val);
+       if (!ret) {
+               u32 mask = ONMPH_STATUS0_MASK;
+
+               val &= mask;
+               oa_tc6_write_register(priv->tc6, ONMPH_REG_SPI_ST0, val);
+       }
+
+       /* Start with non-protected control accesses for single register reads.
+        * NOTE: the device starts in this mode after reset, but it is possible
+        * that the PROTE bit was set by a previous module load/unload.
+        * In this case, non-protected register writes won't work, but -single-
+        * unprotected register reads will. Therefore, we can safely probe the
+        * device using regular control accesses and switch to protected mode
+        * later, when resetting the device.
+        */
+
+       ret = onmph_update_model(priv, priv->tc6);
+       if (ret)
+               goto err_reg_read;
+
+       ret = oa_tc6_read_register(priv->tc6, ONMPH_REG_SPI_CFG0, &val);
+       if (ret)
+               goto err_reg_read;
+
+       ret = device_get_ethdev_address(priv->dev, ndev);
+       if (ret && (priv->capabilities & ONMPH_CAP_MACADDR))
+               ret = onmph_read_mac_from_nvmem(priv);
+
+       if (ret) {
+               eth_hw_addr_random(ndev);
+               dev_warn(&spi->dev, "Using random MAC address %pM", ndev->dev_addr);
+       }
+
+       ret = onmph_init(priv);
+       if (unlikely(ret)) {
+               dev_err(&spi->dev, "failed to onmph_init the device");
+               goto err_reg_read;
+       }
+
+       /* Configure PTP if the model supports it */
+       if (priv->capabilities & ONMPH_CAP_PTP)
+               onmph_ptp_register(priv);
+
+       ret = register_netdev(ndev);
+       if (ret) {
+               dev_err(&spi->dev, "failed to register the ONMPH device\n");
+               ret = -ENODEV;
+
+               goto err_reg_read;
+       }
+       return 0;
+
+err_reg_read:
+       dev_err(&spi->dev, "could not initialize macphy");
+       devlink_unregister(priv->devlink);
+devl_reg_err:
+       devlink_free(priv->devlink);
+       return ret;
+}
+
+static void onmph_remove(struct spi_device *spi)
+{
+       struct onmph_info *priv = spi->dev.platform_data;
+
+       dev_info(&spi->dev, "%s", "onmph_remove");
+
+       devlink_unregister(priv->devlink);
+       devlink_free(priv->devlink);
+       onmph_ptp_unregister(priv);
+       unregister_netdev(priv->ndev);
+       oa_tc6_exit(priv->tc6);
+}
+
+static const struct of_device_id onmph_of_match[] = {
+       { .compatible = "onnn,ncn260xx" },
+       {}
+};
+
+static const struct spi_device_id onmph_ids[] = {
+       { "ncn260xx" },
+       {}
+};
+
+MODULE_DEVICE_TABLE(spi, onmph_ids);
+
+static struct spi_driver ncn260xx_driver = {
+       .driver = {
+               .name   = DRV_NAME,
+               .of_match_table = onmph_of_match,
+       },
+       .probe          = onmph_probe,
+       .remove         = onmph_remove,
+       .id_table       = onmph_ids,
+};
+
+module_spi_driver(ncn260xx_driver);
+
+MODULE_AUTHOR("Piergiorgio Beruto <Pier.Beruto@onsemi.com>");
+MODULE_DESCRIPTION("onsemi NCN260xx MACPHY ethernet driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.h b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.h
new file mode 100644
index 000000000..dbd1d402e
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_macphy.h
@@ -0,0 +1,283 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's NCN260xx/TS2500 10BASE-T1S MAC-PHY driver
+ */
+
+#ifndef NCN260XX_MACPHY_H
+#define NCN260XX_MACPHY_H
+
+#include <linux/hrtimer.h>
+#include <linux/irq.h>
+#include <linux/irqdomain.h>
+#include <linux/kernel.h>
+#include <linux/netdevice.h>
+#include <linux/phylink.h>
+#include <linux/spi/spi.h>
+#include <linux/oa_tc6.h>
+#include <linux/net_tstamp.h>
+#include <linux/ptp_clock_kernel.h>
+#include <linux/delay.h>
+#include <linux/mutex.h>
+#include <linux/ktime.h>
+#include <linux/errno.h>
+#include <net/devlink.h>
+
+#define DRV_NAME                       "onmph"
+#define DRV_VERSION                    "1.0.3.1"
+
+#define ONMPH_N_MCAST_FILTERS          3
+
+#define ONMPH_MODEL_NCN26010           0x0000
+#define ONMPH_MODEL_TS2500             0xA7A8
+#define ONMPH_TS2500_DEVID2            0xF411
+
+#define ONMPH_MAKE_VERSION(maj, min, stage, build) ( \
+                          (((maj) & 0xF) << 12) | \
+                          (((min) & 0xF) << 8) | \
+                          (((stage) & 0x3) << 6) | \
+                          (((build) & 0x3F)))
+
+#define ONMPH_NCN26010_V0              ONMPH_MAKE_VERSION(0, 0, 0, 1)
+
+/* Number of stat counters for ethtool */
+#define ONMPH_STATS_NUM                        34
+
+/* List of device capabilities */
+#define ONMPH_CAP_MACADDR              BIT(0)  /* MAC address in hardware */
+#define ONMPH_CAP_PTP                  BIT(1)  /* PTP support */
+
+/* ONMPH registers */
+
+/* Definitions for MMS defined in Table 6 Open Alliance TC6 standard
+ * that not present in oa_tc6.h
+ */
+#define ONMPH_OA_TC6_MACPHY_MMS0       0
+#define ONMPH_OA_TC6_MAC_MMS1          1
+#define ONMPH_OA_TC6_VEND1_MMS12       12
+
+#define ONMPH_MMS_MII                  (ONMPH_OA_TC6_MACPHY_MMS0 << 16)
+#define ONMPH_MMS_MAC                  (ONMPH_OA_TC6_MAC_MMS1 << 16)
+#define ONMPH_MMS_PMDPMA               (OA_TC6_PHY_C45_PMA_PMD_MMS3 << 16)
+#define ONMPH_MMS_VS1                  (ONMPH_OA_TC6_VEND1_MMS12 << 16)
+#define ONMPH_MMS_VS2                  (OA_TC6_PHY_C45_VS_PLCA_MMS4 << 16)
+
+/* SPI OID and model register */
+#define ONMPH_REG_SPI_PHYID            0x1
+
+#define ONMPH_SPI_PHYID_OUI_SHIFT      10
+#define ONMPH_SPI_PHYID_OUI_MASK       GENMASK(31, ONMPH_SPI_PHYID_OUI_SHIFT)
+#define ONMPH_SPI_PHYID_MODEL_SHIFT    4
+#define ONMPH_SPI_PHYID_MODEL_MASK     GENMASK(9, ONMPH_SPI_PHYID_MODEL_SHIFT)
+#define ONMPH_SPI_PHYID_REV_SHIFT      0
+#define ONMPH_SPI_PHYID_REV_MASK       GENMASK(3, ONMPH_SPI_PHYID_REV_SHIFT)
+
+/* SPI configuration register #0 */
+#define ONMPH_REG_SPI_CFG0             (ONMPH_MMS_MII + 0x4)
+
+#define ONMPH_SPI_CFG0_SYNC_BIT                BIT(15)
+#define ONMPH_SPI_CFG0_TXFCSVE_BIT     BIT(14)
+#define ONMPH_SPI_CFG0_CSARFE_BIT      BIT(13)
+#define ONMPH_SPI_CFG0_TXCTHRESH_SHIFT 10
+#define ONMPH_SPI_CFG0_TXCTHRESH_MASK  GENMASK(11, ONMPH_SPI_CFG0_TXCTHRESH_SHIFT)
+#define ONMPH_SPI_CFG0_TXCTE_BIT       BIT(9)
+#define ONMPH_SPI_CFG0_RXCTE_BIT       BIT(8)
+#define ONMPH_SPI_CFG0_FTSE_BIT                BIT(7)
+#define ONMPH_SPI_CFG0_FTSS_BIT                BIT(6)
+#define ONMPH_SPI_CFG0_PROTE_BIT       BIT(5)
+#define ONMPH_SPI_CFG0_SEQE_BIT                BIT(4)
+#define ONMPH_SPI_CFG0_CPS_SHIFT       0
+#define ONMPH_SPI_CFG0_CPS_MASK                GENMASK(2, ONMPH_SPI_CFG0_CPS_SHIFT)
+
+#define ONMPH_TXCTHRESH_1              0x0
+#define ONMPH_TXCTHRESH_4              0x1
+#define ONMPH_TXCTHRESH_8              0x2
+#define ONMPH_TXCTHRESH_16             0x3
+
+#define ONMPH_CPS_8                    0x3
+#define ONMPH_CPS_16                   0x4
+#define ONMPH_CPS_32                   0x5
+#define ONMPH_CPS_64                   0x6
+
+/* SPI status register #0 */
+#define ONMPH_REG_SPI_ST0              (ONMPH_MMS_MII + 8)
+
+#define ONMPH_SPI_ST0_CDPE_BIT         BIT(12)
+#define ONMPH_SPI_ST0_TXFCSE_BIT       BIT(11)
+#define ONMPH_SPI_ST0_TTSCAC_BIT       BIT(10)
+#define ONMPH_SPI_ST0_TTSCAB_BIT       BIT(9)
+#define ONMPH_SPI_ST0_TTSCAA_BIT       BIT(8)
+#define ONMPH_SPI_ST0_PHYINT_BIT       BIT(7)
+#define ONMPH_SPI_ST0_RESETC_BIT       BIT(6)
+#define ONMPH_SPI_ST0_HDRE_BIT         BIT(5)
+#define ONMPH_SPI_ST0_LOFE_BIT         BIT(4)
+#define ONMPH_SPI_ST0_RXBOE_BIT                BIT(3)
+#define ONMPH_SPI_ST0_TXBUE_BIT                BIT(2)
+#define ONMPH_SPI_ST0_TXBOE_BIT                BIT(1)
+#define ONMPH_SPI_ST0_TXPE_BIT         BIT(0)
+
+/* SPI IRQ enable register #0 (use the ONMPH_SPI_ST0_*_BIT constants) */
+#define ONMPH_REG_SPI_IRQM0            (ONMPH_MMS_MII + 0xc)
+
+/* SPI buffer status register */
+#define ONMPH_REG_SPI_BUFST            0xb
+
+#define ONMPH_SPI_BUFST_TXC_SHIFT      8
+#define ONMPH_SPI_BUFST_TXC_MASK       GENMASK(15, ONMPH_SPI_BUFST_TXC_SHIFT)
+
+#define ONMPH_REG_MAC_CONTROL          (ONMPH_MMS_MAC + 0)
+
+#define ONMPH_MAC_CONTROL_MCSF_BIT     BIT(18)
+#define ONMPH_MAC_CONTROL_ADRF_BIT     BIT(16)
+#define ONMPH_MAC_CONTROL_FCSA_BIT     BIT(8)
+#define ONMPH_MAC_CONTROL_TXEN_BIT     BIT(1)
+#define ONMPH_MAC_CONTROL_RXEN_BIT     BIT(0)
+
+/* MAC address filter registers */
+#define ONMPH_REG_MAC_ADDRFILTL(n)     (ONMPH_MMS_MAC + (16 + 2 * (n)))
+#define ONMPH_REG_MAC_ADDRFILTH(n)     (ONMPH_MMS_MAC + (17 + 2 * (n)))
+#define ONMPH_REG_MAC_ADDRMASKL(n)     (ONMPH_MMS_MAC + (32 + 2 * (n)))
+#define ONMPH_REG_MAC_ADDRMASKH(n)     (ONMPH_MMS_MAC + (33 + 2 * (n)))
+
+#define ONMPH_MAC_ADDRFILT_EN_BIT      BIT(31)
+
+/* MAC statistic registers */
+#define ONMPH_REG_MAC_STOCTECTSTXL     (ONMPH_MMS_MAC + 48)
+#define ONMPH_REG_MAC_STOCTECTSTXH     (ONMPH_MMS_MAC + 49)
+#define ONMPH_REG_MAC_STFRAMESTXOK     (ONMPH_MMS_MAC + 50)
+#define ONMPH_REG_MAC_STBCASTTXOK      (ONMPH_MMS_MAC + 51)
+#define ONMPH_REG_MAC_STMCASTTXOK      (ONMPH_MMS_MAC + 52)
+#define ONMPH_REG_MAC_STFRAMESTX64     (ONMPH_MMS_MAC + 53)
+#define ONMPH_REG_MAC_STFRAMESTX65     (ONMPH_MMS_MAC + 54)
+#define ONMPH_REG_MAC_STFRAMESTX128    (ONMPH_MMS_MAC + 55)
+#define ONMPH_REG_MAC_STFRAMESTX256    (ONMPH_MMS_MAC + 56)
+#define ONMPH_REG_MAC_STFRAMESTX512    (ONMPH_MMS_MAC + 57)
+#define ONMPH_REG_MAC_STFRAMESTX1024   (ONMPH_MMS_MAC + 58)
+#define ONMPH_REG_MAC_STTXUNDEFLOW     (ONMPH_MMS_MAC + 59)
+#define ONMPH_REG_MAC_STSINGLECOL      (ONMPH_MMS_MAC + 60)
+#define ONMPH_REG_MAC_STMULTICOL       (ONMPH_MMS_MAC + 61)
+#define ONMPH_REG_MAC_STEXCESSCOL      (ONMPH_MMS_MAC + 62)
+#define ONMPH_REG_MAC_STDEFERREDTX     (ONMPH_MMS_MAC + 63)
+#define ONMPH_REG_MAC_STCRSERR         (ONMPH_MMS_MAC + 64)
+#define ONMPH_REG_MAC_STOCTECTSRXL     (ONMPH_MMS_MAC + 65)
+#define ONMPH_REG_MAC_STOCTECTSRXH     (ONMPH_MMS_MAC + 66)
+#define ONMPH_REG_MAC_STFRAMESRXOK     (ONMPH_MMS_MAC + 67)
+#define ONMPH_REG_MAC_STBCASTRXOK      (ONMPH_MMS_MAC + 68)
+#define ONMPH_REG_MAC_STMCASTRXOK      (ONMPH_MMS_MAC + 69)
+#define ONMPH_REG_MAC_STFRAMESRX64     (ONMPH_MMS_MAC + 60)
+#define ONMPH_REG_MAC_STFRAMESRX65     (ONMPH_MMS_MAC + 71)
+#define ONMPH_REG_MAC_STFRAMESRX128    (ONMPH_MMS_MAC + 72)
+#define ONMPH_REG_MAC_STFRAMESRX256    (ONMPH_MMS_MAC + 73)
+#define ONMPH_REG_MAC_STFRAMESRX512    (ONMPH_MMS_MAC + 74)
+#define ONMPH_REG_MAC_STFRAMESRX1024   (ONMPH_MMS_MAC + 75)
+#define ONMPH_REG_MAC_STRUNTSERR       (ONMPH_MMS_MAC + 76)
+#define ONMPH_REG_MAC_STRXTOOLONG      (ONMPH_MMS_MAC + 77)
+#define ONMPH_REG_MAC_STFCSERRS                (ONMPH_MMS_MAC + 78)
+#define ONMPH_REG_MAC_STSYMBOLERRS     (ONMPH_MMS_MAC + 79)
+#define ONMPH_REG_MAC_STALIGNERRS      (ONMPH_MMS_MAC + 80)
+#define ONMPH_REG_MAC_STRXOVERFLOW     (ONMPH_MMS_MAC + 81)
+#define ONMPH_REG_MAC_STRXDROPPED      (ONMPH_MMS_MAC + 82)
+
+/* First/last statistic register for sequential access */
+#define ONMPH_REG_MAC_FIRST_STAT       ONMPH_REG_MAC_STOCTECTSTXL
+#define ONMPH_REG_MAC_LAST_STAT                ONMPH_REG_MAC_STRXDROPPED
+
+#define ONMPH_NUMBER_OF_STAT_REGS \
+       (ONMPH_REG_MAC_LAST_STAT - ONMPH_REG_MAC_FIRST_STAT + 1)
+
+/* Permanent MAC address register */
+#define ONMPH_REG_VS_MACID0            (ONMPH_MMS_VS1 + 0x1002)
+#define ONMPH_REG_VS_MACID1            (ONMPH_MMS_VS1 + 0x1003)
+
+#define ONMPH_MACID1_UID_SHIFT         0
+#define ONMPH_MACID1_UID_MASK          GENMASK(7, ONMPH_MACID1_UID_SHIFT)
+
+/* Chip identification register */
+#define ONMPH_REG_VS_CHIPID            (ONMPH_MMS_VS1 + 0x1000)
+
+#define ONMPH_CHIPID_MODEL_SHIFT       16
+#define ONMPH_CHIPID_MODEL_MASK                GENMASK(31, ONMPH_CHIPID_MODEL_SHIFT)
+#define ONMPH_CHIPID_REVISION_SHIFT    0
+#define ONMPH_CHIPID_REVISION_MASK     GENMASK(15, ONMPH_CHIPID_REVISION_SHIFT)
+
+/* MIIM IRQ status register */
+#define ONMPH_REG_MIIM_IRQ_STATUS      (ONMPH_MMS_VS1 + 0x11)
+#define MIIM_IRQ_STATUS_RSTS_SHIFT     15
+#define MIIM_IRQ_STATUS_RSTS           BIT(MIIM_IRQ_STATUS_RSTS_SHIFT)
+
+#define ONMPH_REG_BCNCNT               (ONMPH_MMS_VS1 + 0x101C)
+
+#define ONMPH_REG_PLCADIAG             (ONMPH_MMS_VS2 + 0xCA06)
+#define ONMPH_PLCADIAG_RXINTO_SHIFT    (2)
+#define ONMPH_PLCADIAG_RXINTO          BIT(ONMPH_PLCADIAG_RXINTO_SHIFT)
+#define ONMPH_PLCADIAG_UNEXPB_SHIFT    (1)
+#define ONMPH_PLCADIAG_UNEXPB          BIT(ONMPH_PLCADIAG_UNEXPB_SHIFT)
+#define ONMPH_PLCADIAG_BCNBFTO_SHIFT   (0)
+#define ONMPH_PLCADIAG_BCNBFTO         BIT(ONMPH_PLCADIAG_BCNBFTO_SHIFT)
+#define ONMPH_PLCADIAG_ERR             (ONMPH_PLCADIAG_RXINTO | \
+                                        ONMPH_PLCADIAG_UNEXPB | \
+                                        ONMPH_PLCADIAG_BCNBFTO)
+
+/* PTP registers */
+#define ONMPH_REG_VS_PTP_SEC           (ONMPH_MMS_VS1 + 0x1010)
+#define ONMPH_REG_VS_PTP_SETSEC                (ONMPH_MMS_VS1 + 0x1012)
+#define ONMPH_REG_VS_PTP_ADJ           (ONMPH_MMS_VS1 + 0x1014)
+
+enum onmph_devlink_param_id {
+       ONMPH_DEVLINK_PARAM_ID_BASE = DEVLINK_PARAM_GENERIC_ID_MAX,
+       ONMPH_PARAM_ID_PLCA,
+};
+
+/* prototypes / forward declarations */
+extern const struct ethtool_ops onmph_ethtool_ops;
+
+struct onmph_info;
+
+struct onmph_devlink_priv {
+       struct onmph_info *onmph_priv;
+};
+
+struct onmph_info {
+       struct device *dev;
+       struct net_device *ndev;
+
+       /* model information */
+       u32 model;
+       u32 version;
+       unsigned int capabilities;
+
+       /* tasks and synchronization variables */
+       spinlock_t lock;
+       struct task_struct *thread;
+
+       /* global state variables */
+       bool event_pending;
+       unsigned int ndev_flags;
+       bool rx_flags_upd;
+
+       bool tx_fcs_calc;
+
+       signed long poll_jiff;
+
+       struct spi_device *spi;
+
+       /* statistic counters variables */
+       u64 stats_data[ONMPH_STATS_NUM];
+
+       /* PTP related variables */
+       struct ptp_clock_info ptp_clock_info;
+       struct ptp_clock *ptp_clock;
+       u32 ts_frames;
+       void *tc6;
+
+       struct devlink_ops devlink_ops;
+       struct onmph_devlink_priv *devlink_priv;
+       struct devlink *devlink;
+};
+
+int onmph_ioctl_timestamp(struct onmph_info *priv, struct ifreq *rq, int cmd);
+void onmph_ptp_unregister(struct onmph_info *priv);
+void onmph_ptp_register(struct onmph_info *priv);
+
+#endif /* NCN260XX_MACPHY_H */
+
diff --git a/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ptp.c b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ptp.c
new file mode 100644
index 000000000..8c3ebbe73
--- /dev/null
+++ b/drivers/net/ethernet/onsemi/ncn260xx/ncn260xx_ptp.c
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2026 Semiconductor Components Industries, LLC ("onsemi").
+ * onsemi's NCN260xx/TS2500 10BASE-T1S MAC-PHY driver
+ */
+
+#include "ncn260xx_macphy.h"
+
+static DEFINE_MUTEX(onmph_ptp_adj_mutex);
+
+static int onmph_ptp_get_time64(struct ptp_clock_info *ptp,
+                               struct timespec64 *ts,
+                               struct ptp_system_timestamp *ptp_sts)
+{
+       struct onmph_info *priv = container_of(ptp, struct onmph_info,
+                                              ptp_clock_info);
+       u32 data[2];
+       int ret;
+
+       ptp_read_system_prets(ptp_sts);
+       ret = oa_tc6_read_registers(priv->tc6, ONMPH_REG_VS_PTP_SEC,
+                                   &data[0], 2);
+       ptp_read_system_postts(ptp_sts);
+
+       if (!ret) {
+               ts->tv_sec = data[0];
+               ts->tv_nsec = data[1];
+       }
+
+       return ret;
+}
+
+static int onmph_ptp_set_time64(struct ptp_clock_info *ptp,
+                               const struct timespec64 *ts)
+{
+       struct onmph_info *priv = container_of(ptp, struct onmph_info,
+                                              ptp_clock_info);
+       u32 data[2];
+
+       if (ts->tv_sec >= (1ULL << 32))
+               return -ERANGE;
+
+       data[0] = (u32)ts->tv_sec;
+       data[1] = ts->tv_nsec | BIT(31); /* bit 31 = execute set command */
+
+       return oa_tc6_write_registers(priv->tc6, ONMPH_REG_VS_PTP_SETSEC,
+                                     &data[0], 2);
+}
+
+static int onmph_ptp_adjfine(struct ptp_clock_info *ptp, long scaled_ppm)
+{
+       struct onmph_info *priv = container_of(ptp, struct onmph_info,
+                                              ptp_clock_info);
+       u32 sign_bit = 0;
+       long adj;
+       u32 val;
+       u64 ppm;
+
+       if (scaled_ppm < 0) {
+               /* split sign / mod */
+               sign_bit = 1U << 31;
+               scaled_ppm = ~scaled_ppm + 1;
+       }
+
+       /**
+        * Convert unsigned scaled_ppm to atto-seconds per clock cycles.
+        * The scaled_ppm format is Qx.16 --> 1 lsb = 1/65536 ppm.
+        * The clock period of the TS2500 is 8ns (125 MHz), so 1 lsb of
+        * adj register LSB is 1 atto-sec / 8ns = 0.000125 ppm.
+        * Represented in Qx.16 format, this is 0.000125 * 2^16 = 8(.192)
+        * To convert scaled_ppm into a register value we need to divide
+        * it by the LSB value, hence adj = (scaled_ppm * 1000) / 8192 to
+        * minimize the precision loss due to the integer arithmetic.
+        * That further reduces to (scaled_ppm * 125) / 1024.
+        */
+       ppm = (u64)scaled_ppm * 125;
+       do_div(ppm, 1024);
+       adj = (long)ppm;
+
+       /* check overflow */
+       if (adj >= (1L << 28))
+               return -ERANGE;
+
+       val = (u32)adj | sign_bit;
+       return oa_tc6_write_register(priv->tc6, ONMPH_REG_VS_PTP_ADJ, val);
+}
+
+/* Implemented by using "settime" */
+static int onmph_ptp_adjtime(struct ptp_clock_info *ptp, s64 delta)
+{
+       struct ptp_system_timestamp sts;
+       struct timespec64 target;
+       unsigned int period_ms;
+       struct timespec64 now;
+       int max_iters = 3;
+       s64 scaled_ppm;
+       s64 remaining;
+       s64 target_ns;
+       int ret = 0;
+       s64 now_ns;
+       s64 num;
+       s64 den;
+
+       if (!ptp)
+               return -EINVAL;
+
+       /* Nothing to do */
+       if (delta == 0)
+               return 0;
+
+       if (mutex_lock_interruptible(&onmph_ptp_adj_mutex))
+               return -EINTR;
+
+       /* Try to slew the clock using adjfine for better accuracy. For large
+        * adjustments fall back to setting time directly.
+        */
+       remaining = delta;
+
+       while (remaining != 0 && max_iters--) {
+               s64 abs_delta = remaining > 0 ? remaining : -remaining;
+
+               /* If the adjustment is very large, more than 1 second,
+                * use settime to avoid very long slewing periods or
+                * excessive frequency offsets.
+                */
+               if (abs_delta > 1000000000LL) {
+                       memset(&sts, 0, sizeof(sts));
+                       ret = ptp->gettimex64(ptp, &now, &sts);
+                       if (!ret) {
+                               struct timespec64 delta_ts;
+
+                               if (remaining >= 0) {
+                                       delta_ts = ns_to_timespec64(remaining);
+                                       target = timespec64_add(now, delta_ts);
+                               } else {
+                                       delta_ts = ns_to_timespec64(-remaining);
+                                       target = timespec64_sub(now, delta_ts);
+                               }
+                       }
+
+                       if (target.tv_sec < 0 || target.tv_sec >= (1ULL << 32))
+                               ret = -ERANGE;
+                       else
+                               ret = ptp->settime64(ptp, &target);
+
+                       remaining = 0;
+                       break;
+               }
+
+               /* Choose a slewing period depending on magnitude */
+               if (abs_delta <= 1000000LL) /* <= 1ms */
+                       period_ms = 1000; /* 1 s */
+               else if (abs_delta <= 100000000LL) /* <= 100ms */
+                       period_ms = 10000; /* 10 s */
+               else
+                       period_ms = 60000; /* 60 s */
+
+               /* compute current time and fixed target for this iteration */
+               memset(&sts, 0, sizeof(sts));
+               ret = ptp->gettimex64(ptp, &now, &sts);
+               if (ret)
+                       break;
+
+               if (remaining >= 0)
+                       target = timespec64_add(now, ns_to_timespec64(remaining));
+               else
+                       target = timespec64_sub(now, ns_to_timespec64(-remaining));
+
+               /* Compute scaled_ppm (Qx.16). scaled_ppm = ppm * 2^16
+                * ppm = (delta_seconds / period_seconds) * 1e6
+                * => scaled_ppm = delta_ns * 65536 / (period_ms * 1000)
+                */
+               num = remaining * 65536LL;
+               den = (s64)period_ms * 1000LL;
+
+               /* Integer division rounds toward zero; keep sign in numerator */
+               scaled_ppm = div_s64(num, den);
+
+               /* Apply frequency adjustment */
+               ret = ptp->adjfine(ptp, (long)scaled_ppm);
+               if (ret)
+                       break;
+
+               /* Sleep for the slew period (interruptible). If interrupted, clear
+                * the adjfine and return with -EINTR.
+                */
+               if (msleep_interruptible(period_ms)) {
+                       /* Clear adjfine */
+                       ptp->adjfine(ptp, 0);
+                       ret = -EINTR;
+                       break;
+               }
+
+               /* Clear adjfine and measure remaining offset */
+               ptp->adjfine(ptp, 0);
+
+               memset(&sts, 0, sizeof(sts));
+               ret = ptp->gettimex64(ptp, &now, &sts);
+               if (ret)
+                       break;
+
+               /* remaining = target - now (in ns) */
+               target_ns = timespec64_to_ns(&target);
+               now_ns = timespec64_to_ns(&now);
+               remaining = target_ns - now_ns;
+
+               /* If remaining is small (< 1us), finish */
+               if (remaining > -1000 && remaining < 1000)
+                       remaining = 0;
+       }
+
+       mutex_unlock(&onmph_ptp_adj_mutex);
+       return ret;
+}
+
+int onmph_ioctl_timestamp(struct onmph_info *priv, struct ifreq *rq, int cmd)
+{
+       if (!(priv->capabilities & ONMPH_CAP_PTP))
+               return -EOPNOTSUPP;
+       return oa_tc6_hwtstamp_ioctl(priv->tc6, rq, cmd);
+}
+
+/* Support is not available for alarms, programmable periodic signals, pin
+ * configuration, external timestamping, programmable pins, and PPS support.
+ */
+void onmph_ptp_register(struct onmph_info *priv)
+{
+       struct ptp_clock_info *info = &priv->ptp_clock_info;
+
+       snprintf(info->name, sizeof(info->name), "%s", "TS2500 PTP Clock");
+       info->max_adj = 100000000;
+       info->owner = THIS_MODULE;
+       info->adjfine = onmph_ptp_adjfine;
+       info->gettimex64 = onmph_ptp_get_time64;
+       info->settime64 = onmph_ptp_set_time64;
+       info->adjtime = onmph_ptp_adjtime;
+
+       priv->ptp_clock = ptp_clock_register(info, priv->dev);
+       if (IS_ERR(priv->ptp_clock)) {
+               dev_err(&priv->spi->dev, "Registration of %s failed",
+                       info->name);
+               return;
+       }
+       dev_info(&priv->spi->dev, "Registered %s index %d", info->name,
+                ptp_clock_index(priv->ptp_clock));
+}
+
+void onmph_ptp_unregister(struct onmph_info *priv)
+{
+       if (priv->ptp_clock)
+               ptp_clock_unregister(priv->ptp_clock);
+}
+
--
2.43.0


Public Information


^ permalink raw reply related

* [PATCH net-next 3/5] net: ethernet: oa_tc6: hardware timestamp support added
From: Selvamani Rajagopal @ 2026-05-01 19:15 UTC (permalink / raw)
  To: Piergiorgio Beruto, parthiban.veerasooran@microchip.com,
	andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org

mii_bus structure can be allocated/populated from vendor's code
to accommodate vendor side customization.

Timestamp support implemented following OPEN Alliance 10BASE-T1x
MACPHY Serial Interface specification.

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>

removed doc from OA TC6 sources

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
 drivers/net/ethernet/oa_tc6.c | 385 ++++++++++++++++++++++++++++++----
 include/linux/oa_tc6.h        |  26 ++-
 2 files changed, 371 insertions(+), 40 deletions(-)

diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 91a906a79..092b41cb5 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -24,9 +24,16 @@
 #define OA_TC6_REG_CONFIG0                     0x0004
 #define CONFIG0_SYNC                           BIT(15)
 #define CONFIG0_ZARFE_ENABLE                   BIT(12)
+#define CONFIG0_FTSE_ENABLE                    BIT(7)
+#define CONFIG0_FTSS_ENABLE                    BIT(6)
+#define CONFIG0_FTS_ENABLE_MASK                        GENMASK(7, 6)

 /* Status Register #0 */
 #define OA_TC6_REG_STATUS0                     0x0008
+#define STATUS0_TTSCAC                         BIT(10)
+#define STATUS0_TTSCAB                         BIT(9)
+#define STATUS0_TTSCAA                         BIT(8)
+#define STATUS0_TTSCA_MASK             GENMASK(10, 8)
 #define STATUS0_RESETC                         BIT(6)  /* Reset Complete */
 #define STATUS0_HEADER_ERROR                   BIT(5)
 #define STATUS0_LOSS_OF_FRAME_ERROR            BIT(4)
@@ -40,10 +47,14 @@

 /* Interrupt Mask Register #0 */
 #define OA_TC6_REG_INT_MASK0                   0x000C
+#define INT_MASK0_TTSCACM                      BIT(10)
+#define INT_MASK0_TTSCABM                      BIT(9)
+#define INT_MASK0_TTSCAAM                      BIT(8)
 #define INT_MASK0_HEADER_ERR_MASK              BIT(5)
 #define INT_MASK0_LOSS_OF_FRAME_ERR_MASK       BIT(4)
 #define INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK  BIT(3)
 #define INT_MASK0_TX_PROTOCOL_ERR_MASK         BIT(0)
+#define INT_MASK0_TTSCA_MASK                   GENMASK(10, 8)

 /* PHY Clause 22 registers base address and mask */
 #define OA_TC6_PHY_STD_REG_ADDR_BASE           0xFF00
@@ -64,6 +75,7 @@
 #define OA_TC6_DATA_HEADER_START_WORD_OFFSET   GENMASK(19, 16)
 #define OA_TC6_DATA_HEADER_END_VALID           BIT(14)
 #define OA_TC6_DATA_HEADER_END_BYTE_OFFSET     GENMASK(13, 8)
+#define OA_TC6_DATA_HEADER_TSC_OFFSET          GENMASK(7, 6)
 #define OA_TC6_DATA_HEADER_PARITY              BIT(0)

 /* Data footer */
@@ -75,18 +87,11 @@
 #define OA_TC6_DATA_FOOTER_START_VALID         BIT(20)
 #define OA_TC6_DATA_FOOTER_START_WORD_OFFSET   GENMASK(19, 16)
 #define OA_TC6_DATA_FOOTER_END_VALID           BIT(14)
+#define OA_TC6_DATA_FOOTER_RTSA_VALID          BIT(7)
+#define OA_TC6_DATA_FOOTER_RTSP_VALID          BIT(6)
 #define OA_TC6_DATA_FOOTER_END_BYTE_OFFSET     GENMASK(13, 8)
 #define OA_TC6_DATA_FOOTER_TX_CREDITS          GENMASK(5, 1)

-/* PHY - Clause 45 registers memory map selector (MMS) as per table 6 in the
- * OPEN Alliance specification.
- */
-#define OA_TC6_PHY_C45_PCS_MMS2                        2       /* MMD 3 */
-#define OA_TC6_PHY_C45_PMA_PMD_MMS3            3       /* MMD 1 */
-#define OA_TC6_PHY_C45_VS_PLCA_MMS4            4       /* MMD 31 */
-#define OA_TC6_PHY_C45_AUTO_NEG_MMS5           5       /* MMD 7 */
-#define OA_TC6_PHY_C45_POWER_UNIT_MMS6         6       /* MMD 13 */
-
 #define OA_TC6_CTRL_HEADER_SIZE                        4
 #define OA_TC6_CTRL_REG_VALUE_SIZE             4
 #define OA_TC6_CTRL_IGNORED_SIZE               4
@@ -105,9 +110,25 @@
 #define STATUS0_RESETC_POLL_DELAY              1000
 #define STATUS0_RESETC_POLL_TIMEOUT            1000000

+#define OA_TC6_REG_TTSCA_HIGH                  (0x1010)
+#define OA_TC6_TSTAMP_SZ                       8
+
+#define OA_TC6_TTSCA_REG_ID                    1
+#define OA_TC6_TTSCB_REG_ID                    2
+#define OA_TC6_TTSCC_REG_ID                    3
+
+struct oa_tc6_ts_info_rx {
+       bool rtsa;
+       bool rtsp;
+};
+
+struct oa_tc6_ts_info_tx {
+       u8 tsc;
+};
+
 /* Internal structure for MAC-PHY drivers */
 struct oa_tc6 {
-       struct device *dev;
+       void *priv;  /* Vendor's private driver data structure */
        struct net_device *netdev;
        struct phy_device *phydev;
        struct mii_bus *mdiobus;
@@ -129,6 +150,9 @@ struct oa_tc6 {
        u8 rx_chunks_available;
        bool rx_buf_overflow;
        bool int_flag;
+       u8 tx_ts_idx;
+       struct hwtstamp_config ts_config;
+       struct list_head tx_ts_skb_q;
 };

 enum oa_tc6_header_type {
@@ -156,6 +180,210 @@ enum oa_tc6_data_end_valid_info {
        OA_TC6_DATA_END_VALID,
 };

+static inline struct oa_tc6_ts_info_tx *oa_tc6_tsinfo_tx(struct sk_buff *skb)
+{
+       return (struct oa_tc6_ts_info_tx *)((skb)->cb);
+}
+
+static inline struct oa_tc6_ts_info_rx *oa_tc6_tsinfo_rx(struct sk_buff *skb)
+{
+       return (struct oa_tc6_ts_info_rx *)((skb)->cb);
+}
+
+static void oa_tc6_defer_for_hwtstamp(struct oa_tc6 *tc6, struct sk_buff *skb)
+{
+       if (tc6->ts_config.tx_type != HWTSTAMP_TX_ON || !skb ||
+           (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP) == 0)
+               return;
+
+       skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
+       u8 ret = tc6->tx_ts_idx++;
+
+       if (ret == OA_TC6_TTSCC_REG_ID)
+               tc6->tx_ts_idx = OA_TC6_TTSCA_REG_ID;
+       oa_tc6_tsinfo_tx(skb)->tsc = ret;
+
+       list_add_tail(&skb->list, &tc6->tx_ts_skb_q);
+}
+
+static int oa_tc6_process_deferred_skb(struct oa_tc6 *tc6, u8 tsc)
+{
+       struct skb_shared_hwtstamps tstamp;
+       struct oa_tc6_ts_info_tx *ski;
+       struct sk_buff *skb, *tmp;
+       bool found = false;
+       int ret = 0;
+
+       /* Size of data must match OA_TC6_TSTAMP_SZ */
+       u32 data[2];
+
+       list_for_each_entry_safe(skb, tmp, &tc6->tx_ts_skb_q, list) {
+               ski = oa_tc6_tsinfo_tx(skb);
+               if (ski->tsc != tsc)
+                       continue;
+               if (found)
+                       dev_warn(&tc6->spi->dev, "Multiple skb with tsc = %d\n", tsc);
+               found = true;
+               list_del(&skb->list);
+
+               // Retrieve the timestamping info
+               ret = oa_tc6_read_registers(tc6,
+                                           OA_TC6_REG_TTSCA_HIGH +
+                                           2 * (tsc - 1), &data[0], 2);
+
+               if (!ret) {
+                       tstamp.hwtstamp = ktime_set(data[0], data[1]);
+                       skb_tstamp_tx(skb, &tstamp);
+               }
+
+               dev_kfree_skb(skb);
+       }
+       return ret;
+}
+
+static int oa_tc6_set_hwtstamp_settings(struct oa_tc6 *tc6)
+{
+       u32 cfg0, irqm, status0;
+       int ret;
+
+       ret = oa_tc6_read_register(tc6, OA_TC6_REG_CONFIG0, &cfg0);
+       if (ret) {
+               dev_err(&tc6->spi->dev, "Failed to read CFG0 register\n");
+               goto out;
+       }
+
+       ret = oa_tc6_read_register(tc6, OA_TC6_REG_INT_MASK0, &irqm);
+       if (ret) {
+               dev_err(&tc6->spi->dev, "failed to read IRQM register\n");
+               goto out;
+       }
+
+       if (tc6->ts_config.tx_type == HWTSTAMP_TX_ON ||
+           tc6->ts_config.rx_filter == HWTSTAMP_FILTER_ALL)
+               cfg0 |= CONFIG0_FTS_ENABLE_MASK;
+       else
+               cfg0 &= ~CONFIG0_FTS_ENABLE_MASK;
+
+       if (tc6->ts_config.tx_type == HWTSTAMP_TX_ON)
+               irqm &= ~INT_MASK0_TTSCA_MASK;
+       else
+               irqm |= INT_MASK0_TTSCA_MASK;
+
+       /* Clear timestamp related IRQs */
+       status0 = STATUS0_TTSCA_MASK;
+       ret = oa_tc6_write_register(tc6, OA_TC6_REG_STATUS0, status0);
+       if (ret) {
+               dev_err(&tc6->spi->dev, "failed to write STATUS0 register\n");
+               goto out;
+       }
+
+       // apply configuration
+       ret = oa_tc6_write_register(tc6, OA_TC6_REG_INT_MASK0, irqm);
+       if (ret) {
+               dev_err(&tc6->spi->dev, "failed to write IRQM register\n");
+               goto out;
+       }
+
+       ret = oa_tc6_write_register(tc6, OA_TC6_REG_CONFIG0, cfg0);
+       if (ret) {
+               dev_err(&tc6->spi->dev, "failed to write CFG0 register\n");
+               goto out;
+       }
+out:
+       return ret;
+}
+
+/**
+ * oa_tc6_hwtstamp_get - gets hardware timestamp config
+ * @tc6: oa_tc6 struct.
+ * @cfg: kernel copy of hardware timestamp config
+ */
+void oa_tc6_hwtstamp_get(struct oa_tc6 *tc6, struct kernel_hwtstamp_config *cfg)
+{
+       cfg->tx_type = tc6->ts_config.tx_type;
+       cfg->rx_filter = tc6->ts_config.rx_filter;
+}
+EXPORT_SYMBOL_GPL(oa_tc6_hwtstamp_get);
+
+/**
+ * oa_tc6_hwtstamp_set - sets hardware timestamp config
+ * @tc6: oa_tc6 struct.
+ * @cfg: kernel copy of hardware timestamp config
+ *
+ * Return: 0 on success otherwise failed.
+ */
+int oa_tc6_hwtstamp_set(struct oa_tc6 *tc6, struct kernel_hwtstamp_config *cfg)
+{
+       switch (cfg->tx_type) {
+       case HWTSTAMP_TX_OFF:
+       case HWTSTAMP_TX_ON:
+               break;
+       default:
+               return -ERANGE;
+       }
+       tc6->ts_config.tx_type = cfg->tx_type;
+       if (cfg->rx_filter == HWTSTAMP_FILTER_NONE)
+               tc6->ts_config.rx_filter = cfg->rx_filter;
+       else
+               tc6->ts_config.rx_filter = HWTSTAMP_FILTER_ALL;
+       return oa_tc6_set_hwtstamp_settings(tc6);
+}
+EXPORT_SYMBOL_GPL(oa_tc6_hwtstamp_set);
+
+/**
+ * oa_tc6_hwtstamp_ioctl - ioctl interface for hardware timestamp
+ * @tc6: oa_tc6 struct.
+ * @rq: request from socket interface
+ * @cmd: value to set/get timestamp configuration
+ *
+ * Return: 0 on success otherwise failed.
+ */
+int oa_tc6_hwtstamp_ioctl(struct oa_tc6 *tc6, struct ifreq *rq, int cmd)
+{
+       struct kernel_hwtstamp_config kcfg;
+       struct hwtstamp_config tscfg;
+       int ret = 0;
+
+       switch (cmd) {
+       case SIOCSHWTSTAMP:
+               if (copy_from_user(&tscfg, rq->ifr_data, sizeof(tscfg)))
+                       return -EFAULT;
+
+               if (tscfg.flags)
+                       return -EINVAL;
+               memset(&kcfg, 0, sizeof(kcfg));
+               kcfg.tx_type = tscfg.tx_type;
+               kcfg.rx_filter = tscfg.rx_filter;
+
+               ret = oa_tc6_hwtstamp_set(tc6, &kcfg);
+               break;
+
+       case SIOCGHWTSTAMP:
+               break;
+       default:
+               return -EOPNOTSUPP;
+       }
+       if (copy_to_user(rq->ifr_data, &tc6->ts_config,
+                        sizeof(tc6->ts_config)))
+               ret = -EFAULT;
+
+       return ret;
+}
+EXPORT_SYMBOL_GPL(oa_tc6_hwtstamp_ioctl);
+
+static void oa_tc6_events_handle(struct oa_tc6 *tc6, u32 val)
+{
+       /* check TX timestamping */
+       if (val & STATUS0_TTSCAA)
+               oa_tc6_process_deferred_skb(tc6, OA_TC6_TTSCA_REG_ID);
+
+       if (val & STATUS0_TTSCAB)
+               oa_tc6_process_deferred_skb(tc6, OA_TC6_TTSCB_REG_ID);
+
+       if (val & STATUS0_TTSCAC)
+               oa_tc6_process_deferred_skb(tc6, OA_TC6_TTSCC_REG_ID);
+}
+
 static int oa_tc6_spi_transfer(struct oa_tc6 *tc6,
                               enum oa_tc6_header_type header_type, u16 length)
 {
@@ -177,7 +405,7 @@ static int oa_tc6_spi_transfer(struct oa_tc6 *tc6,
        return spi_sync(tc6->spi, &msg);
 }

-static int oa_tc6_get_parity(u32 p)
+static u32 oa_tc6_get_parity(u32 p)
 {
        /* Public domain code snippet, lifted from
         * http://www-graphics.stanford.edu/~seander/bithacks.html
@@ -422,7 +650,7 @@ static int oa_tc6_mdiobus_read(struct mii_bus *bus, int addr, int regnum)
 {
        struct oa_tc6 *tc6 = bus->priv;
        u32 regval;
-       bool ret;
+       int ret;

        ret = oa_tc6_read_register(tc6, OA_TC6_PHY_STD_REG_ADDR_BASE |
                                   (regnum & OA_TC6_PHY_STD_REG_ADDR_MASK),
@@ -496,15 +724,20 @@ static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
 {
        int ret;

-       tc6->mdiobus = mdiobus_alloc();
+       /* If vendor didn't provide mdiobus APIs, generic APIs are used */
        if (!tc6->mdiobus) {
-               netdev_err(tc6->netdev, "MDIO bus alloc failed\n");
-               return -ENOMEM;
+               tc6->mdiobus = mdiobus_alloc();
+               if (!tc6->mdiobus) {
+                       netdev_err(tc6->netdev, "MDIO bus alloc failed\n");
+                       return -ENOMEM;
+               }
+               tc6->mdiobus->read = oa_tc6_mdiobus_read;
+               tc6->mdiobus->write = oa_tc6_mdiobus_write;
+               tc6->mdiobus->read_c45 = oa_tc6_mdiobus_read_c45;
+               tc6->mdiobus->write_c45 = oa_tc6_mdiobus_write_c45;
+               tc6->mdiobus->name = "oa-tc6-mdiobus";
        }

-       tc6->mdiobus->priv = tc6;
-       tc6->mdiobus->read = oa_tc6_mdiobus_read;
-       tc6->mdiobus->write = oa_tc6_mdiobus_write;
        /* OPEN Alliance 10BASE-T1x compliance MAC-PHYs will have both C22 and
         * C45 registers space. If the PHY is discovered via C22 bus protocol it
         * assumes it uses C22 protocol and always uses C22 registers indirect
@@ -515,10 +748,8 @@ static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
         * drivers can set .read_mmd/.write_mmd in the PHY driver to call
         * .read_c45/.write_c45. Ex: drivers/net/phy/microchip_t1s.c
         */
-       tc6->mdiobus->read_c45 = oa_tc6_mdiobus_read_c45;
-       tc6->mdiobus->write_c45 = oa_tc6_mdiobus_write_c45;
-       tc6->mdiobus->name = "oa-tc6-mdiobus";
-       tc6->mdiobus->parent = tc6->dev;
+       tc6->mdiobus->priv = tc6;
+       tc6->mdiobus->parent = &tc6->spi->dev;

        snprintf(tc6->mdiobus->id, ARRAY_SIZE(tc6->mdiobus->id), "%s",
                 dev_name(&tc6->spi->dev));
@@ -583,7 +814,7 @@ static void oa_tc6_phy_exit(struct oa_tc6 *tc6)
        oa_tc6_mdiobus_unregister(tc6);
 }

-static int oa_tc6_read_status0(struct oa_tc6 *tc6)
+static u32 oa_tc6_read_status0(struct oa_tc6 *tc6)
 {
        u32 regval;
        int ret;
@@ -681,6 +912,9 @@ static int oa_tc6_process_extended_status(struct oa_tc6 *tc6)
                return ret;
        }

+       if ((value & STATUS0_TTSCA_MASK) != 0)
+               oa_tc6_events_handle(tc6, value & STATUS0_TTSCA_MASK);
+
        /* Clear the error interrupts status */
        ret = oa_tc6_write_register(tc6, OA_TC6_REG_STATUS0, value);
        if (ret) {
@@ -750,12 +984,42 @@ static int oa_tc6_process_rx_chunk_footer(struct oa_tc6 *tc6, u32 footer)
        return 0;
 }

+static void oa_tc6_update_ts_in_rx_skb(struct oa_tc6 *tc6)
+{
+       struct sk_buff *skb = tc6->rx_skb;
+       struct oa_tc6_ts_info_rx *ski;
+       u32 ts[2];
+
+       ski = oa_tc6_tsinfo_rx(skb);
+       if (!ski->rtsa)
+               return;
+
+       ts[0] = be32_to_cpu(*((u32 *)(skb->data)));
+       ts[1] = be32_to_cpu(*((u32 *)(skb->data) + 1));
+
+       /* Check parity */
+       if ((oa_tc6_get_parity(ts[0]) ^ oa_tc6_get_parity(ts[1])) ==
+           !ski->rtsp) {
+               struct skb_shared_hwtstamps *hw_ts;
+               /* Report timestamp to the upper layers */
+               hw_ts = skb_hwtstamps(skb);
+               memset(hw_ts, 0, sizeof(*hw_ts));
+               hw_ts->hwtstamp = ktime_set(ts[0], ts[1]);
+       }
+       skb_pull(skb, sizeof(ts));
+}
+
 static void oa_tc6_submit_rx_skb(struct oa_tc6 *tc6)
 {
+       oa_tc6_update_ts_in_rx_skb(tc6);
+
        tc6->rx_skb->protocol = eth_type_trans(tc6->rx_skb, tc6->netdev);
        tc6->netdev->stats.rx_packets++;
        tc6->netdev->stats.rx_bytes += tc6->rx_skb->len;

+       if ((tc6->netdev->hw_features & NETIF_F_RXFCS) != 0)
+               skb_trim(tc6->rx_skb, tc6->rx_skb->len - ETH_FCS_LEN);
+
        netif_rx(tc6->rx_skb);

        tc6->rx_skb = NULL;
@@ -766,24 +1030,29 @@ static void oa_tc6_update_rx_skb(struct oa_tc6 *tc6, u8 *payload, u8 length)
        memcpy(skb_put(tc6->rx_skb, length), payload, length);
 }

-static int oa_tc6_allocate_rx_skb(struct oa_tc6 *tc6)
+static int oa_tc6_allocate_rx_skb(struct oa_tc6 *tc6, u32 footer)
 {
+       struct oa_tc6_ts_info_rx *ski;
+
        tc6->rx_skb = netdev_alloc_skb_ip_align(tc6->netdev, tc6->netdev->mtu +
-                                               ETH_HLEN + ETH_FCS_LEN);
+                                               ETH_HLEN + ETH_FCS_LEN + OA_TC6_TSTAMP_SZ);
        if (!tc6->rx_skb) {
                tc6->netdev->stats.rx_dropped++;
                return -ENOMEM;
        }

+       ski = oa_tc6_tsinfo_rx(tc6->rx_skb);
+       ski->rtsa = FIELD_GET(OA_TC6_DATA_FOOTER_RTSA_VALID, footer);
+       ski->rtsp = FIELD_GET(OA_TC6_DATA_FOOTER_RTSP_VALID, footer);
        return 0;
 }

 static int oa_tc6_prcs_complete_rx_frame(struct oa_tc6 *tc6, u8 *payload,
-                                        u16 size)
+                                        u16 size, u32 footer)
 {
        int ret;

-       ret = oa_tc6_allocate_rx_skb(tc6);
+       ret = oa_tc6_allocate_rx_skb(tc6, footer);
        if (ret)
                return ret;

@@ -794,11 +1063,11 @@ static int oa_tc6_prcs_complete_rx_frame(struct oa_tc6 *tc6, u8 *payload,
        return 0;
 }

-static int oa_tc6_prcs_rx_frame_start(struct oa_tc6 *tc6, u8 *payload, u16 size)
+static int oa_tc6_prcs_rx_frame_start(struct oa_tc6 *tc6, u8 *payload, u16 size, u32 footer)
 {
        int ret;

-       ret = oa_tc6_allocate_rx_skb(tc6);
+       ret = oa_tc6_allocate_rx_skb(tc6, footer);
        if (ret)
                return ret;

@@ -843,7 +1112,7 @@ static int oa_tc6_prcs_rx_chunk_payload(struct oa_tc6 *tc6, u8 *data,
                size = end_byte_offset + 1 - start_byte_offset;
                return oa_tc6_prcs_complete_rx_frame(tc6,
                                                     &data[start_byte_offset],
-                                                    size);
+                                                    size, footer);
        }

        /* Process the chunk with only rx frame start */
@@ -851,7 +1120,7 @@ static int oa_tc6_prcs_rx_chunk_payload(struct oa_tc6 *tc6, u8 *data,
                size = OA_TC6_CHUNK_PAYLOAD_SIZE - start_byte_offset;
                return oa_tc6_prcs_rx_frame_start(tc6,
                                                  &data[start_byte_offset],
-                                                 size);
+                                                 size, footer);
        }

        /* Process the chunk with only rx frame end */
@@ -876,7 +1145,7 @@ static int oa_tc6_prcs_rx_chunk_payload(struct oa_tc6 *tc6, u8 *data,
                size = OA_TC6_CHUNK_PAYLOAD_SIZE - start_byte_offset;
                return oa_tc6_prcs_rx_frame_start(tc6,
                                                  &data[start_byte_offset],
-                                                 size);
+                                                 size, footer);
        }

        /* Process the chunk with ongoing rx frame data */
@@ -930,13 +1199,15 @@ static int oa_tc6_process_spi_data_rx_buf(struct oa_tc6 *tc6, u16 length)
 }

 static __be32 oa_tc6_prepare_data_header(bool data_valid, bool start_valid,
-                                        bool end_valid, u8 end_byte_offset)
+                                        bool end_valid, u8 end_byte_offset,
+                                        u8 tsc)
 {
        u32 header = FIELD_PREP(OA_TC6_DATA_HEADER_DATA_NOT_CTRL,
                                OA_TC6_DATA_HEADER) |
                     FIELD_PREP(OA_TC6_DATA_HEADER_DATA_VALID, data_valid) |
                     FIELD_PREP(OA_TC6_DATA_HEADER_START_VALID, start_valid) |
                     FIELD_PREP(OA_TC6_DATA_HEADER_END_VALID, end_valid) |
+                    FIELD_PREP(OA_TC6_DATA_HEADER_TSC_OFFSET, tsc) |
                     FIELD_PREP(OA_TC6_DATA_HEADER_END_BYTE_OFFSET,
                                end_byte_offset);

@@ -955,6 +1226,7 @@ static void oa_tc6_add_tx_skb_to_spi_buf(struct oa_tc6 *tc6)
        enum oa_tc6_data_start_valid_info start_valid;
        u8 end_byte_offset = 0;
        u16 length_to_copy;
+       u8 tsc = 0;

        /* Initial value is assigned here to avoid more than 80 characters in
         * the declaration place.
@@ -964,8 +1236,10 @@ static void oa_tc6_add_tx_skb_to_spi_buf(struct oa_tc6 *tc6)
        /* Set start valid if the current tx chunk contains the start of the tx
         * ethernet frame.
         */
-       if (!tc6->tx_skb_offset)
+       if (!tc6->tx_skb_offset) {
                start_valid = OA_TC6_DATA_START_VALID;
+               tsc = oa_tc6_tsinfo_tx(tc6->ongoing_tx_skb)->tsc;
+       }

        /* If the remaining tx skb length is more than the chunk payload size of
         * 64 bytes then copy only 64 bytes and leave the ongoing tx skb for
@@ -986,12 +1260,17 @@ static void oa_tc6_add_tx_skb_to_spi_buf(struct oa_tc6 *tc6)
                tc6->tx_skb_offset = 0;
                tc6->netdev->stats.tx_bytes += tc6->ongoing_tx_skb->len;
                tc6->netdev->stats.tx_packets++;
-               kfree_skb(tc6->ongoing_tx_skb);
+
+               /* Free the ones that are not saved for later processing,
+                * like timestamping.
+                */
+               if (!(skb_shinfo(tc6->ongoing_tx_skb)->tx_flags & SKBTX_IN_PROGRESS))
+                       kfree_skb(tc6->ongoing_tx_skb);
                tc6->ongoing_tx_skb = NULL;
        }

        *tx_buf = oa_tc6_prepare_data_header(OA_TC6_DATA_VALID, start_valid,
-                                            end_valid, end_byte_offset);
+                                            end_valid, end_byte_offset, tsc);
        tc6->spi_data_tx_buf_offset += OA_TC6_CHUNK_SIZE;
 }

@@ -1009,6 +1288,8 @@ static u16 oa_tc6_prepare_spi_tx_buf_for_tx_skbs(struct oa_tc6 *tc6)
                        tc6->ongoing_tx_skb = tc6->waiting_tx_skb;
                        tc6->waiting_tx_skb = NULL;
                        spin_unlock_bh(&tc6->tx_skb_lock);
+                       oa_tc6_defer_for_hwtstamp(tc6,
+                                                 tc6->ongoing_tx_skb);
                }
                if (!tc6->ongoing_tx_skb)
                        break;
@@ -1025,7 +1306,7 @@ static void oa_tc6_add_empty_chunks_to_spi_buf(struct oa_tc6 *tc6,

        header = oa_tc6_prepare_data_header(OA_TC6_DATA_INVALID,
                                            OA_TC6_DATA_START_INVALID,
-                                           OA_TC6_DATA_END_INVALID, 0);
+                                           OA_TC6_DATA_END_INVALID, 0, false);

        while (needed_empty_chunks--) {
                __be32 *tx_buf = tc6->spi_data_tx_buf +
@@ -1071,7 +1352,11 @@ static int oa_tc6_try_spi_transfer(struct oa_tc6 *tc6)

                if (tc6->int_flag) {
                        tc6->int_flag = false;
-                       if (spi_len == 0) {
+
+                       /* If nothing to transmit and interface isn't up,
+                        * avoid sending empty chunks.
+                        */
+                       if (spi_len == 0 && netif_running(tc6->netdev)) {
                                oa_tc6_add_empty_chunks_to_spi_buf(tc6, 1);
                                spi_len = OA_TC6_CHUNK_SIZE;
                        }
@@ -1216,6 +1501,7 @@ netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb)
        spin_lock_bh(&tc6->tx_skb_lock);
        tc6->waiting_tx_skb = skb;
        spin_unlock_bh(&tc6->tx_skb_lock);
+       oa_tc6_tsinfo_tx(skb)->tsc = 0;

        /* Wake spi kthread to perform spi transfer */
        wake_up_interruptible(&tc6->spi_wq);
@@ -1226,13 +1512,17 @@ EXPORT_SYMBOL_GPL(oa_tc6_start_xmit);

 /**
  * oa_tc6_init - allocates and initializes oa_tc6 structure.
+ * @priv: vendor driver's private structure
  * @spi: device with which data will be exchanged.
  * @netdev: network device interface structure.
+ * @bus: vendor allocated mii_bus structure. Can be NULL.
  *
  * Return: pointer reference to the oa_tc6 structure if the MAC-PHY
  * initialization is successful otherwise NULL.
  */
-struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
+struct oa_tc6 *oa_tc6_init(void *priv, struct spi_device *spi,
+                          struct net_device *netdev,
+                          struct mii_bus *bus)
 {
        struct oa_tc6 *tc6;
        int ret;
@@ -1241,11 +1531,15 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
        if (!tc6)
                return NULL;

+       tc6->priv = priv;
+       tc6->mdiobus = bus;
+       tc6->tx_ts_idx = OA_TC6_TTSCA_REG_ID;
        tc6->spi = spi;
        tc6->netdev = netdev;
        SET_NETDEV_DEV(netdev, &spi->dev);
        mutex_init(&tc6->spi_ctrl_lock);
        spin_lock_init(&tc6->tx_skb_lock);
+       INIT_LIST_HEAD(&tc6->tx_ts_skb_q);

        /* Set the SPI controller to pump at realtime priority */
        tc6->spi->rt = true;
@@ -1350,6 +1644,19 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
 }
 EXPORT_SYMBOL_GPL(oa_tc6_init);

+/**
+ * oa_tc6_priv - get vendor private structure
+ * @tc6: oa_tc6 struct
+ *
+ * Return: pointer reference to the vendor's private structure.
+ *         Must be called after successful call to oa_tc6_init.
+ */
+void *oa_tc6_priv(struct oa_tc6 *tc6)
+{
+       return tc6->priv;
+}
+EXPORT_SYMBOL_GPL(oa_tc6_priv);
+
 /**
  * oa_tc6_exit - exit function.
  * @tc6: oa_tc6 struct.
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 15f58e3c5..5f5822282 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -7,12 +7,27 @@
  * Author: Parthiban Veerasooran <parthiban.veerasooran@microchip.com>
  */

+#ifndef _LINUX_OA_TC6_H
+#define _LINUX_OA_TC6_H
+
 #include <linux/etherdevice.h>
 #include <linux/spi/spi.h>
+#include <linux/phy.h>
+
+/* PHY - Clause 45 registers memory map selector (MMS) as per table 6 in the
+ * OPEN Alliance specification.
+ */
+#define OA_TC6_PHY_C45_PCS_MMS2                 2       /* MMD 3 */
+#define OA_TC6_PHY_C45_PMA_PMD_MMS3             3       /* MMD 1 */
+#define OA_TC6_PHY_C45_VS_PLCA_MMS4             4       /* MMD 31 */
+#define OA_TC6_PHY_C45_AUTO_NEG_MMS5            5       /* MMD 7 */
+#define OA_TC6_PHY_C45_POWER_UNIT_MMS6          6       /* MMD 13 */

 struct oa_tc6;

-struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev);
+struct oa_tc6 *oa_tc6_init(void *priv, struct spi_device *spi,
+                          struct net_device *netdev,
+                          struct mii_bus *bus);
 void oa_tc6_exit(struct oa_tc6 *tc6);
 int oa_tc6_write_register(struct oa_tc6 *tc6, u32 address, u32 value);
 int oa_tc6_write_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
@@ -22,3 +37,12 @@ int oa_tc6_read_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
                          u8 length);
 netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb);
 int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6);
+int oa_tc6_hwtstamp_ioctl(struct oa_tc6 *tc6, struct ifreq *rq, int cmd);
+int oa_tc6_hwtstamp_set(struct oa_tc6 *tc6,
+                       struct kernel_hwtstamp_config *cfg);
+void oa_tc6_hwtstamp_get(struct oa_tc6 *tc6,
+                        struct kernel_hwtstamp_config *cfg);
+void *oa_tc6_priv(struct oa_tc6 *tc6);
+
+#endif /* _LINUX_OA_TC6_H */
+
--
2.43.0


Public Information


^ permalink raw reply related

* [PATCH net-next 2/5] net: phy: ncn26000: add support for TS2500 internal PHY
From: Selvamani Rajagopal @ 2026-05-01 19:15 UTC (permalink / raw)
  To: Piergiorgio Beruto, andrew@lunn.ch, hkallweit1@gmail.com,
	linux@armlinux.org.uk, davem@davemloft.net, edumazet@google.com,
	kuba@kernel.org, pabeni@redhat.com, netdev@vger.kernel.org,
	linux-kernel@vger.kernel.org

Support for onsemi's IEEE 802.3cg compliant Ethernet Transceiver
with integrated Media Access Controller (MACPHY). NCN26010 and
TS2500 are supported.

Updated MAINTAINERS file

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
 MAINTAINERS                |  11 ++-
 drivers/net/phy/ncn26000.c | 193 +++++++++++++++++++++++++------------
 2 files changed, 144 insertions(+), 60 deletions(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index 27a073f53..36c1b2f02 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19934,12 +19934,21 @@ S:    Maintained
 F:     arch/mips/boot/dts/ralink/omega2p.dts

 ONSEMI ETHERNET PHY DRIVERS
-M:     Piergiorgio Beruto <piergiorgio.beruto@gmail.com>
+M:     Piergiorgio Beruto <pier.beruto@onsemi.com>
+M:     Selva Rajagopal <selvamani.rajagopal@onsemi.com>
 L:     netdev@vger.kernel.org
 S:     Supported
 W:     http://www.onsemi.com
 F:     drivers/net/phy/ncn*

+ONSEMI NCN260xx/TS2500 10BASE-T1S MACPHY ETHERNET DRIVER
+M:     Piergiorgio Beruto <pier.beruto@onsemi.com>
+M:     Selva Rajagopal <selvamani.rajagopal@onsemi.com>
+L:     netdev@vger.kernel.org
+S:     Maintained
+F:     Documentation/devicetree/bindings/net/onnn,ncn260xx.yaml
+F:     drivers/net/ethernet/onsemi/ncn260xx/ncn*
+
 OP-TEE DRIVER
 M:     Jens Wiklander <jens.wiklander@linaro.org>
 L:     op-tee@lists.trustedfirmware.org (moderated for non-subscribers)
diff --git a/drivers/net/phy/ncn26000.c b/drivers/net/phy/ncn26000.c
index cabdd83c6..d4bb4b230 100644
--- a/drivers/net/phy/ncn26000.c
+++ b/drivers/net/phy/ncn26000.c
@@ -2,7 +2,7 @@
 /*
  *  Driver for the onsemi 10BASE-T1S NCN26000 PHYs family.
  *
- * Copyright 2022 onsemi
+ * Copyright 2026 onsemi
  */
 #include <linux/kernel.h>
 #include <linux/bitfield.h>
@@ -11,39 +11,43 @@
 #include <linux/module.h>
 #include <linux/mii.h>
 #include <linux/phy.h>
+#include <linux/ethtool.h>
+#include <linux/ethtool_netlink.h>
+#include <linux/slab.h>
+#include <linux/delay.h>

 #include "mdio-open-alliance.h"

 #define PHY_ID_NCN26000                        0x180FF5A1
+#define PHY_ID_TS2500                  0x180FF411

-#define NCN26000_REG_IRQ_CTL            16
-#define NCN26000_REG_IRQ_STATUS         17
+#define TO_TMR_DEFAULT                 32

-// the NCN26000 maps link_ctrl to BMCR_ANENABLE
-#define NCN26000_BCMR_LINK_CTRL_BIT    BMCR_ANENABLE
+#define NCN26000_REG_IRQ_CTL           16
+#define NCN26000_REG_IRQ_STATUS                17

-// the NCN26000 maps link_status to BMSR_ANEGCOMPLETE
-#define NCN26000_BMSR_LINK_STATUS_BIT  BMSR_ANEGCOMPLETE
+/* clause 45 vendor specific registers */
+#define NCN26000_REG_PHYCFG1_MMD       MDIO_MMD_VEND2
+#define NCN26000_REG_PHYCFG1           0x8001

 #define NCN26000_IRQ_LINKST_BIT                BIT(0)
-#define NCN26000_IRQ_PLCAST_BIT                BIT(1)
-#define NCN26000_IRQ_LJABBER_BIT       BIT(2)
-#define NCN26000_IRQ_RJABBER_BIT       BIT(3)
-#define NCN26000_IRQ_PLCAREC_BIT       BIT(4)
-#define NCN26000_IRQ_PHYSCOL_BIT       BIT(5)
-#define NCN26000_IRQ_RESET_BIT         BIT(15)

-#define TO_TMR_DEFAULT                 32
+#define NCN26000_PHYCFG1_ENI           BIT(7)
+#define NCN26000_PHYCFG1_ENI_MASK      BIT(7)

+/* Default value of the PLCA TO_TIMER should be 32. NCN26000 reports
+ * as 24. This is fixed in next version, in TS2500.
+ */
 static int ncn26000_config_init(struct phy_device *phydev)
 {
-       /* HW bug workaround: the default value of the PLCA TO_TIMER should be
-        * 32, where the current version of NCN26000 reports 24. This will be
-        * fixed in future PHY versions. For the time being, we force the
-        * correct default here.
-        */
-       return phy_write_mmd(phydev, MDIO_MMD_VEND2, MDIO_OATC14_PLCA_TOTMR,
-                            TO_TMR_DEFAULT);
+       const struct phy_driver *pdrv = phydev->drv;
+       int ret = 0;
+
+       if (pdrv && pdrv->phy_id == PHY_ID_NCN26000)
+               ret = phy_modify_mmd(phydev, MDIO_MMD_VEND2,
+                                    MDIO_OATC14_PLCA_TOTMR,
+                                    0xFFFF, TO_TMR_DEFAULT);
+       return ret;
 }

 static int ncn26000_config_aneg(struct phy_device *phydev)
@@ -55,17 +59,20 @@ static int ncn26000_config_aneg(struct phy_device *phydev)
        phydev->mdix_ctrl = ETH_TP_MDI_AUTO;
        phydev->mdix = ETH_TP_MDI;

-       // bring up the link
-       return phy_write(phydev, MII_BMCR, NCN26000_BCMR_LINK_CTRL_BIT);
+       /* NCN2600x maps link_ctrl to BMCR_ANENABLE */
+       return phy_write(phydev, MII_BMCR, BMCR_ANENABLE);
 }

+/* Link status mapped to BMSR_ANEGCOMPLETE. */
 static int ncn26000_read_status(struct phy_device *phydev)
 {
-       /* The NCN26000 reports NCN26000_LINK_STATUS_BIT if the link status of
-        * the PHY is up. It further reports the logical AND of the link status
-        * and the PLCA status in the BMSR_LSTATUS bit.
-        */
-       int ret;
+       u16 status_check = BMSR_ANEGCOMPLETE;
+       int ret = 0;
+
+       phydev->duplex = DUPLEX_HALF;
+       phydev->speed = SPEED_10;
+       phydev->autoneg = AUTONEG_DISABLE;
+       phydev->pause = 0;

        /* The link state is latched low so that momentary link
         * drops can be detected. Do not double-read the status
@@ -76,7 +83,7 @@ static int ncn26000_read_status(struct phy_device *phydev)
                ret = phy_read(phydev, MII_BMSR);
                if (ret < 0)
                        return ret;
-               else if (ret & NCN26000_BMSR_LINK_STATUS_BIT)
+               if (ret & status_check)
                        goto upd_link;
        }

@@ -85,30 +92,54 @@ static int ncn26000_read_status(struct phy_device *phydev)
                return ret;

 upd_link:
-       // update link status
-       if (ret & NCN26000_BMSR_LINK_STATUS_BIT) {
+       /* Update link status */
+       if (ret & status_check)
                phydev->link = 1;
-               phydev->pause = 0;
-               phydev->duplex = DUPLEX_HALF;
-               phydev->speed = SPEED_10;
-       } else {
+       else
                phydev->link = 0;
-               phydev->duplex = DUPLEX_UNKNOWN;
-               phydev->speed = SPEED_UNKNOWN;
-       }

        return 0;
 }

+/* We capture calls to PLCA set config to intercept PLCA enable/disable
+ * requests and set the proprietary ENI mode accordingly
+ */
+static int ncn26000_plca_set_cfg(struct phy_device *phydev,
+                                const struct phy_plca_cfg *plca_cfg)
+{
+       int ret = genphy_c45_plca_set_cfg(phydev, plca_cfg);
+       u16 cfg_enabled = 0;
+       int ctl0 = 0;
+
+       if (ret || plca_cfg->enabled < 0)
+               return ret;
+
+       ret = phy_read_mmd(phydev, MDIO_MMD_VEND2, MDIO_OATC14_PLCA_CTRL1);
+       if (ret >= 0) {
+               ctl0 = phy_read_mmd(phydev, MDIO_MMD_VEND2,
+                                   MDIO_OATC14_PLCA_CTRL0);
+               if (ctl0 >= 0)
+                       dev_info(&phydev->mdio.dev, "PLCA %s: node id=%d, count=%d",
+                                (ctl0 & MDIO_OATC14_PLCA_EN) ? "ON" : "OFF",
+                                ret & MDIO_OATC14_PLCA_ID,
+                                (ret & MDIO_OATC14_PLCA_NCNT) >> 8);
+       }
+
+       cfg_enabled = (plca_cfg->enabled) ? NCN26000_PHYCFG1_ENI : 0;
+       return phy_modify_mmd(phydev, NCN26000_REG_PHYCFG1_MMD,
+                             NCN26000_REG_PHYCFG1,
+                             NCN26000_PHYCFG1_ENI_MASK, cfg_enabled);
+}
+
 static irqreturn_t ncn26000_handle_interrupt(struct phy_device *phydev)
 {
        int ret;

-       // read and aknowledge the IRQ status register
+       /* Read and acknowledge the IRQ status register */
        ret = phy_read(phydev, NCN26000_REG_IRQ_STATUS);

-       // check only link status changes
-       if (ret < 0 || (ret & NCN26000_REG_IRQ_STATUS) == 0)
+       /* Check only link status changes */
+       if (ret < 0 || ((ret & NCN26000_IRQ_LINKST_BIT) == 0))
                return IRQ_NONE;

        phy_trigger_machine(phydev);
@@ -117,20 +148,17 @@ static irqreturn_t ncn26000_handle_interrupt(struct phy_device *phydev)

 static int ncn26000_config_intr(struct phy_device *phydev)
 {
+       u16 irqe = 0;
        int ret;
-       u16 irqe;

        if (phydev->interrupts == PHY_INTERRUPT_ENABLED) {
-               // acknowledge IRQs
+               /* Acknowledge IRQs */
                ret = phy_read(phydev, NCN26000_REG_IRQ_STATUS);
                if (ret < 0)
                        return ret;

-               // get link status notifications
+               /* Get link status notifications */
                irqe = NCN26000_IRQ_LINKST_BIT;
-       } else {
-               // disable all IRQs
-               irqe = 0;
        }

        ret = phy_write(phydev, NCN26000_REG_IRQ_CTL, irqe);
@@ -140,20 +168,66 @@ static int ncn26000_config_intr(struct phy_device *phydev)
        return 0;
 }

+static void ncn26000_remove(struct phy_device *phydev)
+{
+       phydev->priv = NULL;
+}
+
+static int ncn26000_read_mmd(struct phy_device *phydev, int dev, u16 reg)
+{
+       struct mii_bus *bus = phydev->mdio.bus;
+       int addr = phydev->mdio.addr;
+
+       return __mdiobus_c45_read(bus, addr, dev, reg);
+}
+
+static int ncn26000_write_mmd(struct phy_device *phydev, int dev,
+                             u16 reg, u16 val)
+{
+       struct mii_bus *bus = phydev->mdio.bus;
+       int addr = phydev->mdio.addr;
+
+       return __mdiobus_c45_write(bus, addr, dev, reg, val);
+}
+
 static struct phy_driver ncn26000_driver[] = {
+       {
+               PHY_ID_MATCH_MODEL(PHY_ID_TS2500),
+               .name                  = "TS2500",
+               .features              = PHY_BASIC_T1S_P2MP_FEATURES,
+               .remove                = ncn26000_remove,
+               .config_init           = ncn26000_config_init,
+               .config_intr           = ncn26000_config_intr,
+               .config_aneg           = ncn26000_config_aneg,
+               .read_status           = ncn26000_read_status,
+               .handle_interrupt      = ncn26000_handle_interrupt,
+               .set_plca_cfg          = ncn26000_plca_set_cfg,
+               .get_plca_cfg          = genphy_c45_plca_get_cfg,
+               .get_plca_status       = genphy_c45_plca_get_status,
+               .soft_reset            = genphy_soft_reset,
+               .get_sqi               = genphy_c45_oatc14_get_sqi,
+               .get_sqi_max           = genphy_c45_oatc14_get_sqi_max,
+               .read_mmd              = ncn26000_read_mmd,
+               .write_mmd             = ncn26000_write_mmd,
+               .cable_test_get_status = genphy_c45_oatc14_cable_test_get_status,
+               .cable_test_start      = genphy_c45_oatc14_cable_test_start,
+       },
        {
                PHY_ID_MATCH_MODEL(PHY_ID_NCN26000),
-               .name                   = "NCN26000",
-               .features               = PHY_BASIC_T1S_P2MP_FEATURES,
-               .config_init            = ncn26000_config_init,
-               .config_intr            = ncn26000_config_intr,
-               .config_aneg            = ncn26000_config_aneg,
-               .read_status            = ncn26000_read_status,
-               .handle_interrupt       = ncn26000_handle_interrupt,
-               .get_plca_cfg           = genphy_c45_plca_get_cfg,
-               .set_plca_cfg           = genphy_c45_plca_set_cfg,
-               .get_plca_status        = genphy_c45_plca_get_status,
-               .soft_reset             = genphy_soft_reset,
+               .name                  = "NCN26000",
+               .features              = PHY_BASIC_T1S_P2MP_FEATURES,
+               .remove                = ncn26000_remove,
+               .config_init           = ncn26000_config_init,
+               .config_intr           = ncn26000_config_intr,
+               .config_aneg           = ncn26000_config_aneg,
+               .read_status           = ncn26000_read_status,
+               .handle_interrupt      = ncn26000_handle_interrupt,
+               .set_plca_cfg          = ncn26000_plca_set_cfg,
+               .get_plca_cfg          = genphy_c45_plca_get_cfg,
+               .get_plca_status       = genphy_c45_plca_get_status,
+               .soft_reset            = genphy_soft_reset,
+               .read_mmd              = ncn26000_read_mmd,
+               .write_mmd             = ncn26000_write_mmd,
        },
 };

@@ -161,11 +235,12 @@ module_phy_driver(ncn26000_driver);

 static const struct mdio_device_id __maybe_unused ncn26000_tbl[] = {
        { PHY_ID_MATCH_MODEL(PHY_ID_NCN26000) },
+       { PHY_ID_MATCH_MODEL(PHY_ID_TS2500) },
        { }
 };

 MODULE_DEVICE_TABLE(mdio, ncn26000_tbl);

-MODULE_AUTHOR("Piergiorgio Beruto");
+MODULE_AUTHOR("Piergiorgio Beruto <Pier.Beruto@onsemi.com>");
 MODULE_DESCRIPTION("onsemi 10BASE-T1S PHY driver");
 MODULE_LICENSE("Dual BSD/GPL");
--
2.43.0


Public Information


^ permalink raw reply related

* [PATCH net-next 1/5] dt-bindings: net: add onsemi's TS2500/NCN26010 10BASE-T1S MACPHY
From: Selvamani Rajagopal @ 2026-05-01 19:15 UTC (permalink / raw)
  To: Piergiorgio Beruto, andrew+netdev@lunn.ch, davem@davemloft.net,
	edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
	robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
	netdev@vger.kernel.org, devicetree@vger.kernel.org,
	linux-kernel@vger.kernel.org

Add YAML device tree binding for the onsemi NCN26010 and TS2500
IEEE 802.3cg compliant Ethernet transceiver devices.

Signed-off-by: Selvamani Rajagopal <Selvamani.Rajagopal@onsemi.com>
---
 .../bindings/net/onnn,ncn260xx.yaml           | 71 +++++++++++++++++++
 1 file changed, 71 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/onnn,ncn260xx.yaml

diff --git a/Documentation/devicetree/bindings/net/onnn,ncn260xx.yaml b/Documentation/devicetree/bindings/net/onnn,ncn260xx.yaml
new file mode 100644
index 000000000..198cd7e9d
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/onnn,ncn260xx.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/onnn,ncn260xx.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: onsemi NCN26010/TS2500 10BASE-T1S MACPHY Ethernet Controllers
+
+maintainers:
+  - Piergiorgio Beruto <Pier.Beruto@onsemi.com>
+  - Selva Rajagopal <Selvamani.Rajagopal@onsemi.com>
+
+description: |
+  The NCN26010 and TS2500 combine a Media Access Controller (MAC) and an
+  Ethernet PHY to enable 10BASE‑T1S networks. The Ethernet Media Access
+  Controller (MAC) module implements a 10 Mbps half duplex Ethernet MAC,
+  compatible with the IEEE 802.3 standard and a 10BASE-T1S physical layer
+  transceiver integrated into the NCN26010. The communication between
+  the host and the MAC-PHY is specified in the OPEN Alliance 10BASE-T1x
+  MACPHY Serial Interface (TC6).
+
+  Specifications about the NCN26010 can be found at:
+    https://www.onsemi.com/download/data-sheet/pdf/ncn26010-d.pdf
+    https://www.onsemi.com/products/interfaces/ethernet-controllers/t30hm1ts2500
+
+allOf:
+  - $ref: /schemas/net/ethernet-controller.yaml#
+  - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+  compatible:
+    const: onnn,ncn260xx
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    description: |
+      Interrupt from MAC-PHY asserted in the event of Receive Chunks
+      Available, Transmit Chunk Credits Available and Extended Status
+      Event.
+    maxItems: 1
+
+  spi-max-frequency:
+    minimum: 15000000
+    maximum: 25000000
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - spi-max-frequency
+
+additionalProperties: false
+
+examples:
+  - |
+    spi {
+      #address-cells = <1>;
+      #size-cells = <0>;
+
+      ethernet@0 {
+        compatible = "onnn,ncn260xx";
+        reg = <0>;
+        pinctrl-names = "default";
+        interrupt-parent = <&gpio>;
+        interrupts = <25 2>;
+        status = "okay";
+        spi-max-frequency = <25000000>;
+      };
+    };
--
2.43.0


Public Information

^ permalink raw reply related

* Re: [PATCH net-next 10/12] net: stmmac: tc956x: add TC956x/QPS615 support
From: Andrew Lunn @ 2026-05-01 19:04 UTC (permalink / raw)
  To: Alex Elder
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, maxime.chevallier,
	rmk+kernel, andersson, konradybcio, robh, krzk+dt, conor+dt,
	linusw, brgl, arnd, gregkh, Daniel Thompson, mohd.anwar,
	a0987203069, alexandre.torgue, ast, boon.khai.ng, chenchuangyu,
	chenhuacai, daniel, hawk, hkallweit1, inochiama, john.fastabend,
	julianbraha, livelycarpet87, matthew.gerlach, mcoquelin.stm32, me,
	prabhakar.mahadev-lad.rj, richardcochran, rohan.g.thomas, sdf,
	siyanteng, weishangjuan, wens, netdev, bpf, linux-arm-msm,
	devicetree, linux-gpio, linux-stm32, linux-arm-kernel,
	linux-kernel
In-Reply-To: <20260501155421.3329862-11-elder@riscstar.com>

> +static struct tc956x_mac_speed mac_speed[] = {
> +	{ PHY_INTERFACE_MODE_2500BASEX,	SPEED_2500,  SP_SEL_SGMII_2500M, },
> +	{ PHY_INTERFACE_MODE_SGMII,	SPEED_2500,  SP_SEL_SGMII_2500M, },
> +	{ PHY_INTERFACE_MODE_SGMII,	SPEED_1000,  SP_SEL_SGMII_1000M, },

That looks odd. Some vendors implemented 2500BaseX using SGMII
overclocked. But that is not strictly 2500BaseX. Having the 2500BASEX
entry suggests you have real 2500BASEX, so why have an SGMII entry
with SPEED_2500?

> +/* We have one IRQ chip instance with 25 IRQs in its domain */

One per MAC, or one overall?

> +static struct irq_domain *
> +tc956x_msigen_irq_domain_instantiate(struct tc956x_data *td)
> +{
> +	struct irq_domain_chip_generic_info dgc_info;
> +	struct irq_domain_info info;
> +
> +	dgc_info.name = "tc956x-msigen";

If it is one per MAC, maybe this name should indicate which instance
of the MAC this is.

> +static int tc956x_mac_setup(void *apriv, struct mac_device_info *mac)
> +{
> +	struct stmmac_priv *priv = apriv;
> +	struct stmmac_desc_ops *desc;
> +	struct stmmac_dma_ops *dma;
> +	struct tc956x_data *td;
> +
> +	td = priv->plat->bsp_priv;
> +
> +	/* dwxgmac301_dma_ops needs extending to provide DMA address translation */
> +	dma = &td->dma;
> +	*dma = dwxgmac301_dma_ops;
> +	dma->init_rx_chan = tc956x_dma_init_rx_chan;
> +	dma->init_tx_chan = tc956x_dma_init_tx_chan;
> +	mac->dma = dma;

I could be reading this wrong....

dma points to the global dwxgmac301_dma_ops, which you added a few
patches back.

You then modify it, changing two values in it.

Doesn't that break any other dwxgmac301 in the system? Shouldn't you
be making a copy of the global structure, and then making
modifications to your copy? mac->dma then points to your copy?

> +	/* dwxgmac210_desc_ops also needs extending for the same reason */
> +	desc = &td->desc;
> +	*desc = dwxgmac210_desc_ops;
> +	desc->set_addr = tc956x_desc_set_addr;
> +	desc->set_sec_addr = tc956x_desc_set_sec_addr;
> +	mac->desc = desc;

And the same problem here?

> +/* Called by tc956x_dwmac_probe(); return errors with dev_err_probe() */
> +static int tc956x_dwmac_parse_dt(struct tc956x_data *td)
> +{
> +	struct device_node *mdio_node;
> +	struct device *dev = td->dev;
> +	struct device_node *np;
> +
> +	np = dev_of_node(dev);
> +	if (!np)
> +		return dev_err_probe(dev, -EINVAL, "no devicetree node\n");
> +
> +	/* Find the MDIO bus */
> +	for_each_child_of_node(np, mdio_node) {
> +		if (of_device_is_compatible(mdio_node,
> +					    "snps,dwmac-mdio"))
> +			break;
> +	}

It looks like if you put the ethernet properties into an ethernet node
in DT, this might go away? Or at least allow you to use
stmmac_of_get_mdio(). 

	Andrew

^ permalink raw reply

* [PATCH net-next v2 2/2] net: bcmasp: Keep PHY link during WoL sleep cycle
From: Justin Chen @ 2026-05-01 18:56 UTC (permalink / raw)
  To: netdev
  Cc: bcm-kernel-feedback-list, pabeni, kuba, edumazet, davem,
	andrew+netdev, florian.fainelli, Justin Chen
In-Reply-To: <20260501185625.422361-1-justin.chen@broadcom.com>

We currently more or less restart all the HW on resume whatever WoL is
enabled or not. The PHY is powered when WoL is enabled which means we
have no reason to re-negotiate the link on resume. So instead of doing
a full restart in this case, we do a partial restart and keep the
negotiated link. That way we can resume network taffic with a much smaller
delay. In the non-WoL case, we do not want to keep the PHY powered, so we
follow the same flow.

Signed-off-by: Justin Chen <justin.chen@broadcom.com>
---
v2
- In the resume case with a HW reset. We trigger a relink by setting the link
  to PHY_UP instead of calling phy_restart_aneg().

 .../net/ethernet/broadcom/asp2/bcmasp_intf.c  | 51 ++++++++++++++-----
 1 file changed, 37 insertions(+), 14 deletions(-)

diff --git a/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c b/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
index e2b51ec903af..9ffd0dfa5969 100644
--- a/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
+++ b/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
@@ -923,7 +923,7 @@ static void bcmasp_phy_hw_unprepare(struct bcmasp_intf *intf)
 		bcmasp_rgmii_mode_en_set(intf, false);
 }
 
-static void bcmasp_netif_deinit(struct net_device *dev)
+static void bcmasp_netif_deinit(struct net_device *dev, bool stop_phy)
 {
 	struct bcmasp_intf *intf = netdev_priv(dev);
 	u32 reg, timeout = 1000;
@@ -946,7 +946,8 @@ static void bcmasp_netif_deinit(struct net_device *dev)
 
 	umac_enable_set(intf, UMC_CMD_TX_EN, 0);
 
-	phy_stop(dev->phydev);
+	if (stop_phy)
+		phy_stop(dev->phydev);
 
 	umac_enable_set(intf, UMC_CMD_RX_EN, 0);
 
@@ -974,7 +975,7 @@ static int bcmasp_stop(struct net_device *dev)
 	/* Stop tx from updating HW */
 	netif_tx_disable(dev);
 
-	bcmasp_netif_deinit(dev);
+	bcmasp_netif_deinit(dev, true);
 
 	bcmasp_reclaim_free_buffers(intf);
 
@@ -1385,15 +1386,20 @@ int bcmasp_interface_suspend(struct bcmasp_intf *intf)
 {
 	struct device *kdev = &intf->parent->pdev->dev;
 	struct net_device *dev = intf->ndev;
+	bool wake;
 
 	if (!netif_running(dev))
 		return 0;
 
 	netif_device_detach(dev);
 
-	bcmasp_netif_deinit(dev);
+	wake = device_may_wakeup(kdev) && intf->wolopts;
 
-	if (!intf->wolopts) {
+	bcmasp_netif_deinit(dev, !wake);
+
+	if (wake) {
+		bcmasp_suspend_to_wol(intf);
+	} else {
 		bcmasp_phy_hw_unprepare(intf);
 
 		/* If Wake-on-LAN is disabled, we can safely
@@ -1402,9 +1408,6 @@ int bcmasp_interface_suspend(struct bcmasp_intf *intf)
 		bcmasp_core_clock_set_intf(intf, false);
 	}
 
-	if (device_may_wakeup(kdev) && intf->wolopts)
-		bcmasp_suspend_to_wol(intf);
-
 	clk_disable_unprepare(intf->parent->clk);
 
 	return 0;
@@ -1428,8 +1431,11 @@ static void bcmasp_resume_from_wol(struct bcmasp_intf *intf)
 
 int bcmasp_interface_resume(struct bcmasp_intf *intf)
 {
+	struct device *kdev = &intf->parent->pdev->dev;
 	struct net_device *dev = intf->ndev;
+	bool wake;
 	int ret;
+	u32 reg;
 
 	if (!netif_running(dev))
 		return 0;
@@ -1438,17 +1444,34 @@ int bcmasp_interface_resume(struct bcmasp_intf *intf)
 	if (ret)
 		return ret;
 
-	bcmasp_core_clock_set_intf(intf, true);
-
-	bcmasp_resume_from_wol(intf);
+	wake = device_may_wakeup(kdev) && intf->wolopts;
 
-	bcmasp_phy_hw_prepare(intf);
+	bcmasp_core_clock_set_intf(intf, true);
 
-	umac_reset_and_init(intf, dev->dev_addr);
+	/* The interface might be HW reset in some suspend modes, so we may
+	 * need to restore the UNIMAC/PHY if that is the case.
+	 */
+	reg = umac_rl(intf, UMC_CMD);
+	if (wake && (reg & UMC_CMD_RX_EN)) {
+		umac_enable_set(intf, UMC_CMD_TX_EN, 1);
+		bcmasp_resume_from_wol(intf);
+	} else {
+		bcmasp_phy_hw_prepare(intf);
+		umac_reset_and_init(intf, dev->dev_addr);
+	}
 
 	bcmasp_netif_init(dev);
 
-	phy_start(dev->phydev);
+	/* If HW was reset, we need to force a relink */
+	if (wake && !(reg & UMC_CMD_RX_EN)) {
+		mutex_lock(&dev->phydev->lock);
+		if (dev->phydev->state == PHY_RUNNING)
+			dev->phydev->state = PHY_UP;
+		mutex_unlock(&dev->phydev->lock);
+		phy_trigger_machine(dev->phydev);
+	} else if (!wake) {
+		phy_start(dev->phydev);
+	}
 
 	netif_device_attach(dev);
 
-- 
2.34.1


^ permalink raw reply related

* [PATCH net-next v2 1/2] net: bcmasp: Divide init to allow partial bring up
From: Justin Chen @ 2026-05-01 18:56 UTC (permalink / raw)
  To: netdev
  Cc: bcm-kernel-feedback-list, pabeni, kuba, edumazet, davem,
	andrew+netdev, florian.fainelli, Justin Chen
In-Reply-To: <20260501185625.422361-1-justin.chen@broadcom.com>

To prepare for a partial bring up of the interface during resume,
we break apart the bcmasp_netif_init() function into smaller chunks
that can be called as necessary. Also consolidate some functions that
do not need to be standalone.

Signed-off-by: Justin Chen <justin.chen@broadcom.com>
---
v2
- Moved tx_lpi_timer read to only bcmasp_open(). We don't want to overwrite
  the SW ctx with the HW default in the resume/suspend case.

 .../net/ethernet/broadcom/asp2/bcmasp_intf.c  | 210 ++++++++----------
 1 file changed, 98 insertions(+), 112 deletions(-)

diff --git a/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c b/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
index ec63f50a849e..e2b51ec903af 100644
--- a/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
+++ b/drivers/net/ethernet/broadcom/asp2/bcmasp_intf.c
@@ -344,40 +344,35 @@ static netdev_tx_t bcmasp_xmit(struct sk_buff *skb, struct net_device *dev)
 	return NETDEV_TX_OK;
 }
 
-static void bcmasp_netif_start(struct net_device *dev)
+static void umac_reset_and_init(struct bcmasp_intf *intf,
+				const unsigned char *addr)
 {
-	struct bcmasp_intf *intf = netdev_priv(dev);
-
-	bcmasp_set_rx_mode(dev);
-	napi_enable(&intf->tx_napi);
-	napi_enable(&intf->rx_napi);
-
-	bcmasp_enable_rx_irq(intf, 1);
-	bcmasp_enable_tx_irq(intf, 1);
-	bcmasp_enable_phy_irq(intf, 1);
-
-	phy_start(dev->phydev);
-}
+	struct phy_device *phydev = intf->ndev->phydev;
+	u32 mac0, mac1;
 
-static void umac_reset(struct bcmasp_intf *intf)
-{
 	umac_wl(intf, 0x0, UMC_CMD);
 	umac_wl(intf, UMC_CMD_SW_RESET, UMC_CMD);
 	usleep_range(10, 100);
 	/* We hold the umac in reset and bring it out of
 	 * reset when phy link is up.
 	 */
-}
 
-static void umac_set_hw_addr(struct bcmasp_intf *intf,
-			     const unsigned char *addr)
-{
-	u32 mac0 = (addr[0] << 24) | (addr[1] << 16) | (addr[2] << 8) |
-		    addr[3];
-	u32 mac1 = (addr[4] << 8) | addr[5];
+	umac_wl(intf, 0x800, UMC_FRM_LEN);
+	umac_wl(intf, 0xffff, UMC_PAUSE_CNTRL);
+	umac_wl(intf, 0x800, UMC_RX_MAX_PKT_SZ);
+
+	mac0 = (addr[0] << 24) | (addr[1] << 16) | (addr[2] << 8) |
+		addr[3];
+	mac1 = (addr[4] << 8) | addr[5];
 
 	umac_wl(intf, mac0, UMC_MAC0);
 	umac_wl(intf, mac1, UMC_MAC1);
+
+	/* Reset shadow values since we reset the umac */
+	intf->old_duplex = -1;
+	intf->old_link = -1;
+	intf->old_pause = -1;
+	phydev->eee_cfg.tx_lpi_timer = umac_rl(intf, UMC_EEE_LPI_TIMER);
 }
 
 static void umac_enable_set(struct bcmasp_intf *intf, u32 mask,
@@ -401,13 +396,6 @@ static void umac_enable_set(struct bcmasp_intf *intf, u32 mask,
 		usleep_range(1000, 2000);
 }
 
-static void umac_init(struct bcmasp_intf *intf)
-{
-	umac_wl(intf, 0x800, UMC_FRM_LEN);
-	umac_wl(intf, 0xffff, UMC_PAUSE_CNTRL);
-	umac_wl(intf, 0x800, UMC_RX_MAX_PKT_SZ);
-}
-
 static int bcmasp_tx_reclaim(struct bcmasp_intf *intf)
 {
 	struct bcmasp_intf_stats64 *stats = &intf->stats64;
@@ -927,6 +915,14 @@ static void bcmasp_rgmii_mode_en_set(struct bcmasp_intf *intf, bool enable)
 	rgmii_wl(intf, reg, RGMII_OOB_CNTRL);
 }
 
+static void bcmasp_phy_hw_unprepare(struct bcmasp_intf *intf)
+{
+	if (intf->internal_phy)
+		bcmasp_ephy_enable_set(intf, false);
+	else
+		bcmasp_rgmii_mode_en_set(intf, false);
+}
+
 static void bcmasp_netif_deinit(struct net_device *dev)
 {
 	struct bcmasp_intf *intf = netdev_priv(dev);
@@ -984,11 +980,7 @@ static int bcmasp_stop(struct net_device *dev)
 
 	phy_disconnect(dev->phydev);
 
-	/* Disable internal EPHY or external PHY */
-	if (intf->internal_phy)
-		bcmasp_ephy_enable_set(intf, false);
-	else
-		bcmasp_rgmii_mode_en_set(intf, false);
+	bcmasp_phy_hw_unprepare(intf);
 
 	/* Disable the interface clocks */
 	bcmasp_core_clock_set_intf(intf, false);
@@ -998,10 +990,15 @@ static int bcmasp_stop(struct net_device *dev)
 	return 0;
 }
 
-static void bcmasp_configure_port(struct bcmasp_intf *intf)
+static void bcmasp_phy_hw_prepare(struct bcmasp_intf *intf)
 {
 	u32 reg, id_mode_dis = 0;
 
+	if (intf->internal_phy)
+		bcmasp_ephy_enable_set(intf, true);
+	else
+		bcmasp_rgmii_mode_en_set(intf, true);
+
 	reg = rgmii_rl(intf, RGMII_PORT_CNTRL);
 	reg &= ~RGMII_PORT_MODE_MASK;
 
@@ -1036,26 +1033,8 @@ static void bcmasp_configure_port(struct bcmasp_intf *intf)
 	rgmii_wl(intf, reg, RGMII_OOB_CNTRL);
 }
 
-static int bcmasp_netif_init(struct net_device *dev, bool phy_connect)
+static phy_interface_t bcmasp_phy_iface_for_connect(phy_interface_t mode)
 {
-	struct bcmasp_intf *intf = netdev_priv(dev);
-	phy_interface_t phy_iface = intf->phy_interface;
-	u32 phy_flags = PHY_BRCM_AUTO_PWRDWN_ENABLE |
-			PHY_BRCM_DIS_TXCRXC_NOENRGY |
-			PHY_BRCM_IDDQ_SUSPEND;
-	struct phy_device *phydev = NULL;
-	int ret;
-
-	/* Always enable interface clocks */
-	bcmasp_core_clock_set_intf(intf, true);
-
-	/* Enable internal PHY or external PHY before any MAC activity */
-	if (intf->internal_phy)
-		bcmasp_ephy_enable_set(intf, true);
-	else
-		bcmasp_rgmii_mode_en_set(intf, true);
-	bcmasp_configure_port(intf);
-
 	/* This is an ugly quirk but we have not been correctly
 	 * interpreting the phy_interface values and we have done that
 	 * across different drivers, so at least we are consistent in
@@ -1081,46 +1060,43 @@ static int bcmasp_netif_init(struct net_device *dev, bool phy_connect)
 	 * affected because they use different phy_interface_t values
 	 * or the Generic PHY driver.
 	 */
-	switch (phy_iface) {
+	switch (mode) {
 	case PHY_INTERFACE_MODE_RGMII:
-		phy_iface = PHY_INTERFACE_MODE_RGMII_ID;
-		break;
+		return PHY_INTERFACE_MODE_RGMII_ID;
 	case PHY_INTERFACE_MODE_RGMII_TXID:
-		phy_iface = PHY_INTERFACE_MODE_RGMII_RXID;
-		break;
+		return PHY_INTERFACE_MODE_RGMII_RXID;
 	default:
-		break;
+		return mode;
 	}
+}
 
-	if (phy_connect) {
-		phydev = of_phy_connect(dev, intf->phy_dn,
-					bcmasp_adj_link, phy_flags,
-					phy_iface);
-		if (!phydev) {
-			ret = -ENODEV;
-			netdev_err(dev, "could not attach to PHY\n");
-			goto err_phy_disable;
-		}
-
-		if (intf->internal_phy)
-			dev->phydev->irq = PHY_MAC_INTERRUPT;
-
-		/* Indicate that the MAC is responsible for PHY PM */
-		phydev->mac_managed_pm = true;
-
-		/* Set phylib's copy of the LPI timer */
-		phydev->eee_cfg.tx_lpi_timer = umac_rl(intf, UMC_EEE_LPI_TIMER);
+static int bcmasp_phy_attach(struct bcmasp_intf *intf)
+{
+	u32 phy_flags = PHY_BRCM_AUTO_PWRDWN_ENABLE |
+			PHY_BRCM_DIS_TXCRXC_NOENRGY |
+			PHY_BRCM_IDDQ_SUSPEND;
+	struct phy_device *phydev;
+	phy_interface_t phy_iface;
+
+	phy_iface = bcmasp_phy_iface_for_connect(intf->phy_interface);
+	phydev = of_phy_connect(intf->ndev, intf->phy_dn,
+				bcmasp_adj_link, phy_flags,
+				phy_iface);
+	if (!phydev) {
+		netdev_err(intf->ndev, "could not attach to PHY\n");
+		return -ENODEV;
 	}
+	if (intf->internal_phy)
+		intf->ndev->phydev->irq = PHY_MAC_INTERRUPT;
 
-	umac_reset(intf);
-
-	umac_init(intf);
+	phydev->mac_managed_pm = true;
 
-	umac_set_hw_addr(intf, dev->dev_addr);
+	return 0;
+}
 
-	intf->old_duplex = -1;
-	intf->old_link = -1;
-	intf->old_pause = -1;
+static void bcmasp_netif_init(struct net_device *dev)
+{
+	struct bcmasp_intf *intf = netdev_priv(dev);
 
 	bcmasp_init_tx(intf);
 	netif_napi_add_tx(intf->ndev, &intf->tx_napi, bcmasp_tx_poll);
@@ -1132,18 +1108,13 @@ static int bcmasp_netif_init(struct net_device *dev, bool phy_connect)
 
 	intf->crc_fwd = !!(umac_rl(intf, UMC_CMD) & UMC_CMD_CRC_FWD);
 
-	bcmasp_netif_start(dev);
-
-	netif_start_queue(dev);
-
-	return 0;
+	bcmasp_set_rx_mode(dev);
+	napi_enable(&intf->tx_napi);
+	napi_enable(&intf->rx_napi);
 
-err_phy_disable:
-	if (intf->internal_phy)
-		bcmasp_ephy_enable_set(intf, false);
-	else
-		bcmasp_rgmii_mode_en_set(intf, false);
-	return ret;
+	bcmasp_enable_rx_irq(intf, 1);
+	bcmasp_enable_tx_irq(intf, 1);
+	bcmasp_enable_phy_irq(intf, 1);
 }
 
 static int bcmasp_open(struct net_device *dev)
@@ -1161,14 +1132,30 @@ static int bcmasp_open(struct net_device *dev)
 	if (ret)
 		goto err_free_mem;
 
-	ret = bcmasp_netif_init(dev, true);
-	if (ret) {
-		clk_disable_unprepare(intf->parent->clk);
-		goto err_free_mem;
-	}
+	bcmasp_core_clock_set_intf(intf, true);
+
+	bcmasp_phy_hw_prepare(intf);
+
+	ret = bcmasp_phy_attach(intf);
+	if (ret)
+		goto err_phy_attach;
+
+	umac_reset_and_init(intf, dev->dev_addr);
+
+	dev->phydev->eee_cfg.tx_lpi_timer = umac_rl(intf, UMC_EEE_LPI_TIMER);
+
+	bcmasp_netif_init(dev);
+
+	phy_start(dev->phydev);
+
+	netif_start_queue(dev);
 
 	return ret;
 
+err_phy_attach:
+	bcmasp_phy_hw_unprepare(intf);
+	bcmasp_core_clock_set_intf(intf, false);
+	clk_disable_unprepare(intf->parent->clk);
 err_free_mem:
 	bcmasp_reclaim_free_buffers(intf);
 
@@ -1407,10 +1394,7 @@ int bcmasp_interface_suspend(struct bcmasp_intf *intf)
 	bcmasp_netif_deinit(dev);
 
 	if (!intf->wolopts) {
-		if (intf->internal_phy)
-			bcmasp_ephy_enable_set(intf, false);
-		else
-			bcmasp_rgmii_mode_en_set(intf, false);
+		bcmasp_phy_hw_unprepare(intf);
 
 		/* If Wake-on-LAN is disabled, we can safely
 		 * disable the network interface clocks.
@@ -1454,17 +1438,19 @@ int bcmasp_interface_resume(struct bcmasp_intf *intf)
 	if (ret)
 		return ret;
 
-	ret = bcmasp_netif_init(dev, false);
-	if (ret)
-		goto out;
+	bcmasp_core_clock_set_intf(intf, true);
 
 	bcmasp_resume_from_wol(intf);
 
+	bcmasp_phy_hw_prepare(intf);
+
+	umac_reset_and_init(intf, dev->dev_addr);
+
+	bcmasp_netif_init(dev);
+
+	phy_start(dev->phydev);
+
 	netif_device_attach(dev);
 
 	return 0;
-
-out:
-	clk_disable_unprepare(intf->parent->clk);
-	return ret;
 }
-- 
2.34.1


^ permalink raw reply related

* [PATCH net-next v2 0/2] Keep PHY link during WoL sleep cycle
From: Justin Chen @ 2026-05-01 18:56 UTC (permalink / raw)
  To: netdev
  Cc: bcm-kernel-feedback-list, pabeni, kuba, edumazet, davem,
	andrew+netdev, florian.fainelli, Justin Chen

First we divide the init/deinit path to allow for a partial init/deinit
during a sleep cycle. We also remove some unnecessary small functions at
the same time.

Then we modify the suspend and resume path to allow for a partial bring
down and bring up. This allow us to keep the PHY link up and to resume
network traffic much quicker. Note we only do this when WoL is enabled
since the PHY is already powered. In the non-WoL case we want to follow
the same flow.

Justin Chen (2):
  net: bcmasp: Divide init to allow partial bring up
  net: bcmasp: Keep PHY link during WoL sleep cycle

 .../net/ethernet/broadcom/asp2/bcmasp_intf.c  | 251 +++++++++---------
 1 file changed, 130 insertions(+), 121 deletions(-)

-- 
2.34.1


^ permalink raw reply

* Re: [PATCH net-next 09/12] gpio: tc956x: add TC956x/QPS615 support
From: Andrew Lunn @ 2026-05-01 18:36 UTC (permalink / raw)
  To: Alex Elder
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, maxime.chevallier,
	rmk+kernel, andersson, konradybcio, robh, krzk+dt, conor+dt,
	linusw, brgl, arnd, gregkh, daniel, mohd.anwar, a0987203069,
	alexandre.torgue, ast, boon.khai.ng, chenchuangyu, chenhuacai,
	daniel, hawk, hkallweit1, inochiama, john.fastabend, julianbraha,
	livelycarpet87, matthew.gerlach, mcoquelin.stm32, me,
	prabhakar.mahadev-lad.rj, richardcochran, rohan.g.thomas, sdf,
	siyanteng, weishangjuan, wens, netdev, bpf, linux-arm-msm,
	devicetree, linux-gpio, linux-stm32, linux-arm-kernel,
	linux-kernel
In-Reply-To: <20260501155421.3329862-10-elder@riscstar.com>

> + * There is a TC956X PCI power controller driver that accesses the
> + * direction and output value registers for GPIOs 2 and 3.  These
> + * GPIOs control the reset signal for the two downstream PCIe ports.
> + * Their values will never change during operation of this driver, and
> + * this driver reserves these two GPIOS.

Why doesn't this power controller driver actually use this driver to
control the GPIOs? Chicken/egg?

Maybe add a comment why gpio-regmap.c cannot be used. You probably
need to instantiate it twice, but i still think you will end up with
less code.

	Andrew

^ permalink raw reply

* Re: [PATCH net-next 02/12] net: pcs: pcs-xpcs: select operating mode for 10G-baseR capable PCS
From: Alex Elder @ 2026-05-01 18:07 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, maxime.chevallier,
	rmk+kernel, andersson, konradybcio, robh, krzk+dt, conor+dt,
	linusw, brgl, arnd, gregkh, Daniel Thompson, mohd.anwar,
	a0987203069, alexandre.torgue, ast, boon.khai.ng, chenchuangyu,
	chenhuacai, daniel, hawk, hkallweit1, inochiama, john.fastabend,
	julianbraha, livelycarpet87, matthew.gerlach, mcoquelin.stm32, me,
	prabhakar.mahadev-lad.rj, richardcochran, rohan.g.thomas, sdf,
	siyanteng, weishangjuan, wens, netdev, bpf, linux-arm-msm,
	devicetree, linux-gpio, linux-stm32, linux-arm-kernel,
	linux-kernel
In-Reply-To: <f9a581a2-02ea-4948-8c97-835cb7638b1d@lunn.ch>

On 5/1/26 11:50 AM, Andrew Lunn wrote:
>> +static int xpcs_config_operating_mode(struct dw_xpcs *xpcs, int an_mode)
>> +{
>> +	int mdio_stat2, ret;
>> +
>> +	switch (an_mode) {
>> +	case DW_AN_C37_SGMII:
>> +	case DW_AN_C37_1000BASEX:
>> +	case DW_2500BASEX:
>> +		mdio_stat2 = xpcs_read(xpcs, MDIO_MMD_PCS, MDIO_STAT2);
>> +		if (mdio_stat2 < 0)
>> +			return mdio_stat2;
>> +
>> +		/*
>> +		 * If this XPCS supports 10Gbase-R then it will be the default
>> +		 * which prevents 1000base-X and slower from working correctly.
> 
> It would be interesting to know if Toshiba messed up the integration
> of the PCS, or there is an errata for the licensed IP.
> 
> Anybody got access to the databook and erratas?
> 
> 	Andrew

I have the same question.  We do not (currently/yet?) have access
to this information.

					-Alex

^ permalink raw reply

* Re: [PATCH net-next 04/12] net: stmmac: dma: create a separate dma_device pointer
From: Alex Elder @ 2026-05-01 18:06 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, maxime.chevallier,
	rmk+kernel, andersson, konradybcio, robh, krzk+dt, conor+dt,
	linusw, brgl, arnd, gregkh, daniel, mohd.anwar, a0987203069,
	alexandre.torgue, ast, boon.khai.ng, chenchuangyu, chenhuacai,
	daniel, hawk, hkallweit1, inochiama, john.fastabend, julianbraha,
	livelycarpet87, matthew.gerlach, mcoquelin.stm32, me,
	prabhakar.mahadev-lad.rj, richardcochran, rohan.g.thomas, sdf,
	siyanteng, weishangjuan, wens, netdev, bpf, linux-arm-msm,
	devicetree, linux-gpio, linux-stm32, linux-arm-kernel,
	linux-kernel
In-Reply-To: <9203d8dd-8ec0-415e-9c2e-5b06b1b8dc11@lunn.ch>

On 5/1/26 12:13 PM, Andrew Lunn wrote:
>>   	if (priv->dma_cap.host_dma_width) {
>> -		ret = dma_set_mask_and_coherent(device,
>> -				DMA_BIT_MASK(priv->dma_cap.host_dma_width));
>> +		u64 mask = DMA_BIT_MASK(priv->dma_cap.host_dma_width);
>> +
>> +		ret = dma_set_mask_and_coherent(priv->dma_device, mask);
> 
> I'm nitpicking, but i don't think you need to introduce mask.
> DMA_BIT_MASK... is already on a line of its own, and is within the 80
> limit. Nothing changes here with s/device/priv->dma_device/
> 
> 	Andrew

I did this.  It was simply to silence a checkpatch.pl warning
about a long line.

I don't care either way, I'll gladly put it back the way it was.

					-Alex

^ permalink raw reply

* [PATCH v12 net-next 5/5] selftest/net: psp: Add test for dev-assoc/disassoc
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang
In-Reply-To: <20260501175528.2130551-1-weibunny.kernel@gmail.com>

From: Wei Wang <weibunny@fb.com>

Add a new param to NetDrvContEnv to add an additional bpf redirect
program on nk_host to redirect traffic to the psp_dev_local.
The topology looks like this:
  Host NS:  psp_dev_local <---> nk_host
                |                |
                |                | (netkit pair)
                |                |
  Remote NS: psp_dev_peer      Guest NS: nk_guest
             (responder)             (PSP tests)

Add following tests for dev-assoc/dev-disassoc functionality:
1. Test the output of `./tools/net/ynl/pyynl/cli.py --spec
Documentation/netlink/specs/psp.yaml --dump dev-get` in both default and
the guest netns.
2. Test the case where we associate netkit with psp_dev_local, and
send PSP traffic from nk_guest to psp_dev_peer in 2 different netns.
3. Test to make sure the key rotation notification is sent to the netns
for associated dev as well
4. Test to make sure the dev change notification is sent to the netns
for associated dev as well
5. Test for dev-assoc/dev-disassoc without nsid parameter.
6. Test the deletion of nk_guest in client netns, and proper cleanup in
the assoc-list for psp dev.

Signed-off-by: Wei Wang <weibunny@fb.com>
---
 tools/testing/selftests/drivers/net/config    |   1 +
 .../selftests/drivers/net/lib/py/env.py       |  54 ++-
 tools/testing/selftests/drivers/net/psp.py    | 457 ++++++++++++++++--
 3 files changed, 478 insertions(+), 34 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/config b/tools/testing/selftests/drivers/net/config
index fd16994366f4..b8a559b360e4 100644
--- a/tools/testing/selftests/drivers/net/config
+++ b/tools/testing/selftests/drivers/net/config
@@ -8,5 +8,6 @@ CONFIG_NETCONSOLE=m
 CONFIG_NETCONSOLE_DYNAMIC=y
 CONFIG_NETCONSOLE_EXTENDED_LOG=y
 CONFIG_NETDEVSIM=m
+CONFIG_NETKIT=y
 CONFIG_VLAN_8021Q=m
 CONFIG_XDP_SOCKETS=y
diff --git a/tools/testing/selftests/drivers/net/lib/py/env.py b/tools/testing/selftests/drivers/net/lib/py/env.py
index 24ce122abd9c..cd3a08cbe968 100644
--- a/tools/testing/selftests/drivers/net/lib/py/env.py
+++ b/tools/testing/selftests/drivers/net/lib/py/env.py
@@ -2,6 +2,7 @@
 
 import ipaddress
 import os
+import re
 import time
 import json
 from pathlib import Path
@@ -336,7 +337,7 @@ class NetDrvContEnv(NetDrvEpEnv):
               +---------------+
     """
 
-    def __init__(self, src_path, rxqueues=1, **kwargs):
+    def __init__(self, src_path, rxqueues=1, install_tx_redirect_bpf=False, **kwargs):
         self.netns = None
         self._nk_host_ifname = None
         self._nk_guest_ifname = None
@@ -347,6 +348,8 @@ class NetDrvContEnv(NetDrvEpEnv):
         self._init_ns_attached = False
         self._old_fwd = None
         self._old_accept_ra = None
+        self._nk_host_tc_attached = False
+        self._nk_host_bpf_prog_pref = None
 
         super().__init__(src_path, **kwargs)
 
@@ -397,7 +400,13 @@ class NetDrvContEnv(NetDrvEpEnv):
         self._setup_ns()
         self._attach_bpf()
 
+        if install_tx_redirect_bpf:
+            self._attach_tx_redirect_bpf()
+
     def __del__(self):
+        if self._nk_host_tc_attached:
+            cmd(f"tc filter del dev {self._nk_host_ifname} ingress pref {self._nk_host_bpf_prog_pref}", fail=False)
+            self._nk_host_tc_attached = False
         if self._tc_attached:
             cmd(f"tc filter del dev {self.ifname} ingress pref {self._bpf_prog_pref}")
             self._tc_attached = False
@@ -505,3 +514,46 @@ class NetDrvContEnv(NetDrvEpEnv):
         value = ipv6_bytes + ifindex_bytes
         value_hex = ' '.join(f'{b:02x}' for b in value)
         bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
+
+    def _attach_tx_redirect_bpf(self):
+        """
+        Attach BPF program on nk_host ingress to redirect TX traffic.
+
+        Packets from nk_guest destined for the nsim network arrive at nk_host
+        via the netkit pair. This BPF program redirects them to the physical
+        interface so they can reach the remote peer.
+        """
+        bpf_obj = self.test_dir / "nk_redirect.bpf.o"
+        if not bpf_obj.exists():
+            raise KsftSkipEx("BPF prog nk_redirect.bpf.o not found")
+
+        cmd(f"tc qdisc add dev {self._nk_host_ifname} clsact")
+
+        cmd(f"tc filter add dev {self._nk_host_ifname} ingress bpf obj {bpf_obj} sec tc/ingress direct-action")
+        self._nk_host_tc_attached = True
+
+        tc_info = cmd(f"tc filter show dev {self._nk_host_ifname} ingress").stdout
+        match = re.search(r'pref (\d+).*nk_redirect\.bpf.*id (\d+)', tc_info)
+        if not match:
+            raise Exception("Failed to get TX redirect BPF prog ID")
+        self._nk_host_bpf_prog_pref = int(match.group(1))
+        nk_host_bpf_prog_id = int(match.group(2))
+
+        prog_info = bpftool(f"prog show id {nk_host_bpf_prog_id}", json=True)
+        map_ids = prog_info.get("map_ids", [])
+
+        bss_map_id = None
+        for map_id in map_ids:
+            map_info = bpftool(f"map show id {map_id}", json=True)
+            if map_info.get("name").endswith("bss"):
+                bss_map_id = map_id
+
+        if bss_map_id is None:
+            raise Exception("Failed to find TX redirect BPF .bss map")
+
+        ipv6_addr = ipaddress.IPv6Address(self.nsim_v6_pfx)
+        ipv6_bytes = ipv6_addr.packed
+        ifindex_bytes = self.ifindex.to_bytes(4, byteorder='little')
+        value = ipv6_bytes + ifindex_bytes
+        value_hex = ' '.join(f'{b:02x}' for b in value)
+        bpftool(f"map update id {bss_map_id} key hex 00 00 00 00 value hex {value_hex}")
diff --git a/tools/testing/selftests/drivers/net/psp.py b/tools/testing/selftests/drivers/net/psp.py
index 864d9fce1094..79da4d425c50 100755
--- a/tools/testing/selftests/drivers/net/psp.py
+++ b/tools/testing/selftests/drivers/net/psp.py
@@ -5,6 +5,7 @@
 
 import errno
 import fcntl
+import os
 import socket
 import struct
 import termios
@@ -14,9 +15,12 @@ from lib.py import defer
 from lib.py import ksft_run, ksft_exit, ksft_pr
 from lib.py import ksft_true, ksft_eq, ksft_ne, ksft_gt, ksft_raises
 from lib.py import ksft_not_none
-from lib.py import KsftSkipEx
-from lib.py import NetDrvEpEnv, PSPFamily, NlError
+from lib.py import ksft_variants, KsftNamedVariant
+from lib.py import KsftSkipEx, KsftFailEx
+from lib.py import NetDrvEpEnv, NetDrvContEnv, PSPFamily, NlError
+from lib.py import NetNSEnter
 from lib.py import bkg, rand_port, wait_port_listen
+from lib.py import ip
 
 
 def _get_outq(s):
@@ -117,11 +121,13 @@ def _get_stat(cfg, key):
 # Test case boiler plate
 #
 
-def _init_psp_dev(cfg):
+def _init_psp_dev(cfg, use_psp_ifindex=False):
     if not hasattr(cfg, 'psp_dev_id'):
         # Figure out which local device we are testing against
+        # For NetDrvContEnv: use psp_ifindex instead of ifindex
+        target_ifindex = cfg.psp_ifindex if use_psp_ifindex else cfg.ifindex
         for dev in cfg.pspnl.dev_get({}, dump=True):
-            if dev['ifindex'] == cfg.ifindex:
+            if dev['ifindex'] == target_ifindex:
                 cfg.psp_info = dev
                 cfg.psp_dev_id = cfg.psp_info['id']
                 break
@@ -394,6 +400,297 @@ def _data_basic_send(cfg, version, ipver):
     _close_psp_conn(cfg, s)
 
 
+def _data_basic_send_netkit_psp_assoc(cfg, version, ipver):
+    """
+    Test basic data send with netkit interface associated with PSP dev.
+    """
+
+    _init_psp_dev(cfg, True)
+    psp_dev_id_for_assoc = cfg.psp_dev_id
+
+    # Associate PSP device with nk_guest interface (in guest namespace)
+    nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0]
+    nk_guest_ifindex = nk_guest_dev['ifindex']
+
+    cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    # Check if assoc-list contains nk_guest
+    dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc})
+
+    if 'assoc-list' in dev_info:
+        found = False
+        for assoc in dev_info['assoc-list']:
+            if assoc['ifindex'] == nk_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid:
+                found = True
+                break
+        ksft_true(found, "Associated device not found in dev_get() response")
+    else:
+        raise RuntimeError("No assoc-list in dev_get() response after association")
+
+    # Enter guest namespace (netns) to run PSP test
+    with NetNSEnter(cfg.netns.name):
+        cfg.pspnl = PSPFamily()
+
+        s = _make_psp_conn(cfg, version, ipver)
+
+        rx_assoc = cfg.pspnl.rx_assoc({"version": version,
+                                       "dev-id": cfg.psp_dev_id,
+                                       "sock-fd": s.fileno()})
+        rx = rx_assoc['rx-key']
+        tx = _spi_xchg(s, rx)
+
+        cfg.pspnl.tx_assoc({"dev-id": cfg.psp_dev_id,
+                            "version": version,
+                            "tx-key": tx,
+                            "sock-fd": s.fileno()})
+
+        data_len = _send_careful(cfg, s, 100)
+        _check_data_rx(cfg, data_len)
+        _close_psp_conn(cfg, s)
+
+    # Clean up - back in host namespace
+    cfg.pspnl = PSPFamily()
+    cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    del cfg.psp_dev_id
+    del cfg.psp_info
+
+
+def _key_rotation_notify_multi_ns_netkit(cfg):
+    """ Test key rotation notifications across multiple namespaces using netkit """
+    _init_psp_dev(cfg, True)
+    psp_dev_id_for_assoc = cfg.psp_dev_id
+
+    # Associate PSP device with nk_guest interface (in guest namespace)
+    nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0]
+    nk_guest_ifindex = nk_guest_dev['ifindex']
+
+    cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    # Create listener in guest namespace; socket stays bound to that ns
+    with NetNSEnter(cfg.netns.name):
+        peer_pspnl = PSPFamily()
+        peer_pspnl.ntf_subscribe('use')
+
+    # Create listener in main namespace
+    main_pspnl = PSPFamily()
+    main_pspnl.ntf_subscribe('use')
+
+    # Trigger key rotation on the PSP device
+    cfg.pspnl.key_rotate({"id": psp_dev_id_for_assoc})
+
+    # Poll both sockets from main thread
+    for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]:
+        for i in range(100):
+            pspnl.check_ntf()
+
+            try:
+                msg = pspnl.async_msg_queue.get_nowait()
+                break
+            except Exception:
+                pass
+
+            time.sleep(0.1)
+        else:
+            raise KsftFailEx(f"No key rotation notification received in {label} namespace")
+
+        ksft_true(msg['msg'].get('id') == psp_dev_id_for_assoc,
+                  f"Key rotation notification for correct device not found in {label} namespace")
+
+    # Clean up
+    cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+    del cfg.psp_dev_id
+    del cfg.psp_info
+
+
+def _dev_change_notify_multi_ns_netkit(cfg):
+    """ Test dev_change notifications across multiple namespaces using netkit """
+    _init_psp_dev(cfg, True)
+    psp_dev_id_for_assoc = cfg.psp_dev_id
+
+    # Associate PSP device with nk_guest interface (in guest namespace)
+    nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0]
+    nk_guest_ifindex = nk_guest_dev['ifindex']
+
+    cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    # Create listener in guest namespace; socket stays bound to that ns
+    with NetNSEnter(cfg.netns.name):
+        peer_pspnl = PSPFamily()
+        peer_pspnl.ntf_subscribe('mgmt')
+
+    # Create listener in main namespace
+    main_pspnl = PSPFamily()
+    main_pspnl.ntf_subscribe('mgmt')
+
+    # Trigger dev_change by calling dev_set (notification is always sent)
+    cfg.pspnl.dev_set({'id': psp_dev_id_for_assoc, 'psp-versions-ena': cfg.psp_info['psp-versions-cap']})
+
+    # Poll both sockets from main thread
+    for pspnl, label in [(main_pspnl, "main"), (peer_pspnl, "guest")]:
+        for i in range(100):
+            pspnl.check_ntf()
+
+            try:
+                msg = pspnl.async_msg_queue.get_nowait()
+                break
+            except Exception:
+                pass
+
+            time.sleep(0.1)
+        else:
+            raise KsftFailEx(f"No dev_change notification received in {label} namespace")
+
+        ksft_true(msg['msg'].get('id') == psp_dev_id_for_assoc,
+                  f"Dev_change notification for correct device not found in {label} namespace")
+
+    # Clean up
+    cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+    del cfg.psp_dev_id
+    del cfg.psp_info
+
+
+def _psp_dev_get_check_netkit_psp_assoc(cfg):
+    """ Check psp dev-get output with netkit interface associated with PSP dev """
+
+    _init_psp_dev(cfg, True)
+    psp_dev_id_for_assoc = cfg.psp_dev_id
+
+    # Associate PSP device with nk_guest interface (in guest namespace)
+    nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0]
+    nk_guest_ifindex = nk_guest_dev['ifindex']
+
+    cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    # Check 1: In default netns, verify dev-get has correct ifindex and assoc-list
+    dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc})
+
+    # Verify the PSP device has the correct ifindex
+    ksft_eq(dev_info['ifindex'], cfg.psp_ifindex)
+
+    # Verify assoc-list exists and contains the associated nk_guest with correct ifindex and nsid
+    ksft_true('assoc-list' in dev_info, "No assoc-list in dev_get() response after association")
+    found = False
+    for assoc in dev_info['assoc-list']:
+        if assoc['ifindex'] == nk_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid:
+            found = True
+            break
+    ksft_true(found, "Associated device not found in assoc-list with correct ifindex and nsid")
+
+    # Check 2: In guest netns, verify dev-get has assoc-list with nk_guest device
+    with NetNSEnter(cfg.netns.name):
+        peer_pspnl = PSPFamily()
+
+        # Dump all devices in the guest namespace
+        peer_devices = peer_pspnl.dev_get({}, dump=True)
+
+        # Find the device with by-association flag
+        peer_dev = None
+        for dev in peer_devices:
+            if dev.get('by-association'):
+                peer_dev = dev
+                break
+
+        ksft_not_none(peer_dev, "No PSP device found with by-association flag in guest netns")
+
+        # Verify assoc-list contains the nk_guest device
+        ksft_true('assoc-list' in peer_dev and len(peer_dev['assoc-list']) > 0,
+                  "Guest device should have assoc-list with local devices")
+
+        # Verify the assoc-list contains nk_guest ifindex with nsid=-1 (same namespace)
+        found = False
+        for assoc in peer_dev['assoc-list']:
+            if assoc['ifindex'] == nk_guest_ifindex:
+                ksft_eq(assoc['nsid'], -1,
+                        "nsid should be -1 (NETNSA_NSID_NOT_ASSIGNED) for same-namespace device")
+                found = True
+                break
+        ksft_true(found, "nk_guest ifindex not found in assoc-list")
+
+    # Clean up
+    cfg.pspnl.dev_disassoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    del cfg.psp_dev_id
+    del cfg.psp_info
+
+
+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
+
+
+def _psp_dev_assoc_cleanup_on_netkit_del(cfg):
+    """ Test that assoc-list is cleared when associated netkit interface is deleted """
+    _init_psp_dev(cfg, True)
+    psp_dev_id_for_assoc = cfg.psp_dev_id
+
+    # Associate PSP device with nk_guest interface (in guest namespace)
+    nk_guest_dev = ip(f"link show dev {cfg._nk_guest_ifname}", json=True, ns=cfg.netns)[0]
+    nk_guest_ifindex = nk_guest_dev['ifindex']
+
+    cfg.pspnl.dev_assoc({'id': psp_dev_id_for_assoc, 'ifindex': nk_guest_ifindex, 'nsid': cfg.psp_dev_peer_nsid})
+
+    # Verify assoc-list exists in default netns
+    dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc})
+    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_guest_ifindex and assoc['nsid'] == cfg.psp_dev_peer_nsid:
+            found = True
+            break
+    ksft_true(found, "Associated device not found in assoc-list")
+
+    # Delete the netkit interface in the guest namespace
+    ip(f"link del {cfg._nk_guest_ifname}", ns=cfg.netns)
+
+    # Mark netkit as already deleted so cleanup won't try to delete it again
+    # (deleting nk_guest also removes nk_host since they're a pair)
+    cfg._nk_host_ifname = None
+    cfg._nk_guest_ifname = None
+
+    # Verify assoc-list is gone in default netns after netkit deletion
+    dev_info = cfg.pspnl.dev_get({'id': psp_dev_id_for_assoc})
+    ksft_true('assoc-list' not in dev_info or len(dev_info['assoc-list']) == 0,
+              "assoc-list should be empty after netkit deletion")
+
+    del cfg.psp_dev_id
+    del cfg.psp_info
+
+
 def __bad_xfer_do(cfg, s, tx, version='hdr0-aes-gcm-128'):
     # Make sure we accept the ACK for the SPI before we seal with the bad assoc
     _check_data_outq(s, 0)
@@ -571,33 +868,127 @@ def removal_device_bi(cfg):
         _close_conn(cfg, s)
 
 
-def psp_ip_ver_test_builder(name, test_func, psp_ver, ipver):
-    """Build test cases for each combo of PSP version and IP version"""
-    def test_case(cfg):
-        cfg.require_ipver(ipver)
-        test_func(cfg, psp_ver, ipver)
-
-    test_case.__name__ = f"{name}_v{psp_ver}_ip{ipver}"
-    return test_case
+@ksft_variants([
+    KsftNamedVariant(f"v{v}_ip{ip}", v, ip)
+    for v in range(4) for ip in ("4", "6")
+])
+def data_basic_send(cfg, version, ipver):
+    cfg.require_ipver(ipver)
+    _data_basic_send(cfg, version, ipver)
+
+
+@ksft_variants([
+    KsftNamedVariant(f"ip{ip}", ip)
+    for ip in ("4", "6")
+])
+def data_mss_adjust(cfg, ipver):
+    cfg.require_ipver(ipver)
+    _data_mss_adjust(cfg, ipver)
+
+
+@ksft_variants([
+    KsftNamedVariant(f"v{v}_ip6", v, "6")
+    for v in range(4)
+])
+def data_basic_send_netkit_psp_assoc(cfg, version, ipver):
+    cfg.require_ipver(ipver)
+    _data_basic_send_netkit_psp_assoc(cfg, version, ipver)
+
+
+
+def _get_nsid(ns_name):
+    """Get the nsid for a namespace."""
+    for entry in ip("netns list-id", json=True):
+        if entry.get("name") == str(ns_name):
+            return entry["nsid"]
+    raise KsftSkipEx(f"nsid not found for namespace {ns_name}")
+
+
+def _setup_psp_attributes(cfg):
+    """
+    Set up PSP-specific attributes on the environment.
+
+    This sets attributes needed for PSP tests based on whether we're using
+    netdevsim or a real NIC.
+    """
+    if cfg._ns is not None:
+        # netdevsim case: PSP device is the local dev (in host namespace)
+        cfg.psp_dev = cfg._ns.nsims[0].dev
+        cfg.psp_ifname = cfg.psp_dev['ifname']
+        cfg.psp_ifindex = cfg.psp_dev['ifindex']
+
+        # PSP peer device is the remote dev (in _netns, where psp_responder runs)
+        cfg.psp_dev_peer = cfg._ns_peer.nsims[0].dev
+        cfg.psp_dev_peer_ifname = cfg.psp_dev_peer['ifname']
+        cfg.psp_dev_peer_ifindex = cfg.psp_dev_peer['ifindex']
+    else:
+        # Real NIC case: PSP device is the local interface
+        cfg.psp_dev = cfg.dev
+        cfg.psp_ifname = cfg.ifname
+        cfg.psp_ifindex = cfg.ifindex
+
+        # PSP peer device is the remote interface
+        cfg.psp_dev_peer = cfg.remote_dev
+        cfg.psp_dev_peer_ifname = cfg.remote_ifname
+        cfg.psp_dev_peer_ifindex = cfg.remote_ifindex
+
+    # Get nsid for the guest namespace (netns) where nk_guest is
+    cfg.psp_dev_peer_nsid = _get_nsid(cfg.netns.name)
+
+
+def _setup_psp_routes(cfg):
+    """
+    Set up routes for cross-namespace connectivity.
+
+    Traffic flows:
+    1. remote (_netns) -> nk_guest (netns):
+       psp_dev_peer -> psp_dev_local -> BPF redirect -> nk_host -> nk_guest
+       Needs: route in _netns to nk_v6_pfx/64 via psp_dev_local
+
+    2. nk_guest (netns) -> remote (_netns):
+       nk_guest -> nk_host -> psp_dev_local -> psp_dev_peer
+       Needs: route in netns to dev_v6_pfx/64 via nk_host
+    """
+    # In _netns (remote namespace): add route to nk_guest prefix via psp_dev_local
+    # psp_dev_peer can reach psp_dev_local via the link, then traffic goes through BPF
+    ip(f"-6 route add {cfg.nk_v6_pfx}/64 via {cfg.nsim_v6_pfx}1 dev {cfg.psp_dev_peer_ifname}",
+       ns=cfg._netns)
+
+    # In netns (guest namespace): add route to remote peer prefix
+    # nk_guest default route goes to nk_host, but we need explicit route to dev_v6_pfx/64
+    ip(f"-6 route add {cfg.nsim_v6_pfx}/64 via fe80::1 dev {cfg._nk_guest_ifname}",
+       ns=cfg.netns)
 
 
-def ipver_test_builder(name, test_func, ipver):
-    """Build test cases for each IP version"""
-    def test_case(cfg):
-        cfg.require_ipver(ipver)
-        test_func(cfg, ipver)
+def main() -> None:
+    """ Ksft boiler plate main """
 
-    test_case.__name__ = f"{name}_ip{ipver}"
-    return test_case
+    # Use a different prefix for netkit guest to avoid conflict with dev prefix
+    nk_v6_pfx = "2001:db9::"
 
+    # Set LOCAL_PREFIX_V6 to a DIFFERENT prefix than the dev prefix to avoid BPF
+    # redirecting psp_responder traffic. The BPF only redirects traffic
+    # matching LOCAL_PREFIX_V6, so dev traffic (2001:db8::) won't be affected.
+    if "LOCAL_PREFIX_V6" not in os.environ:
+        os.environ["LOCAL_PREFIX_V6"] = nk_v6_pfx
 
-def main() -> None:
-    """ Ksft boiler plate main """
+    try:
+        env = NetDrvContEnv(__file__, install_tx_redirect_bpf=True)
+        has_cont = True
+    except KsftSkipEx:
+        env = NetDrvEpEnv(__file__)
+        has_cont = False
 
-    with NetDrvEpEnv(__file__) as cfg:
+    with env as cfg:
         cfg.pspnl = PSPFamily()
 
+        if has_cont:
+            cfg.nk_v6_pfx = nk_v6_pfx
+            _setup_psp_attributes(cfg)
+            _setup_psp_routes(cfg)
+
         # Set up responder and communication sock
+        # psp_responder runs in _netns (remote namespace with psp_dev_peer)
         responder = cfg.remote.deploy("psp_responder")
 
         cfg.comm_port = rand_port()
@@ -611,17 +1002,17 @@ def main() -> None:
                                                           cfg.comm_port),
                                                          timeout=1)
 
-                cases = [
-                    psp_ip_ver_test_builder(
-                        "data_basic_send", _data_basic_send, version, ipver
-                    )
-                    for version in range(0, 4)
-                    for ipver in ("4", "6")
-                ]
-                cases += [
-                    ipver_test_builder("data_mss_adjust", _data_mss_adjust, ipver)
-                    for ipver in ("4", "6")
-                ]
+                cases = [data_basic_send, data_mss_adjust]
+
+                if has_cont:
+                    cases += [
+                        data_basic_send_netkit_psp_assoc,
+                        _key_rotation_notify_multi_ns_netkit,
+                        _dev_change_notify_multi_ns_netkit,
+                        _psp_dev_get_check_netkit_psp_assoc,
+                        _dev_assoc_no_nsid,
+                        _psp_dev_assoc_cleanup_on_netkit_del,
+                    ]
 
                 ksft_run(cases=cases, globs=globals(),
                          case_pfx={"dev_", "data_", "assoc_", "removal_"},
-- 
2.52.0


^ permalink raw reply related

* [PATCH v12 net-next 4/5] selftests/net: Add bpf skb forwarding program
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang, Bobby Eshleman
In-Reply-To: <20260501175528.2130551-1-weibunny.kernel@gmail.com>

From: Wei Wang <weibunny@fb.com>

Add nk_redirect.bpf.c, a BPF program that forwards skbs matching some IPv6
prefix received on eth0 ifindex to a specified dev ifindex.
bpf_redirect_neigh() is used to make sure neighbor lookup is performed
and proper MAC addr is being used.

Signed-off-by: Wei Wang <weibunny@fb.com>
Reviewed-by: Bobby Eshleman <bobbyeshleman@meta.com>
Tested-by: Bobby Eshleman <bobbyeshleman@meta.com>
---
 .../drivers/net/hw/nk_redirect.bpf.c          | 60 +++++++++++++++++++
 1 file changed, 60 insertions(+)
 create mode 100644 tools/testing/selftests/drivers/net/hw/nk_redirect.bpf.c

diff --git a/tools/testing/selftests/drivers/net/hw/nk_redirect.bpf.c b/tools/testing/selftests/drivers/net/hw/nk_redirect.bpf.c
new file mode 100644
index 000000000000..7ac9ffd50f15
--- /dev/null
+++ b/tools/testing/selftests/drivers/net/hw/nk_redirect.bpf.c
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * BPF program for redirecting traffic using bpf_redirect_neigh().
+ * Unlike bpf_redirect() which preserves L2 headers, bpf_redirect_neigh()
+ * performs neighbor lookup and fills in the correct L2 addresses for the
+ * target interface. This is necessary when redirecting across different
+ * device types (e.g., from netdevsim to netkit).
+ */
+#include <linux/bpf.h>
+#include <linux/pkt_cls.h>
+#include <linux/if_ether.h>
+#include <linux/ipv6.h>
+#include <linux/in6.h>
+#include <bpf/bpf_endian.h>
+#include <bpf/bpf_helpers.h>
+
+#define TC_ACT_OK 0
+#define ETH_P_IPV6 0x86DD
+
+#define ctx_ptr(field)		((void *)(long)(field))
+
+#define v6_p64_equal(a, b)	(a.s6_addr32[0] == b.s6_addr32[0] && \
+				 a.s6_addr32[1] == b.s6_addr32[1])
+
+volatile __u32 redirect_ifindex;
+volatile __u8 ipv6_prefix[16];
+
+SEC("tc/ingress")
+int tc_redirect(struct __sk_buff *skb)
+{
+	void *data_end = ctx_ptr(skb->data_end);
+	void *data = ctx_ptr(skb->data);
+	struct in6_addr *match_prefix;
+	struct ipv6hdr *ip6h;
+	struct ethhdr *eth;
+
+	match_prefix = (struct in6_addr *)ipv6_prefix;
+
+	if (skb->protocol != bpf_htons(ETH_P_IPV6))
+		return TC_ACT_OK;
+
+	eth = data;
+	if ((void *)(eth + 1) > data_end)
+		return TC_ACT_OK;
+
+	ip6h = data + sizeof(struct ethhdr);
+	if ((void *)(ip6h + 1) > data_end)
+		return TC_ACT_OK;
+
+	if (!v6_p64_equal(ip6h->daddr, (*match_prefix)))
+		return TC_ACT_OK;
+
+	/*
+	 * Use bpf_redirect_neigh() to perform neighbor lookup and fill in
+	 * correct L2 addresses for the target interface.
+	 */
+	return bpf_redirect_neigh(redirect_ifindex, NULL, 0, 0);
+}
+
+char __license[] SEC("license") = "GPL";
-- 
2.52.0


^ permalink raw reply related

* [PATCH v12 net-next 3/5] psp: add a new netdev event for dev unregister
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang
In-Reply-To: <20260501175528.2130551-1-weibunny.kernel@gmail.com>

From: Wei Wang <weibunny@fb.com>

Add a new netdev event for dev unregister and handle the removal of this
dev from psp->assoc_dev_list, upon the first dev-assoc operation.

Signed-off-by: Wei Wang <weibunny@fb.com>
Reviewed-by: Daniel Zahka <daniel.zahka@gmail.com>
---
 Documentation/netlink/specs/psp.yaml |  2 +-
 net/psp/psp-nl-gen.c                 |  2 +-
 net/psp/psp-nl-gen.h                 |  3 ++
 net/psp/psp.h                        |  1 +
 net/psp/psp_main.c                   | 76 ++++++++++++++++++++++++++++
 net/psp/psp_nl.c                     | 29 +++++++++++
 6 files changed, 111 insertions(+), 2 deletions(-)

diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml
index 3d1b7223e084..538ed9184965 100644
--- a/Documentation/netlink/specs/psp.yaml
+++ b/Documentation/netlink/specs/psp.yaml
@@ -328,7 +328,7 @@ operations:
             - nsid
         reply:
           attributes: []
-        pre: psp-device-get-locked
+        pre: psp-device-get-locked-dev-assoc
         post: psp-device-unlock
     -
       name: dev-disassoc
diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c
index 114299c64423..389a8480cc3d 100644
--- a/net/psp/psp-nl-gen.c
+++ b/net/psp/psp-nl-gen.c
@@ -135,7 +135,7 @@ static const struct genl_split_ops psp_nl_ops[] = {
 	},
 	{
 		.cmd		= PSP_CMD_DEV_ASSOC,
-		.pre_doit	= psp_device_get_locked,
+		.pre_doit	= psp_device_get_locked_dev_assoc,
 		.doit		= psp_nl_dev_assoc_doit,
 		.post_doit	= psp_device_unlock,
 		.policy		= psp_dev_assoc_nl_policy,
diff --git a/net/psp/psp-nl-gen.h b/net/psp/psp-nl-gen.h
index 4dd0f0f23053..24d51bff997f 100644
--- a/net/psp/psp-nl-gen.h
+++ b/net/psp/psp-nl-gen.h
@@ -21,6 +21,9 @@ int psp_device_get_locked_admin(const struct genl_split_ops *ops,
 				struct sk_buff *skb, struct genl_info *info);
 int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 				struct sk_buff *skb, struct genl_info *info);
+int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops,
+				    struct sk_buff *skb,
+				    struct genl_info *info);
 void
 psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb,
 		  struct genl_info *info);
diff --git a/net/psp/psp.h b/net/psp/psp.h
index 0f9c4e4e52cb..c82b21bae240 100644
--- a/net/psp/psp.h
+++ b/net/psp/psp.h
@@ -15,6 +15,7 @@ extern struct mutex psp_devs_lock;
 
 void psp_dev_free(struct psp_dev *psd);
 int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin);
+int psp_attach_netdev_notifier(void);
 
 void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd);
 
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index 792a47105a6c..a57d30db22be 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -391,6 +391,82 @@ int psp_dev_rcv(struct sk_buff *skb, u16 dev_id, u8 generation, bool strip_icv)
 }
 EXPORT_SYMBOL(psp_dev_rcv);
 
+static void psp_dev_disassoc_one(struct psp_dev *psd, struct net_device *dev)
+{
+	struct psp_assoc_dev *entry, *tmp;
+
+	list_for_each_entry_safe(entry, tmp, &psd->assoc_dev_list, dev_list) {
+		if (entry->assoc_dev == dev) {
+			list_del(&entry->dev_list);
+			psd->assoc_dev_cnt--;
+			rcu_assign_pointer(entry->assoc_dev->psp_dev, NULL);
+			netdev_put(entry->assoc_dev, &entry->dev_tracker);
+			kfree(entry);
+			return;
+		}
+	}
+}
+
+static int psp_netdev_event(struct notifier_block *nb, unsigned long event,
+			    void *ptr)
+{
+	struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+	struct psp_dev *psd;
+
+	if (event != NETDEV_UNREGISTER)
+		return NOTIFY_DONE;
+
+	rcu_read_lock();
+	psd = rcu_dereference(dev->psp_dev);
+	if (psd && psp_dev_tryget(psd)) {
+		rcu_read_unlock();
+		mutex_lock(&psd->lock);
+		psp_dev_disassoc_one(psd, dev);
+		mutex_unlock(&psd->lock);
+		psp_dev_put(psd);
+	} else {
+		rcu_read_unlock();
+	}
+
+	return NOTIFY_DONE;
+}
+
+static struct notifier_block psp_netdev_notifier = {
+	.notifier_call = psp_netdev_event,
+};
+
+static DEFINE_MUTEX(psp_notifier_lock);
+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).
+ */
+int psp_attach_netdev_notifier(void)
+{
+	int err = 0;
+
+	if (READ_ONCE(psp_notifier_registered))
+		return 0;
+
+	mutex_lock(&psp_notifier_lock);
+	if (!psp_notifier_registered) {
+		err = register_netdevice_notifier(&psp_netdev_notifier);
+		if (!err)
+			WRITE_ONCE(psp_notifier_registered, true);
+	}
+	mutex_unlock(&psp_notifier_lock);
+
+	return err;
+}
+
 static int __init psp_init(void)
 {
 	mutex_init(&psp_devs_lock);
diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c
index 75ca32821d28..d622f91a979e 100644
--- a/net/psp/psp_nl.c
+++ b/net/psp/psp_nl.c
@@ -167,6 +167,22 @@ int psp_device_get_locked(const struct genl_split_ops *ops,
 	return __psp_device_get_locked(ops, skb, info, false);
 }
 
+/*
+ * Non-admin version of psp_device_get_locked() + psp_attach_netdev_notifier()
+ * only used for dev-assoc.
+ */
+int psp_device_get_locked_dev_assoc(const struct genl_split_ops *ops,
+				    struct sk_buff *skb, struct genl_info *info)
+{
+	int err;
+
+	err = psp_attach_netdev_notifier();
+	if (err)
+		return err;
+
+	return __psp_device_get_locked(ops, skb, info, false);
+}
+
 static struct net *psp_nl_resolve_assoc_dev_ns(struct psp_dev *psd,
 					       struct genl_info *info)
 {
@@ -532,6 +548,19 @@ int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info)
 	}
 
 	psp_assoc_dev->assoc_dev = assoc_dev;
+
+	/* Check for race with NETDEV_UNREGISTER. The cmpxchg above is a
+	 * full barrier, and the unregister path has synchronize_net()
+	 * between setting NETREG_UNREGISTERING and reading psp_dev in the
+	 * notifier. So at least one side would do the clean-up if we are in
+	 * the middle of unregitering assoc_dev.
+	 * And the clean-up is serialized by psd->lock.
+	 */
+	if (READ_ONCE(assoc_dev->reg_state) != NETREG_REGISTERED) {
+		err = -ENODEV;
+		goto rsp_err;
+	}
+
 	rsp = psp_nl_reply_new(info);
 	if (!rsp) {
 		err = -ENOMEM;
-- 
2.52.0


^ permalink raw reply related

* [PATCH v12 net-next 2/5] psp: add new netlink cmd for dev-assoc and dev-disassoc
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang
In-Reply-To: <20260501175528.2130551-1-weibunny.kernel@gmail.com>

From: Wei Wang <weibunny@fb.com>

The main purpose of this cmd is to be able to associate a
non-psp-capable device (e.g. veth or netkit) with a psp device.
One use case is if we create a pair of veth/netkit, and assign 1 end
inside a netns, while leaving the other end within the default netns,
with a real PSP device, e.g. netdevsim or a physical PSP-capable NIC.
With this command, we could associate the veth/netkit inside the netns
with PSP device, so the virtual device could act as PSP-capable device
to initiate PSP connections, and performs PSP encryption/decryption on
the real PSP device.

Signed-off-by: Wei Wang <weibunny@fb.com>
Reviewed-by: Daniel Zahka <daniel.zahka@gmail.com>
---
 Documentation/netlink/specs/psp.yaml |  67 +++++-
 include/net/psp/types.h              |  23 ++
 include/uapi/linux/psp.h             |  13 +
 net/psp/psp-nl-gen.c                 |  32 +++
 net/psp/psp-nl-gen.h                 |   2 +
 net/psp/psp_main.c                   |  21 ++
 net/psp/psp_nl.c                     | 340 ++++++++++++++++++++++++++-
 7 files changed, 486 insertions(+), 12 deletions(-)

diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml
index c54e1202cbe0..3d1b7223e084 100644
--- a/Documentation/netlink/specs/psp.yaml
+++ b/Documentation/netlink/specs/psp.yaml
@@ -13,6 +13,17 @@ definitions:
               hdr0-aes-gmac-128, hdr0-aes-gmac-256]
 
 attribute-sets:
+  -
+    name: assoc-dev-info
+    attributes:
+      -
+        name: ifindex
+        doc: ifindex of an associated network device.
+        type: u32
+      -
+        name: nsid
+        doc: Network namespace ID of the associated device.
+        type: s32
   -
     name: dev
     attributes:
@@ -24,7 +35,9 @@ attribute-sets:
           min: 1
       -
         name: ifindex
-        doc: ifindex of the main netdevice linked to the PSP device.
+        doc: |
+          ifindex of the main netdevice linked to the PSP device,
+          or the ifindex to associate with the PSP device.
         type: u32
       -
         name: psp-versions-cap
@@ -38,6 +51,28 @@ attribute-sets:
         type: u32
         enum: version
         enum-as-flags: true
+      -
+        name: assoc-list
+        doc: List of associated virtual devices.
+        type: nest
+        nested-attributes: assoc-dev-info
+        multi-attr: true
+      -
+        name: nsid
+        doc: |
+          Network namespace ID for the device to associate/disassociate.
+          Optional for dev-assoc and dev-disassoc; if not present, the
+          device is looked up in the caller's network namespace.
+        type: s32
+      -
+        name: by-association
+        doc: |
+          Flag indicating the PSP device is an associated device from a
+          different network namespace.
+          Present when in associated namespace, absent when in primary/host
+          namespace.
+        type: flag
+
   -
     name: assoc
     attributes:
@@ -170,6 +205,8 @@ operations:
             - ifindex
             - psp-versions-cap
             - psp-versions-ena
+            - assoc-list
+            - by-association
         pre: psp-device-get-locked
         post: psp-device-unlock
       dump:
@@ -279,6 +316,34 @@ operations:
         post: psp-device-unlock
       dump:
         reply: *stats-all
+    -
+      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
 
 mcast-groups:
   list:
diff --git a/include/net/psp/types.h b/include/net/psp/types.h
index 25a9096d4e7d..87991a1ea02d 100644
--- a/include/net/psp/types.h
+++ b/include/net/psp/types.h
@@ -5,6 +5,7 @@
 
 #include <linux/mutex.h>
 #include <linux/refcount.h>
+#include <net/net_trackers.h>
 
 struct netlink_ext_ack;
 
@@ -43,9 +44,29 @@ struct psp_dev_config {
 	u32 versions;
 };
 
+/* Max number of devices that can be associated with a single PSP device.
+ * Each entry consumes ~24 bytes in the netlink dev-get response, and the
+ * response must fit in GENLMSG_DEFAULT_SIZE (~3.7KB).
+ */
+#define PSP_ASSOC_DEV_MAX	128
+
+/**
+ * struct psp_assoc_dev - wrapper for associated net_device
+ * @dev_list: list node for psp_dev::assoc_dev_list
+ * @assoc_dev: the associated net_device
+ * @dev_tracker: tracker for the net_device reference
+ */
+struct psp_assoc_dev {
+	struct list_head dev_list;
+	struct net_device *assoc_dev;
+	netdevice_tracker dev_tracker;
+};
+
 /**
  * struct psp_dev - PSP device struct
  * @main_netdev: original netdevice of this PSP device
+ * @assoc_dev_list: list of psp_assoc_dev entries associated with this PSP device
+ * @assoc_dev_cnt: number of entries in @assoc_dev_list
  * @ops:	driver callbacks
  * @caps:	device capabilities
  * @drv_priv:	driver priv pointer
@@ -67,6 +88,8 @@ struct psp_dev_config {
  */
 struct psp_dev {
 	struct net_device *main_netdev;
+	struct list_head assoc_dev_list;
+	int assoc_dev_cnt;
 
 	struct psp_dev_ops *ops;
 	struct psp_dev_caps *caps;
diff --git a/include/uapi/linux/psp.h b/include/uapi/linux/psp.h
index a3a336488dc3..1c8899cd4da5 100644
--- a/include/uapi/linux/psp.h
+++ b/include/uapi/linux/psp.h
@@ -17,11 +17,22 @@ enum psp_version {
 	PSP_VERSION_HDR0_AES_GMAC_256,
 };
 
+enum {
+	PSP_A_ASSOC_DEV_INFO_IFINDEX = 1,
+	PSP_A_ASSOC_DEV_INFO_NSID,
+
+	__PSP_A_ASSOC_DEV_INFO_MAX,
+	PSP_A_ASSOC_DEV_INFO_MAX = (__PSP_A_ASSOC_DEV_INFO_MAX - 1)
+};
+
 enum {
 	PSP_A_DEV_ID = 1,
 	PSP_A_DEV_IFINDEX,
 	PSP_A_DEV_PSP_VERSIONS_CAP,
 	PSP_A_DEV_PSP_VERSIONS_ENA,
+	PSP_A_DEV_ASSOC_LIST,
+	PSP_A_DEV_NSID,
+	PSP_A_DEV_BY_ASSOCIATION,
 
 	__PSP_A_DEV_MAX,
 	PSP_A_DEV_MAX = (__PSP_A_DEV_MAX - 1)
@@ -74,6 +85,8 @@ enum {
 	PSP_CMD_RX_ASSOC,
 	PSP_CMD_TX_ASSOC,
 	PSP_CMD_GET_STATS,
+	PSP_CMD_DEV_ASSOC,
+	PSP_CMD_DEV_DISASSOC,
 
 	__PSP_CMD_MAX,
 	PSP_CMD_MAX = (__PSP_CMD_MAX - 1)
diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c
index 1f5e73e7ccc1..114299c64423 100644
--- a/net/psp/psp-nl-gen.c
+++ b/net/psp/psp-nl-gen.c
@@ -53,6 +53,20 @@ static const struct nla_policy psp_get_stats_nl_policy[PSP_A_STATS_DEV_ID + 1] =
 	[PSP_A_STATS_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
 };
 
+/* PSP_CMD_DEV_ASSOC - do */
+static const struct nla_policy psp_dev_assoc_nl_policy[PSP_A_DEV_NSID + 1] = {
+	[PSP_A_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+	[PSP_A_DEV_IFINDEX] = { .type = NLA_U32, },
+	[PSP_A_DEV_NSID] = { .type = NLA_S32, },
+};
+
+/* PSP_CMD_DEV_DISASSOC - do */
+static const struct nla_policy psp_dev_disassoc_nl_policy[PSP_A_DEV_NSID + 1] = {
+	[PSP_A_DEV_ID] = NLA_POLICY_MIN(NLA_U32, 1),
+	[PSP_A_DEV_IFINDEX] = { .type = NLA_U32, },
+	[PSP_A_DEV_NSID] = { .type = NLA_S32, },
+};
+
 /* Ops table for psp */
 static const struct genl_split_ops psp_nl_ops[] = {
 	{
@@ -119,6 +133,24 @@ static const struct genl_split_ops psp_nl_ops[] = {
 		.dumpit	= psp_nl_get_stats_dumpit,
 		.flags	= GENL_CMD_CAP_DUMP,
 	},
+	{
+		.cmd		= PSP_CMD_DEV_ASSOC,
+		.pre_doit	= psp_device_get_locked,
+		.doit		= psp_nl_dev_assoc_doit,
+		.post_doit	= psp_device_unlock,
+		.policy		= psp_dev_assoc_nl_policy,
+		.maxattr	= PSP_A_DEV_NSID,
+		.flags		= GENL_CMD_CAP_DO,
+	},
+	{
+		.cmd		= PSP_CMD_DEV_DISASSOC,
+		.pre_doit	= psp_device_get_locked,
+		.doit		= psp_nl_dev_disassoc_doit,
+		.post_doit	= psp_device_unlock,
+		.policy		= psp_dev_disassoc_nl_policy,
+		.maxattr	= PSP_A_DEV_NSID,
+		.flags		= GENL_CMD_CAP_DO,
+	},
 };
 
 static const struct genl_multicast_group psp_nl_mcgrps[] = {
diff --git a/net/psp/psp-nl-gen.h b/net/psp/psp-nl-gen.h
index 977355455395..4dd0f0f23053 100644
--- a/net/psp/psp-nl-gen.h
+++ b/net/psp/psp-nl-gen.h
@@ -33,6 +33,8 @@ int psp_nl_rx_assoc_doit(struct sk_buff *skb, struct genl_info *info);
 int psp_nl_tx_assoc_doit(struct sk_buff *skb, struct genl_info *info);
 int psp_nl_get_stats_doit(struct sk_buff *skb, struct genl_info *info);
 int psp_nl_get_stats_dumpit(struct sk_buff *skb, struct netlink_callback *cb);
+int psp_nl_dev_assoc_doit(struct sk_buff *skb, struct genl_info *info);
+int psp_nl_dev_disassoc_doit(struct sk_buff *skb, struct genl_info *info);
 
 enum {
 	PSP_NLGRP_MGMT,
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index b56a51d524f5..792a47105a6c 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -37,8 +37,18 @@ struct mutex psp_devs_lock;
  */
 int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin)
 {
+	struct psp_assoc_dev *entry;
+
 	if (dev_net(psd->main_netdev) == net)
 		return 0;
+
+	if (!admin) {
+		list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) {
+			if (dev_net(entry->assoc_dev) == net)
+				return 0;
+		}
+	}
+
 	return -ENOENT;
 }
 
@@ -74,6 +84,7 @@ psp_dev_create(struct net_device *netdev,
 		return ERR_PTR(-ENOMEM);
 
 	psd->main_netdev = netdev;
+	INIT_LIST_HEAD(&psd->assoc_dev_list);
 	psd->ops = psd_ops;
 	psd->caps = psd_caps;
 	psd->drv_priv = priv_ptr;
@@ -125,6 +136,7 @@ void psp_dev_free(struct psp_dev *psd)
  */
 void psp_dev_unregister(struct psp_dev *psd)
 {
+	struct psp_assoc_dev *entry, *entry_tmp;
 	struct psp_assoc *pas, *next;
 
 	mutex_lock(&psp_devs_lock);
@@ -144,6 +156,15 @@ void psp_dev_unregister(struct psp_dev *psd)
 	list_for_each_entry_safe(pas, next, &psd->stale_assocs, assocs_list)
 		psp_dev_tx_key_del(psd, pas);
 
+	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);
+	}
+	psd->assoc_dev_cnt = 0;
+
 	rcu_assign_pointer(psd->main_netdev->psp_dev, NULL);
 
 	psd->ops = NULL;
diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c
index eb47a9ee4438..75ca32821d28 100644
--- a/net/psp/psp_nl.c
+++ b/net/psp/psp_nl.c
@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: GPL-2.0-only
 
 #include <linux/ethtool.h>
+#include <linux/net_namespace.h>
 #include <linux/skbuff.h>
 #include <linux/xarray.h>
 #include <net/genetlink.h>
@@ -38,6 +39,73 @@ static int psp_nl_reply_send(struct sk_buff *rsp, struct genl_info *info)
 	return genlmsg_reply(rsp, info);
 }
 
+/**
+ * psp_nl_multicast_per_ns() - multicast a notification to each unique netns
+ * @psd: PSP device (must be locked)
+ * @group: multicast group
+ * @build_ntf: callback to build an skb for a given netns, or NULL on failure
+ * @ctx: opaque context passed to @build_ntf
+ *
+ * Iterates all unique network namespaces from the associated device list
+ * plus the main device's netns. For each unique netns, calls @build_ntf
+ * to construct a notification skb and multicasts it.
+ */
+static void psp_nl_multicast_per_ns(struct psp_dev *psd, unsigned int group,
+				    struct sk_buff *(*build_ntf)(struct psp_dev *,
+								 struct net *,
+								 void *),
+				    void *ctx)
+{
+	struct psp_assoc_dev *entry;
+	struct xarray sent_nets;
+	struct net *main_net;
+	struct sk_buff *ntf;
+
+	main_net = dev_net(psd->main_netdev);
+	xa_init(&sent_nets);
+
+	list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) {
+		struct net *assoc_net = dev_net(entry->assoc_dev);
+		int ret;
+
+		if (net_eq(assoc_net, main_net))
+			continue;
+
+		ret = xa_insert(&sent_nets, (unsigned long)assoc_net, assoc_net,
+				GFP_KERNEL);
+		if (ret == -EBUSY)
+			continue;
+
+		ntf = build_ntf(psd, assoc_net, ctx);
+		if (!ntf)
+			continue;
+
+		genlmsg_multicast_netns(&psp_nl_family, assoc_net, ntf, 0,
+					group, GFP_KERNEL);
+	}
+	xa_destroy(&sent_nets);
+
+	/* Send to main device netns */
+	ntf = build_ntf(psd, main_net, ctx);
+	if (!ntf)
+		return;
+	genlmsg_multicast_netns(&psp_nl_family, main_net, ntf, 0, group,
+				GFP_KERNEL);
+}
+
+static struct sk_buff *psp_nl_clone_ntf(struct psp_dev *psd, struct net *net,
+					void *ctx)
+{
+	return skb_clone(ctx, GFP_KERNEL);
+}
+
+static void psp_nl_multicast_all_ns(struct psp_dev *psd, struct sk_buff *ntf,
+				    unsigned int group)
+{
+	psp_nl_multicast_per_ns(psd, group, psp_nl_clone_ntf, ntf);
+	nlmsg_free(ntf);
+}
+
 /* Device stuff */
 
 static struct psp_dev *
@@ -79,18 +147,58 @@ static int __psp_device_get_locked(const struct genl_split_ops *ops,
 	return PTR_ERR_OR_ZERO(info->user_ptr[0]);
 }
 
+/*
+ * Admin version of psp_device_get_locked() where it returns psd only if
+ * current netns is the same as psd->main_netdev's netns.
+ */
 int psp_device_get_locked_admin(const struct genl_split_ops *ops,
 				struct sk_buff *skb, struct genl_info *info)
 {
 	return __psp_device_get_locked(ops, skb, info, true);
 }
 
+/*
+ * Non-admin version of psp_device_get_locked() where it returns psd in netns
+ * for not only psd->main_netdev but all netdevs in psd->assoc_dev_list.
+ */
 int psp_device_get_locked(const struct genl_split_ops *ops,
 			  struct sk_buff *skb, struct genl_info *info)
 {
 	return __psp_device_get_locked(ops, skb, info, false);
 }
 
+static struct net *psp_nl_resolve_assoc_dev_ns(struct psp_dev *psd,
+					       struct genl_info *info)
+{
+	struct net *net;
+	int nsid;
+
+	if (GENL_REQ_ATTR_CHECK(info, PSP_A_DEV_IFINDEX))
+		return ERR_PTR(-EINVAL);
+
+	if (info->attrs[PSP_A_DEV_NSID]) {
+		/* Only callers in the main netns may specify nsid */
+		if (dev_net(psd->main_netdev) != genl_info_net(info)) {
+			NL_SET_BAD_ATTR(info->extack,
+					info->attrs[PSP_A_DEV_NSID]);
+			return ERR_PTR(-EPERM);
+		}
+
+		nsid = nla_get_s32(info->attrs[PSP_A_DEV_NSID]);
+
+		net = get_net_ns_by_id(genl_info_net(info), nsid);
+		if (!net) {
+			NL_SET_BAD_ATTR(info->extack,
+					info->attrs[PSP_A_DEV_NSID]);
+			return ERR_PTR(-EINVAL);
+		}
+	} else {
+		net = get_net(genl_info_net(info));
+	}
+
+	return net;
+}
+
 void
 psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb,
 		  struct genl_info *info)
@@ -103,11 +211,74 @@ psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb,
 		sockfd_put(socket);
 }
 
+static bool psp_has_assoc_dev_in_ns(struct psp_dev *psd, struct net *net)
+{
+	struct psp_assoc_dev *entry;
+
+	list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) {
+		if (dev_net(entry->assoc_dev) == net)
+			return true;
+	}
+
+	return false;
+}
+
+static int psp_nl_fill_assoc_dev_list(struct psp_dev *psd, struct sk_buff *rsp,
+				      struct net *cur_net,
+				      struct net *filter_net)
+{
+	struct psp_assoc_dev *entry;
+	struct net *dev_net_ns;
+	struct nlattr *nest;
+	int nsid;
+
+	list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) {
+		dev_net_ns = dev_net(entry->assoc_dev);
+
+		if (filter_net && dev_net_ns != filter_net)
+			continue;
+
+		/* When filtering by namespace, all devices are in the caller's
+		 * namespace so nsid is always NETNSA_NSID_NOT_ASSIGNED (-1).
+		 * Otherwise, calculate the nsid relative to cur_net.
+		 */
+		nsid = filter_net ? NETNSA_NSID_NOT_ASSIGNED :
+				    peernet2id_alloc(cur_net, dev_net_ns,
+						     GFP_KERNEL);
+
+		nest = nla_nest_start(rsp, PSP_A_DEV_ASSOC_LIST);
+		if (!nest)
+			return -1;
+
+		if (nla_put_u32(rsp, PSP_A_ASSOC_DEV_INFO_IFINDEX,
+				entry->assoc_dev->ifindex) ||
+		    nla_put_s32(rsp, PSP_A_ASSOC_DEV_INFO_NSID, nsid)) {
+			nla_nest_cancel(rsp, nest);
+			return -1;
+		}
+
+		nla_nest_end(rsp, nest);
+	}
+
+	return 0;
+}
+
 static int
 psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp,
 		const struct genl_info *info)
 {
+	struct net *cur_net;
 	void *hdr;
+	int err;
+
+	cur_net = genl_info_net(info);
+
+	/* Skip this device if we're in an associated netns but have no
+	 * associated devices in cur_net
+	 */
+	if (cur_net != dev_net(psd->main_netdev) &&
+	    !psp_has_assoc_dev_in_ns(psd, cur_net))
+		return 0;
 
 	hdr = genlmsg_iput(rsp, info);
 	if (!hdr)
@@ -119,6 +290,22 @@ psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp,
 	    nla_put_u32(rsp, PSP_A_DEV_PSP_VERSIONS_ENA, psd->config.versions))
 		goto err_cancel_msg;
 
+	if (cur_net == dev_net(psd->main_netdev)) {
+		/* Primary device - dump assoc list */
+		err = psp_nl_fill_assoc_dev_list(psd, rsp, cur_net, NULL);
+		if (err)
+			goto err_cancel_msg;
+	} else {
+		/* In netns: set by-association flag and dump filtered
+		 * assoc list containing only devices in cur_net
+		 */
+		if (nla_put_flag(rsp, PSP_A_DEV_BY_ASSOCIATION))
+			goto err_cancel_msg;
+		err = psp_nl_fill_assoc_dev_list(psd, rsp, cur_net, cur_net);
+		if (err)
+			goto err_cancel_msg;
+	}
+
 	genlmsg_end(rsp, hdr);
 	return 0;
 
@@ -127,27 +314,34 @@ psp_nl_dev_fill(struct psp_dev *psd, struct sk_buff *rsp,
 	return -EMSGSIZE;
 }
 
-void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd)
+static struct sk_buff *psp_nl_build_dev_ntf(struct psp_dev *psd,
+					    struct net *net, void *ctx)
 {
+	u32 cmd = *(u32 *)ctx;
 	struct genl_info info;
 	struct sk_buff *ntf;
 
-	if (!genl_has_listeners(&psp_nl_family, dev_net(psd->main_netdev),
-				PSP_NLGRP_MGMT))
-		return;
+	if (!genl_has_listeners(&psp_nl_family, net, PSP_NLGRP_MGMT))
+		return NULL;
 
 	ntf = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL);
 	if (!ntf)
-		return;
+		return NULL;
 
 	genl_info_init_ntf(&info, &psp_nl_family, cmd);
+	genl_info_net_set(&info, net);
 	if (psp_nl_dev_fill(psd, ntf, &info)) {
 		nlmsg_free(ntf);
-		return;
+		return NULL;
 	}
 
-	genlmsg_multicast_netns(&psp_nl_family, dev_net(psd->main_netdev), ntf,
-				0, PSP_NLGRP_MGMT, GFP_KERNEL);
+	return ntf;
+}
+
+void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd)
+{
+	psp_nl_multicast_per_ns(psd, PSP_NLGRP_MGMT,
+				psp_nl_build_dev_ntf, &cmd);
 }
 
 int psp_nl_dev_get_doit(struct sk_buff *req, struct genl_info *info)
@@ -281,8 +475,9 @@ int psp_nl_key_rotate_doit(struct sk_buff *skb, struct genl_info *info)
 	psd->stats.rotations++;
 
 	nlmsg_end(ntf, (struct nlmsghdr *)ntf->data);
-	genlmsg_multicast_netns(&psp_nl_family, dev_net(psd->main_netdev), ntf,
-				0, PSP_NLGRP_USE, GFP_KERNEL);
+
+	psp_nl_multicast_all_ns(psd, ntf, PSP_NLGRP_USE);
+
 	return psp_nl_reply_send(rsp, info);
 
 err_free_ntf:
@@ -292,6 +487,128 @@ int psp_nl_key_rotate_doit(struct sk_buff *skb, struct genl_info *info)
 	return err;
 }
 
+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;
+	struct sk_buff *rsp;
+	u32 assoc_ifindex;
+	struct net *net;
+	int err;
+
+	if (psd->assoc_dev_cnt >= PSP_ASSOC_DEV_MAX) {
+		NL_SET_ERR_MSG(info->extack,
+			       "Maximum number of associated devices reached");
+		return -ENOSPC;
+	}
+
+	net = psp_nl_resolve_assoc_dev_ns(psd, info);
+	if (IS_ERR(net))
+		return PTR_ERR(net);
+
+	psp_assoc_dev = kzalloc_obj(*psp_assoc_dev, GFP_KERNEL);
+	if (!psp_assoc_dev) {
+		err = -ENOMEM;
+		goto err_put_net;
+	}
+
+	assoc_ifindex = nla_get_u32(info->attrs[PSP_A_DEV_IFINDEX]);
+	assoc_dev = netdev_get_by_index(net, assoc_ifindex,
+					&psp_assoc_dev->dev_tracker,
+					GFP_KERNEL);
+	if (!assoc_dev) {
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_DEV_IFINDEX]);
+		err = -ENODEV;
+		goto assoc_dev_err;
+	}
+
+	/* Check if device is already associated with a PSP device */
+	if (cmpxchg(&assoc_dev->psp_dev, NULL, RCU_INITIALIZER(psd))) {
+		NL_SET_ERR_MSG(info->extack,
+			       "Device already associated with a PSP device");
+		err = -EBUSY;
+		goto cmpxchg_err;
+	}
+
+	psp_assoc_dev->assoc_dev = assoc_dev;
+	rsp = psp_nl_reply_new(info);
+	if (!rsp) {
+		err = -ENOMEM;
+		goto rsp_err;
+	}
+
+	list_add_tail(&psp_assoc_dev->dev_list, &psd->assoc_dev_list);
+	psd->assoc_dev_cnt++;
+
+	put_net(net);
+
+	psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF);
+
+	return psp_nl_reply_send(rsp, info);
+
+rsp_err:
+	rcu_assign_pointer(assoc_dev->psp_dev, NULL);
+cmpxchg_err:
+	netdev_put(assoc_dev, &psp_assoc_dev->dev_tracker);
+assoc_dev_err:
+	kfree(psp_assoc_dev);
+err_put_net:
+	put_net(net);
+
+	return err;
+}
+
+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];
+	struct sk_buff *rsp;
+	u32 assoc_ifindex;
+	struct net *net;
+
+	net = psp_nl_resolve_assoc_dev_ns(psd, info);
+	if (IS_ERR(net))
+		return PTR_ERR(net);
+
+	assoc_ifindex = nla_get_u32(info->attrs[PSP_A_DEV_IFINDEX]);
+
+	/* Search the association list by ifindex and netns */
+	list_for_each_entry(entry, &psd->assoc_dev_list, dev_list) {
+		if (entry->assoc_dev->ifindex == assoc_ifindex &&
+		    dev_net(entry->assoc_dev) == net) {
+			found = entry;
+			break;
+		}
+	}
+
+	if (!found) {
+		put_net(net);
+		NL_SET_BAD_ATTR(info->extack, info->attrs[PSP_A_DEV_IFINDEX]);
+		return -ENODEV;
+	}
+
+	rsp = psp_nl_reply_new(info);
+	if (!rsp) {
+		put_net(net);
+		return -ENOMEM;
+	}
+
+	/* Notify before removal */
+	psp_nl_notify_dev(psd, PSP_CMD_DEV_CHANGE_NTF);
+
+	/* Remove from the association list */
+	list_del(&found->dev_list);
+	psd->assoc_dev_cnt--;
+	rcu_assign_pointer(found->assoc_dev->psp_dev, NULL);
+	netdev_put(found->assoc_dev, &found->dev_tracker);
+	kfree(found);
+
+	put_net(net);
+
+	return psp_nl_reply_send(rsp, info);
+}
+
 /* Key etc. */
 
 int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
@@ -320,8 +637,10 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 
 	psd = psp_dev_get_for_sock(socket->sk);
 	if (psd) {
+		mutex_lock(&psd->lock);
 		err = psp_dev_check_access(psd, genl_info_net(info), false);
 		if (err) {
+			mutex_unlock(&psd->lock);
 			psp_dev_put(psd);
 			psd = NULL;
 		}
@@ -334,7 +653,6 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 
 	id = info->attrs[PSP_A_ASSOC_DEV_ID];
 	if (psd) {
-		mutex_lock(&psd->lock);
 		if (id && psd->id != nla_get_u32(id)) {
 			mutex_unlock(&psd->lock);
 			NL_SET_ERR_MSG_ATTR(info->extack, id,
-- 
2.52.0


^ permalink raw reply related

* [PATCH v12 net-next 1/5] psp: add admin/non-admin version of psp_device_get_locked
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang
In-Reply-To: <20260501175528.2130551-1-weibunny.kernel@gmail.com>

From: Wei Wang <weibunny@fb.com>

Introduce 2 versions of psp_device_get_locked:
1. psp_device_get_locked_admin(): This version is used for operations
   that would change the status of the psd, and are currently used for
   dev-set and key-rotation.
2. psp_device_get_locked(): This is the non-admin version, which are
   used for broader user issued operations including: dev-get, rx-assoc,
   tx-assoc, get-stats.

Following commit will be implementing both of the checks.

Signed-off-by: Wei Wang <weibunny@fb.com>
Reviewed-by: Daniel Zahka <daniel.zahka@gmail.com>
---
 Documentation/netlink/specs/psp.yaml |  4 ++--
 net/psp/psp-nl-gen.c                 |  4 ++--
 net/psp/psp-nl-gen.h                 |  2 ++
 net/psp/psp.h                        |  2 +-
 net/psp/psp_main.c                   |  7 +++++-
 net/psp/psp_nl.c                     | 33 ++++++++++++++++++++--------
 6 files changed, 37 insertions(+), 15 deletions(-)

diff --git a/Documentation/netlink/specs/psp.yaml b/Documentation/netlink/specs/psp.yaml
index 100c36cda8e5..c54e1202cbe0 100644
--- a/Documentation/netlink/specs/psp.yaml
+++ b/Documentation/netlink/specs/psp.yaml
@@ -195,7 +195,7 @@ operations:
             - psp-versions-ena
         reply:
           attributes: []
-        pre: psp-device-get-locked
+        pre: psp-device-get-locked-admin
         post: psp-device-unlock
     -
       name: dev-change-ntf
@@ -214,7 +214,7 @@ operations:
         reply:
           attributes:
             - id
-        pre: psp-device-get-locked
+        pre: psp-device-get-locked-admin
         post: psp-device-unlock
     -
       name: key-rotate-ntf
diff --git a/net/psp/psp-nl-gen.c b/net/psp/psp-nl-gen.c
index 22a48d0fa378..1f5e73e7ccc1 100644
--- a/net/psp/psp-nl-gen.c
+++ b/net/psp/psp-nl-gen.c
@@ -71,7 +71,7 @@ static const struct genl_split_ops psp_nl_ops[] = {
 	},
 	{
 		.cmd		= PSP_CMD_DEV_SET,
-		.pre_doit	= psp_device_get_locked,
+		.pre_doit	= psp_device_get_locked_admin,
 		.doit		= psp_nl_dev_set_doit,
 		.post_doit	= psp_device_unlock,
 		.policy		= psp_dev_set_nl_policy,
@@ -80,7 +80,7 @@ static const struct genl_split_ops psp_nl_ops[] = {
 	},
 	{
 		.cmd		= PSP_CMD_KEY_ROTATE,
-		.pre_doit	= psp_device_get_locked,
+		.pre_doit	= psp_device_get_locked_admin,
 		.doit		= psp_nl_key_rotate_doit,
 		.post_doit	= psp_device_unlock,
 		.policy		= psp_key_rotate_nl_policy,
diff --git a/net/psp/psp-nl-gen.h b/net/psp/psp-nl-gen.h
index 599c5f1c82f2..977355455395 100644
--- a/net/psp/psp-nl-gen.h
+++ b/net/psp/psp-nl-gen.h
@@ -17,6 +17,8 @@ extern const struct nla_policy psp_keys_nl_policy[PSP_A_KEYS_SPI + 1];
 
 int psp_device_get_locked(const struct genl_split_ops *ops,
 			  struct sk_buff *skb, struct genl_info *info);
+int psp_device_get_locked_admin(const struct genl_split_ops *ops,
+				struct sk_buff *skb, struct genl_info *info);
 int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 				struct sk_buff *skb, struct genl_info *info);
 void
diff --git a/net/psp/psp.h b/net/psp/psp.h
index 9f19137593a0..0f9c4e4e52cb 100644
--- a/net/psp/psp.h
+++ b/net/psp/psp.h
@@ -14,7 +14,7 @@ extern struct xarray psp_devs;
 extern struct mutex psp_devs_lock;
 
 void psp_dev_free(struct psp_dev *psd);
-int psp_dev_check_access(struct psp_dev *psd, struct net *net);
+int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin);
 
 void psp_nl_notify_dev(struct psp_dev *psd, u32 cmd);
 
diff --git a/net/psp/psp_main.c b/net/psp/psp_main.c
index 524978dfb8fd..b56a51d524f5 100644
--- a/net/psp/psp_main.c
+++ b/net/psp/psp_main.c
@@ -27,10 +27,15 @@ struct mutex psp_devs_lock;
  * psp_dev_check_access() - check if user in a given net ns can access PSP dev
  * @psd:	PSP device structure user is trying to access
  * @net:	net namespace user is in
+ * @admin:	If true, only allow access from @psd's main device's netns,
+ *		for admin operations like config changes and key rotation.
+ *		If false, also allow access from network namespaces that have
+ *		an associated device with @psd, for read-only and association
+ *		management operations.
  *
  * Return: 0 if PSP device should be visible in @net, errno otherwise.
  */
-int psp_dev_check_access(struct psp_dev *psd, struct net *net)
+int psp_dev_check_access(struct psp_dev *psd, struct net *net, bool admin)
 {
 	if (dev_net(psd->main_netdev) == net)
 		return 0;
diff --git a/net/psp/psp_nl.c b/net/psp/psp_nl.c
index 6afd7707ec12..eb47a9ee4438 100644
--- a/net/psp/psp_nl.c
+++ b/net/psp/psp_nl.c
@@ -41,7 +41,8 @@ static int psp_nl_reply_send(struct sk_buff *rsp, struct genl_info *info)
 /* Device stuff */
 
 static struct psp_dev *
-psp_device_get_and_lock(struct net *net, struct nlattr *dev_id)
+psp_device_get_and_lock(struct net *net, struct nlattr *dev_id,
+			bool admin)
 {
 	struct psp_dev *psd;
 	int err;
@@ -56,7 +57,7 @@ psp_device_get_and_lock(struct net *net, struct nlattr *dev_id)
 	mutex_lock(&psd->lock);
 	mutex_unlock(&psp_devs_lock);
 
-	err = psp_dev_check_access(psd, net);
+	err = psp_dev_check_access(psd, net, admin);
 	if (err) {
 		mutex_unlock(&psd->lock);
 		return ERR_PTR(err);
@@ -65,17 +66,31 @@ psp_device_get_and_lock(struct net *net, struct nlattr *dev_id)
 	return psd;
 }
 
-int psp_device_get_locked(const struct genl_split_ops *ops,
-			  struct sk_buff *skb, struct genl_info *info)
+static int __psp_device_get_locked(const struct genl_split_ops *ops,
+				   struct sk_buff *skb, struct genl_info *info,
+				   bool admin)
 {
 	if (GENL_REQ_ATTR_CHECK(info, PSP_A_DEV_ID))
 		return -EINVAL;
 
 	info->user_ptr[0] = psp_device_get_and_lock(genl_info_net(info),
-						    info->attrs[PSP_A_DEV_ID]);
+						    info->attrs[PSP_A_DEV_ID],
+						    admin);
 	return PTR_ERR_OR_ZERO(info->user_ptr[0]);
 }
 
+int psp_device_get_locked_admin(const struct genl_split_ops *ops,
+				struct sk_buff *skb, struct genl_info *info)
+{
+	return __psp_device_get_locked(ops, skb, info, true);
+}
+
+int psp_device_get_locked(const struct genl_split_ops *ops,
+			  struct sk_buff *skb, struct genl_info *info)
+{
+	return __psp_device_get_locked(ops, skb, info, false);
+}
+
 void
 psp_device_unlock(const struct genl_split_ops *ops, struct sk_buff *skb,
 		  struct genl_info *info)
@@ -160,7 +175,7 @@ static int
 psp_nl_dev_get_dumpit_one(struct sk_buff *rsp, struct netlink_callback *cb,
 			  struct psp_dev *psd)
 {
-	if (psp_dev_check_access(psd, sock_net(rsp->sk)))
+	if (psp_dev_check_access(psd, sock_net(rsp->sk), false))
 		return 0;
 
 	return psp_nl_dev_fill(psd, rsp, genl_info_dump(cb));
@@ -305,7 +320,7 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 
 	psd = psp_dev_get_for_sock(socket->sk);
 	if (psd) {
-		err = psp_dev_check_access(psd, genl_info_net(info));
+		err = psp_dev_check_access(psd, genl_info_net(info), false);
 		if (err) {
 			psp_dev_put(psd);
 			psd = NULL;
@@ -330,7 +345,7 @@ int psp_assoc_device_get_locked(const struct genl_split_ops *ops,
 
 		psp_dev_put(psd);
 	} else {
-		psd = psp_device_get_and_lock(genl_info_net(info), id);
+		psd = psp_device_get_and_lock(genl_info_net(info), id, false);
 		if (IS_ERR(psd)) {
 			err = PTR_ERR(psd);
 			goto err_sock_put;
@@ -573,7 +588,7 @@ static int
 psp_nl_stats_get_dumpit_one(struct sk_buff *rsp, struct netlink_callback *cb,
 			    struct psp_dev *psd)
 {
-	if (psp_dev_check_access(psd, sock_net(rsp->sk)))
+	if (psp_dev_check_access(psd, sock_net(rsp->sk), false))
 		return 0;
 
 	return psp_nl_stats_fill(psd, rsp, genl_info_dump(cb));
-- 
2.52.0


^ permalink raw reply related

* [PATCH v12 net-next 0/5] psp: Add support for dev-assoc/disassoc
From: Wei Wang @ 2026-05-01 17:55 UTC (permalink / raw)
  To: netdev, Jakub Kicinski, Daniel Zahka, Willem de Bruijn, David Wei,
	Andrew Lunn, David S . Miller, Eric Dumazet, Simon Horman,
	Paolo Abeni
  Cc: Wei Wang

From: Wei Wang <weibunny@fb.com>

The main purpose of this feature is to associate virtual devices like
veth or netkit with a real PSP device, so we could provide PSP
functionality to the application running with virtual devices.

A typical deployment that works with this feature is as follows:
     Host Namespace:
     psp_dev_local  ←──physically linked──→ psp_dev_peer
	  (PSP device)
	       │
	       │ BPF on psp_dev_local ingress: bpf_redirect_peer() to nk_guest
	       │
	  nk_host / veth_host
	       │
	       │ BPF on nk_host ingress: bpf_redirect_neigh() to psp_dev_local
	       │
      Guest Namespace (netns):
	       │
	  nk_guest / veth_guest
	  ★ PSP application run here

      Remote Namespace (_netns):
	  psp_dev_peer
	  ★ PSP server application runs here

Note:
The general requirement for this feature to work:
For PSP to work correctly, the egress device at validate_xmit_skb()
time must have psp_dev matching the association's psd. Any device
stacking or traffic redirection that changes the egress device will
cause either:
1. TX validation failure (SKB_DROP_REASON_PSP_OUTPUT) - fail-safe
2. RX policy failure after tx-assoc - packets without PSP extension
   are rejected by receiver expecting encrypted traffic

Here are a few examples that this feature would not work:
- Bonding with load balancing in round-robin, XOR, 802.3ad mode across
  multiple PSP devices, or mixed PSP and non-PSP devices
- Bonding with active-backup mode might work without PSP migration for
  failover case.
- ipvlan/macvlan in bridge mode would not work given packets are
  loopbacked locally without going through the PSP device.

Changes since v11:
- Cap max number of associated devs per psd to 128 to avoid overflowing
  GENLMSG_DEFAULT_SIZE for netlink msg in patch 2
- Make common function for getting net in psp_nl_dev_assoc_doit() and
  psp_nl_dev_disassoc_doit() in patch 2
- Only allow NSID to be passed in when user is in dev_net(psd->main_netdev)
  to avoid manipulation of dev-assoc/dev-disassoc from netns other than
  its own in patch 2
- Fixed the race between netdev_unregister() and psp_nl_dev_assoc_doit()
  by adding devreg status check in psp_nl_dev_assoc_doit().

Changes since v10:
- Corrected typo on patch 1
- Removed the kdoc style comments, Use goto style in
  psp_nl_dev_assoc_doit() clean up code, Resolved "TOCTOU" issue in
  psp_assoc_device_get_locked() in patch 2
- Replaced psp_devs_lock with a new mutex in
  psp_attach_netdev_notifier(), Fixed kdoc style comments in patch 3

Changes since v9:
- Added comments for psp_device_get_locked(), fixed lint issue, fixed
  rcu warning in patch 2
- Return error if register_netdevice_notifier() fails in
  psp_device_get_locked_dev_assoc() in patch 3
- Removed psp version and ip version for unnecessary tests cases in
  patch 5

Changes since v8:
- Rebase

Changes since v7:
- Refactor in patch 1 to have a common helper for
  psp_device_get_locked_admin() and psp_device_get_locked()
- Take psd->lock in psp_assoc_device_get_locked() before
  psp_dev_check_access() in patch 2
- Use cmpxchg() for assoc_dev->psp_dev assignment when doing dev-assoc
  in patch 2
- Check for err for register_netdevice_notifier() in patch 3
- Call psp_attach_netdev_notifier() in pre_doit handler for dev-assoc to
  avoid releasing of psd->lock in patch 3

Changes since v6:
- Remove the unused remote_addr, nk_guest_addr and import cmd in patch 5

Changes since v5:
- Remove module_exit() in patch 3

Changes since v4:
- Address compilation warning in patch 3
- Removed the call to psp_nl_has_listeners_any_ns() and check listeners
  when looping through netns in psp_nl_notify_dev() in patch 2. This
  makes sure we only send notification to netns that has listeners.

Changes since v3:
- Make nsid optional for dev-assoc/dev-disassoc operation, and use
  the ns user is in when it's not specified. Also added a test for this.
- Fix psp_nl_notify_dev() to compute the correct nsid relative to the
  listener's netns.
- Only register the new netdev event for psp dev cleanup upon the first
  successful dev-assoc operation.
- Change the following in selftest:
  - Add CONFIG_NETKIT to driver/net's config
  - Fall back to NetDrvEpEnv and run basic test cases if NetDrvContEnv
    does not load
  - Use ksft_variants instead of psp_ip_ver_test_builder

Changes since v2:
- Change the newly added parameter to psp_device_get_and_lock() to
  admin in patch 1. Introduce 2 device check functions:
  - psp_device_get_locked_admin() for dev-set and key-rotate
  - psp_device_get_locked() for all other operations
  Flip the logic for checking the dev_assoc_list accordingly in patch 2.
- Move psp_nl_notify_dev() before removing the dev from assoc_dev_list
  in psp_nl_dev_disassoc_doit() and correct the typo in commit msg in
  patch 2.
- Remove the threading and subprocess and some comment updates in patch 5. 

Changes since v1:
- Update the first 4 patches to reflect the latest changes in
  https://lore.kernel.org/netdev/20260302053315.1919859-1-dw@davidwei.uk/
- Update patch 9 to add a param to NetDrvContEnv to control the loading
  of the tx forwarding bpf program

Wei Wang (5):
  psp: add admin/non-admin version of psp_device_get_locked
  psp: add new netlink cmd for dev-assoc and dev-disassoc
  psp: add a new netdev event for dev unregister
  selftests/net: Add bpf skb forwarding program
  selftest/net: psp: Add test for dev-assoc/disassoc

 Documentation/netlink/specs/psp.yaml          |  71 ++-
 include/net/psp/types.h                       |  23 +
 include/uapi/linux/psp.h                      |  13 +
 net/psp/psp-nl-gen.c                          |  36 +-
 net/psp/psp-nl-gen.h                          |   7 +
 net/psp/psp.h                                 |   3 +-
 net/psp/psp_main.c                            | 104 +++-
 net/psp/psp_nl.c                              | 402 ++++++++++++++-
 tools/testing/selftests/drivers/net/config    |   1 +
 .../drivers/net/hw/nk_redirect.bpf.c          |  60 +++
 .../selftests/drivers/net/lib/py/env.py       |  54 ++-
 tools/testing/selftests/drivers/net/psp.py    | 457 ++++++++++++++++--
 12 files changed, 1170 insertions(+), 61 deletions(-)
 create mode 100644 tools/testing/selftests/drivers/net/hw/nk_redirect.bpf.c

-- 
2.52.0


^ permalink raw reply

* Re: [PATCH iproute2-next 0/5] netshaper: Extend netshaper support
From: David Ahern @ 2026-05-01 17:52 UTC (permalink / raw)
  To: Mohsin Bashir, netdev; +Cc: stephen, pabeni, kuba, ernis
In-Reply-To: <20260501011611.3533573-1-mohsin.bashr@gmail.com>

On 4/30/26 7:16 PM, Mohsin Bashir wrote:
> From: Mohsin Bashir <hmohsin@meta.com>
> 
> This series extends the netshaper CLI with missing parameter support
> and adds the group command for building scheduling hierarchies.
> 
> The existing netshaper tool only supports setting bw-max on individual
> shapers. This series adds the remaining shaper attributes (bw-min,
> weight, priority) needed for TX scheduling, and introduces the
> group command which ties leaf shapers to a parent node in a single
> operation.
> 
> Mohsin Bashir (5):
>   netshaper: Extract parse_scope() and parse_rate() helpers
>   netshaper: Add bw-min and weight parameter support
>   netshaper: Extend show output with parent, bw-min and weight
>   netshaper: Make handle id optional for node scope
>   netshaper: Add group command for creating scheduling hierarchies
> 
>  netshaper/netshaper.c | 398 ++++++++++++++++++++++++++++++++++--------
>  1 file changed, 324 insertions(+), 74 deletions(-)
> 

man page?

^ permalink raw reply

* Re: [PATCH iproute2-next 2/5] netshaper: Add bw-min and weight parameter support
From: David Ahern @ 2026-05-01 17:50 UTC (permalink / raw)
  To: Mohsin Bashir, netdev; +Cc: stephen, pabeni, kuba, ernis
In-Reply-To: <20260501011611.3533573-3-mohsin.bashr@gmail.com>

On 4/30/26 7:16 PM, Mohsin Bashir wrote:
> diff --git a/netshaper/netshaper.c b/netshaper/netshaper.c
> index 1603e6e5..4ee30525 100644
> --- a/netshaper/netshaper.c
> +++ b/netshaper/netshaper.c
> @@ -33,11 +33,12 @@ static void usage(void)
>  		"OPTIONS := { -V[ersion] | -c[olor] | -help }\n"
>  		"COMMAND := { set | get | delete } dev DEVNAME\n"
>  		"	    handle scope HANDLE_SCOPE [id HANDLE_ID]\n"
> -		"	    [bw-max BW_MAX]\n"
> +		"	    [bw-max BW_MAX] [bw-min BW_MIN] [weight WEIGHT]\n"

natural order is min then max

>  		"Where: DEVNAME         := STRING\n"
>  		"       HANDLE_SCOPE    := { netdev | queue | node }\n"
>  		"       HANDLE_ID       := UINT (required for queue/node, optional for netdev)\n"
> -		"       BW_MAX          := UINT{ kbit | mbit | gbit }\n");
> +		"       BW_MAX/BW_MIN   := UINT{ kbit | mbit | gbit }\n"
> +		"       WEIGHT          := UINT\n");
>  }
>  
>  static const char *net_shaper_scope_names[NET_SHAPER_SCOPE_MAX + 1] = {
> @@ -125,13 +126,13 @@ static int do_cmd(int argc, char **argv, int cmd)
>  	GENL_REQUEST(req, 1024, genl_family, 0, NET_SHAPER_FAMILY_VERSION, cmd,
>  		     NLM_F_REQUEST | NLM_F_ACK);
>  
> -	struct nlmsghdr *answer;
> -	__u64 bw_max_bps = 0;
> -	int ifindex = -1;
> +	bool has_bw_max = false, has_bw_min = false, has_weight = false;

can `bw_{max,min}_bps != 0` be used instead of a separate bool?
similarly for weight


^ permalink raw reply

* Re: [PATCH iproute2-next 1/5] netshaper: Extract parse_scope() and parse_rate() helpers
From: David Ahern @ 2026-05-01 17:47 UTC (permalink / raw)
  To: Mohsin Bashir, netdev; +Cc: stephen, pabeni, kuba, ernis
In-Reply-To: <20260501011611.3533573-2-mohsin.bashr@gmail.com>

On 4/30/26 7:16 PM, Mohsin Bashir wrote:
> diff --git a/netshaper/netshaper.c b/netshaper/netshaper.c
> index 47fb805e..1603e6e5 100644
> --- a/netshaper/netshaper.c
> +++ b/netshaper/netshaper.c
> @@ -47,6 +47,28 @@ static const char *net_shaper_scope_names[NET_SHAPER_SCOPE_MAX + 1] = {
>  	"node"
>  };
>  
> +static int parse_scope(const char *str)
> +{
> +	if (strcmp(str, "netdev") == 0)
> +		return NET_SHAPER_SCOPE_NETDEV;
> +	if (strcmp(str, "queue") == 0)
> +		return NET_SHAPER_SCOPE_QUEUE;
> +	if (strcmp(str, "node") == 0)
> +		return NET_SHAPER_SCOPE_NODE;
> +	return -1;

net_shaper_scope_names exists. Create 1 table used for both parsing and
printing.

> +}
> +
> +static int parse_rate(const char *str, __u64 *rate_bps)
> +{
> +	if (get_rate64(rate_bps, str)) {
> +		fprintf(stderr, "Invalid rate value \"%s\"\n", str);
> +		return -1;
> +	}
> +	/* get_rate64 returns bytes/sec, convert to bits/sec */
> +	*rate_bps *= 8;

no magic numbers. Add a define for BITS_PER_BYTE to include/utils.h
above BIT()

^ permalink raw reply

* Re: [PATCH net-next 08/12] dt-bindings: net: toshiba,tc965x-dwmac: add TC956x Ethernet bridge
From: Andrew Lunn @ 2026-05-01 17:38 UTC (permalink / raw)
  To: Alex Elder
  Cc: andrew+netdev, davem, edumazet, kuba, pabeni, maxime.chevallier,
	rmk+kernel, andersson, konradybcio, robh, krzk+dt, conor+dt,
	linusw, brgl, arnd, gregkh, Daniel Thompson, mohd.anwar,
	a0987203069, alexandre.torgue, ast, boon.khai.ng, chenchuangyu,
	chenhuacai, daniel, hawk, hkallweit1, inochiama, john.fastabend,
	julianbraha, livelycarpet87, matthew.gerlach, mcoquelin.stm32, me,
	prabhakar.mahadev-lad.rj, richardcochran, rohan.g.thomas, sdf,
	siyanteng, weishangjuan, wens, netdev, bpf, linux-arm-msm,
	devicetree, linux-gpio, linux-stm32, linux-arm-kernel,
	linux-kernel
In-Reply-To: <20260501155421.3329862-9-elder@riscstar.com>

Your ASCII art of the chip might be useful here as documentation.

> +  # We can't allOf reference Ethernet-controller.yaml because we end up with
> +  # contradictory $nodename rules (`ethernet@` versus `pci@`). Happily only a
> +  # small number of the properties are useful on TC956x so we can just reference
> +  # what we need.

Why not add an subnodes for the ethernet interfaces?

> +examples:
> +  - |
> +    pcie {
> +      #address-cells = <3>;
> +      #size-cells = <2>;
> +
> +      tc956x_emac0: pci@0,0 {
> +        compatible = "pci1179,0220";
> +        reg = <0x50000 0x0 0x0 0x0 0x0>;
> +        #address-cells = <3>;
> +        #size-cells = <2>;
> +        device_type = "pci";
> +        ranges;
> +
> +        gpio-controller;
> +        #gpio-cells = <2>;

I've not got to the GPIO driver patch yet...

Is the GPIO part of the ethernet device, or part of the chip? The
hierarchy here should match the hierarchy of the hardware.

> +        phy-mode = "10gbase-r";
> +        phy-handle = <&tc956x_emac0_phy>;
> +
> +        mdio {
> +          compatible = "snps,dwmac-mdio";
> +          #address-cells = <1>;
> +          #size-cells = <0>;
> +
> +          tc956x_emac0_phy: ethernet-phy@1c {
> +            compatible = "ethernet-phy-id311c.1c12";
> +            reg = <0x1c>;
> +          };
> +        };
> +      };
> +      pci@0,1 {
> +        compatible = "pci1179,0220";
> +        reg = <0x50100 0x0 0x0 0x0 0x0>;
> +        #address-cells = <3>;
> +        #size-cells = <2>;
> +        device_type = "pci";
> +        ranges;
> +

You second ethernet does not have a gpio controller?

	Andrew

^ permalink raw reply

* Re: [PATCH] amd-xgbe: fix PTP addend overflow causing frozen clock
From: Gregory Fuchedgi @ 2026-05-01 17:32 UTC (permalink / raw)
  To: Simon Horman
  Cc: Raju Rangoju, Prashanth Kumar K R, Andrew Lunn, David S. Miller,
	Eric Dumazet, Jakub Kicinski, Paolo Abeni, Richard Cochran,
	netdev, linux-kernel
In-Reply-To: <20260501153253.GJ15617@horms.kernel.org>

Thanks for the review, Simon.

I don't know what frequency gets returned for v1 in practice. Maybe we could
increase v1 SSINC if the ptpclk_rate can be low (<=50MHz), but I don't have
v1 hardware to experiment with.

On Fri, May 1, 2026 at 8:32 AM Simon Horman <horms@kernel.org> wrote:
>
> On Wed, Apr 29, 2026 at 02:54:14PM -0700, Gregory Fuchedgi via B4 Relay wrote:
> > From: Gregory Fuchedgi <gfuchedgi@gmail.com>
> >
> > XGBE_PTP_ACT_CLK_FREQ and XGBE_V2_PTP_ACT_CLK_FREQ were 10x too
> > large (500MHz/1GHz instead of 50MHz/100MHz), causing the computed
> > addend to overflow the 32-bit tstamp_addend. In the general case
> > this would result in the clock advancing at the wrong rate. For v2
> > (PCI), ptpclk_rate is hardcoded to 125MHz, so the addend formula
> > (ACT_CLK_FREQ << 32) / ptpclk_rate yields exactly 8 * 2^32, and
> > when stored to the 32-bit tstamp_addend the value is zero. With
> > addend = 0 the hardware accumulator never overflows and the PTP
> > clock is fully stopped. For v1 (platform), ptpclk_rate is read from
> > ACPI/DT so the exact overflow behavior depends on the
> > firmware-reported frequency.
> >
> > Define the constants as NSEC_PER_SEC / SSINC so the relationship is
> > explicit and cannot drift out of sync.
> >
> > Fixes: fbd47be098b5 ("amd-xgbe: add hardware PTP timestamping support")
> > Tested-by: Gregory Fuchedgi <gfuchedgi@gmail.com>
> > Signed-off-by: Gregory Fuchedgi <gfuchedgi@gmail.com>
>
> Reviewed-by: Simon Horman <horms@kernel.org>
>
> There is an AI generated review of this patch available on sashiko.dev.
> While I do believe the issues flagged there warrant investigation
> as possible follow-up, I do not think they should delay progress
> of this patch.

^ permalink raw reply

* Re: [PATCH net-next 2/3] vsock: convert to getsockopt_iter
From: Bobby Eshleman @ 2026-05-01 17:32 UTC (permalink / raw)
  To: Breno Leitao
  Cc: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
	Simon Horman, Stefano Garzarella, Shuah Khan, sdf.kernel, netdev,
	linux-kernel, virtualization, linux-kselftest, kernel-team
In-Reply-To: <20260501-getsock_one-v1-2-810ce23ea70e@debian.org>

On Fri, May 01, 2026 at 08:52:52AM -0700, Breno Leitao wrote:
> Convert AF_VSOCK's getsockopt implementation to use the new
> getsockopt_iter callback with sockopt_t. The single
> vsock_connectible_getsockopt() callback is shared by both
> vsock_stream_ops and vsock_seqpacket_ops, so both proto_ops are
> updated to use .getsockopt_iter.
> 
> Key changes:
> - Replace (char __user *optval, int __user *optlen) with sockopt_t *opt
> - Use opt->optlen for buffer length (input) and returned size (output)
> - Use copy_to_iter() instead of put_user()/copy_to_user()
> 
> Signed-off-by: Breno Leitao <leitao@debian.org>
> ---
>  net/vmw_vsock/af_vsock.c | 16 +++++++---------
>  1 file changed, 7 insertions(+), 9 deletions(-)
> 
> diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c
> index 44037b066a5ff..d4a97eeb596e6 100644
> --- a/net/vmw_vsock/af_vsock.c
> +++ b/net/vmw_vsock/af_vsock.c
> @@ -155,6 +155,7 @@
>  #include <linux/random.h>
>  #include <linux/skbuff.h>
>  #include <linux/smp.h>
> +#include <linux/uio.h>
>  #include <linux/socket.h>
>  #include <linux/stddef.h>
>  #include <linux/sysctl.h>
> @@ -2091,8 +2092,7 @@ static int vsock_connectible_setsockopt(struct socket *sock,
>  
>  static int vsock_connectible_getsockopt(struct socket *sock,
>  					int level, int optname,
> -					char __user *optval,
> -					int __user *optlen)
> +					sockopt_t *opt)
>  {
>  	struct sock *sk = sock->sk;
>  	struct vsock_sock *vsk = vsock_sk(sk);
> @@ -2110,8 +2110,7 @@ static int vsock_connectible_getsockopt(struct socket *sock,
>  	if (level != AF_VSOCK)
>  		return -ENOPROTOOPT;
>  
> -	if (get_user(len, optlen))
> -		return -EFAULT;
> +	len = opt->optlen;
>  
>  	memset(&v, 0, sizeof(v));
>  
> @@ -2142,11 +2141,10 @@ static int vsock_connectible_getsockopt(struct socket *sock,
>  		return -EINVAL;
>  	if (len > lv)
>  		len = lv;
> -	if (copy_to_user(optval, &v, len))
> +	if (copy_to_iter(&v, len, &opt->iter_out) != len)
>  		return -EFAULT;
>  
> -	if (put_user(len, optlen))
> -		return -EFAULT;
> +	opt->optlen = len;
>  
>  	return 0;
>  }
> @@ -2631,7 +2629,7 @@ static const struct proto_ops vsock_stream_ops = {
>  	.listen = vsock_listen,
>  	.shutdown = vsock_shutdown,
>  	.setsockopt = vsock_connectible_setsockopt,
> -	.getsockopt = vsock_connectible_getsockopt,
> +	.getsockopt_iter = vsock_connectible_getsockopt,
>  	.sendmsg = vsock_connectible_sendmsg,
>  	.recvmsg = vsock_connectible_recvmsg,
>  	.mmap = sock_no_mmap,
> @@ -2653,7 +2651,7 @@ static const struct proto_ops vsock_seqpacket_ops = {
>  	.listen = vsock_listen,
>  	.shutdown = vsock_shutdown,
>  	.setsockopt = vsock_connectible_setsockopt,
> -	.getsockopt = vsock_connectible_getsockopt,
> +	.getsockopt_iter = vsock_connectible_getsockopt,
>  	.sendmsg = vsock_connectible_sendmsg,
>  	.recvmsg = vsock_connectible_recvmsg,
>  	.mmap = sock_no_mmap,
> 
> -- 
> 2.52.0
> 

Reviewed-by: Bobby Eshleman <bobbyeshleman@meta.com>

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox