[PATCH net-next 04/11] ethtool: Add CMIS loopback helpers for module loopback control
From: Björn Töpel <bjorn@kernel.org>
Date: 2026-03-10 10:48:17
Also in:
linux-kselftest, linux-rdma, lkml
Subsystem:
networking [ethtool], networking [general], the rest · Maintainers:
Andrew Lunn, Jakub Kicinski, "David S. Miller", Eric Dumazet, Paolo Abeni, Linus Torvalds
Add CMIS loopback functions and wire them into loopback.c for the
MODULE component:
- ethtool_cmis_get_loopback(): reads Page 13h capabilities and
current state, appends one entry per supported loopback point
("cmis-host" and/or "cmis-media").
- ethtool_cmis_get_loopback_by_index(): used to enumerate what the
module supports.
- ethtool_cmis_set_loopback(): resolves name to a pair of control
byte indices, validates direction, and writes the Page 13h control
bytes (0xff = all lanes on, 0x00 = off).
Directions are mutually exclusive: switching from near-end to far-end
first disables the active direction in a separate EEPROM write, then
enables the new one. Requesting multiple direction flags is rejected.
CMIS register mapping (Page 13h, Bytes 180-183):
- MODULE, "cmis-host", near-end -> Host Side Input (Byte 183)
- MODULE, "cmis-host", far-end -> Host Side Output (Byte 182)
- MODULE, "cmis-media", near-end -> Media Side Input (Byte 181)
- MODULE, "cmis-media", far-end -> Media Side Output (Byte 180)
The helpers work entirely over get/set_module_eeprom_by_page, so any
driver with EEPROM page access gets module loopback without new
ethtool_ops or driver changes. SET is rejected when firmware flashing
is in progress or the interface is UP.
Signed-off-by: Björn Töpel <bjorn@kernel.org>
---
net/ethtool/Makefile | 2 +-
net/ethtool/cmis_loopback.c | 407 ++++++++++++++++++++++++++++++++++++
net/ethtool/loopback.c | 6 +-
net/ethtool/netlink.h | 8 +
4 files changed, 421 insertions(+), 2 deletions(-)
create mode 100644 net/ethtool/cmis_loopback.c
diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile
index ef534b55d724..2f821c7875e1 100644
--- a/net/ethtool/Makefile
+++ b/net/ethtool/Makefile@@ -9,4 +9,4 @@ ethtool_nl-y := netlink.o bitset.o strset.o linkinfo.o linkmodes.o rss.o \ channels.o coalesce.o pause.o eee.o tsinfo.o cabletest.o \ tunnels.o fec.o eeprom.o stats.o phc_vclocks.o mm.o \ module.o cmis_fw_update.o cmis_cdb.o pse-pd.o plca.o \ - phy.o tsconfig.o mse.o loopback.o + phy.o tsconfig.o mse.o loopback.o cmis_loopback.o
diff --git a/net/ethtool/cmis_loopback.c b/net/ethtool/cmis_loopback.c
new file mode 100644
index 000000000000..0d419c369d64
--- /dev/null
+++ b/net/ethtool/cmis_loopback.c@@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-2.0-only + +/* CMIS loopback helpers for drivers implementing ethtool + * get/set_loopback. + * + * Maps the generic ethtool loopback model to CMIS Page 13h registers + * (CMIS 5.3, Table 8-128). + * + * Capabilities are read from Page 13h Byte 128, with Page 13h + * availability checked via Page 01h Byte 142 bit 5. + */ + +#include <linux/ethtool.h> +#include <linux/sfp.h> + +#include "common.h" +#include "module_fw.h" +#include "cmis.h" + +/* CMIS Page 00h, Byte 0: Physical module identifier */ +#define CMIS_PHYS_ID_PAGE 0x00 +#define CMIS_PHYS_ID_OFFSET 0x00 + +/* CMIS Page 01h, Byte 142: Diagnostic Pages Support */ +#define CMIS_DIAG_SUPPORT_PAGE 0x01 +#define CMIS_DIAG_SUPPORT_OFFSET 0x8e +#define CMIS_DIAG_PAGE13_BIT BIT(5) + +/* CMIS Page 13h, Byte 128: Loopback Capability Advertisement */ +#define CMIS_LB_CAPS_PAGE 0x13 +#define CMIS_LB_CAPS_OFFSET 0x80 +#define CMIS_LB_CAP_MEDIA_OUTPUT BIT(0) +#define CMIS_LB_CAP_MEDIA_INPUT BIT(1) +#define CMIS_LB_CAP_HOST_OUTPUT BIT(2) +#define CMIS_LB_CAP_HOST_INPUT BIT(3) + +/* CMIS Page 13h, Bytes 180-183: Per-Lane Loopback Control + * Byte 180 (0xb4): Media Side Output -> MODULE, "cmis-media", far-end + * Byte 181 (0xb5): Media Side Input -> MODULE, "cmis-media", near-end + * Byte 182 (0xb6): Host Side Output -> MODULE, "cmis-host", far-end + * Byte 183 (0xb7): Host Side Input -> MODULE, "cmis-host", near-end + */ +#define CMIS_LB_CTRL_PAGE 0x13 +#define CMIS_LB_CTRL_OFFSET 0xb4 +#define CMIS_LB_CTRL_LEN 4 +#define CMIS_LB_CTRL_IDX_MEDIA_OUTPUT 0 +#define CMIS_LB_CTRL_IDX_MEDIA_INPUT 1 +#define CMIS_LB_CTRL_IDX_HOST_OUTPUT 2 +#define CMIS_LB_CTRL_IDX_HOST_INPUT 3 + +#define CMIS_LB_NAME_HOST "cmis-host" +#define CMIS_LB_NAME_MEDIA "cmis-media" + +static bool cmis_is_module(u8 phys_id) +{ + switch (phys_id) { + case SFF8024_ID_QSFP_DD: + case SFF8024_ID_OSFP: + case SFF8024_ID_DSFP: + case SFF8024_ID_QSFP_PLUS_CMIS: + case SFF8024_ID_SFP_DD_CMIS: + case SFF8024_ID_SFP_PLUS_CMIS: + return true; + default: + return false; + } +} + +/** + * cmis_loopback_caps - Read CMIS loopback capability mask + * @dev: Network device + * + * Return: >0 capability bitmask, 0 if not a CMIS module or no Page + * 13h, negative errno on failure. + */ +static int cmis_loopback_caps(struct net_device *dev) +{ + const struct ethtool_ops *ops = dev->ethtool_ops; + struct ethtool_module_eeprom page = {}; + int ret; + u8 val; + + if (!ops->get_module_eeprom_by_page) + return 0; + + /* Read physical identifier */ + ethtool_cmis_page_init(&page, CMIS_PHYS_ID_PAGE, + CMIS_PHYS_ID_OFFSET, sizeof(val)); + page.data = &val; + ret = ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + if (!cmis_is_module(val)) + return 0; + + /* Check Page 13h availability */ + ethtool_cmis_page_init(&page, CMIS_DIAG_SUPPORT_PAGE, + CMIS_DIAG_SUPPORT_OFFSET, sizeof(val)); + page.data = &val; + ret = ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + if (!(val & CMIS_DIAG_PAGE13_BIT)) + return 0; + + /* Read capability byte */ + ethtool_cmis_page_init(&page, CMIS_LB_CAPS_PAGE, + CMIS_LB_CAPS_OFFSET, sizeof(val)); + page.data = &val; + ret = ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + + return val & (CMIS_LB_CAP_MEDIA_OUTPUT | CMIS_LB_CAP_MEDIA_INPUT | + CMIS_LB_CAP_HOST_OUTPUT | CMIS_LB_CAP_HOST_INPUT); +} + +/** + * cmis_loopback_read - Read CMIS loopback capabilities and build entries + * @dev: Network device with get_module_eeprom_by_page support + * @host: Output host loopback entry (populated if host caps exist) + * @media: Output media loopback entry (populated if media caps exist) + * @has_host: Set to true if host loopback is supported + * @has_media: Set to true if media loopback is supported + * + * Common helper that reads CMIS caps and control bytes, then populates + * the host and media entries with current state. + * + * Return: 0 on success, -EOPNOTSUPP if no CMIS loopback support, + * negative errno on failure. + */ +static int cmis_loopback_read(struct net_device *dev, + struct ethtool_loopback_entry *host, + struct ethtool_loopback_entry *media, + bool *has_host, bool *has_media) +{ + const struct ethtool_ops *ops = dev->ethtool_ops; + struct ethtool_module_eeprom page = {}; + u8 ctrl[CMIS_LB_CTRL_LEN]; + int caps, ret; + + *has_host = false; + *has_media = false; + + if (dev->ethtool->module_fw_flash_in_progress) + return -EBUSY; + + caps = cmis_loopback_caps(dev); + if (caps <= 0) + return caps ? caps : -EOPNOTSUPP; + + ethtool_cmis_page_init(&page, CMIS_LB_CTRL_PAGE, + CMIS_LB_CTRL_OFFSET, sizeof(ctrl)); + page.data = ctrl; + ret = ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + + memset(host, 0, sizeof(*host)); + host->component = ETHTOOL_LOOPBACK_COMPONENT_MODULE; + strscpy(host->name, CMIS_LB_NAME_HOST, sizeof(host->name)); + + memset(media, 0, sizeof(*media)); + media->component = ETHTOOL_LOOPBACK_COMPONENT_MODULE; + strscpy(media->name, CMIS_LB_NAME_MEDIA, sizeof(media->name)); + + if (caps & CMIS_LB_CAP_HOST_INPUT) { + *has_host = true; + host->supported |= ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_HOST_INPUT]) + host->direction |= ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + } + if (caps & CMIS_LB_CAP_HOST_OUTPUT) { + *has_host = true; + host->supported |= ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_HOST_OUTPUT]) + host->direction |= ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + } + if (caps & CMIS_LB_CAP_MEDIA_INPUT) { + *has_media = true; + media->supported |= ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_MEDIA_INPUT]) + media->direction |= ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + } + if (caps & CMIS_LB_CAP_MEDIA_OUTPUT) { + *has_media = true; + media->supported |= ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_MEDIA_OUTPUT]) + media->direction |= ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + } + + return 0; +} + +/** + * ethtool_cmis_get_loopback_by_index - Enumerate CMIS loopback entry by index + * @dev: Network device with get_module_eeprom_by_page support + * @index: Zero-based index of the loopback entry to retrieve + * @entry: Output loopback entry + * + * Used by the dump infrastructure to iterate one entry at a time. + * + * Return: 0 on success, -EOPNOTSUPP if the index is out of range or + * no CMIS loopback support, negative errno on failure. + */ +int ethtool_cmis_get_loopback_by_index(struct net_device *dev, u32 index, + struct ethtool_loopback_entry *entry) +{ + struct ethtool_loopback_entry host, media; + bool has_host, has_media; + u32 cur = 0; + int ret; + + ret = cmis_loopback_read(dev, &host, &media, &has_host, &has_media); + if (ret) + return ret; + + if (has_host) { + if (cur == index) { + memcpy(entry, &host, sizeof(*entry)); + return 0; + } + cur++; + } + + if (has_media) { + if (cur == index) { + memcpy(entry, &media, sizeof(*entry)); + return 0; + } + } + + return -EOPNOTSUPP; +} + +/** + * ethtool_cmis_get_loopback - Look up CMIS loopback entry by name + * @dev: Network device with get_module_eeprom_by_page support + * @name: Loopback point name ("cmis-host" or "cmis-media") + * @entry: Output loopback entry + * + * Used by doit requests to look up a specific loopback point. + * + * Return: 0 on success, -EOPNOTSUPP if name doesn't match or no CMIS + * support, negative errno on failure. + */ +int ethtool_cmis_get_loopback(struct net_device *dev, + const char *name, + struct ethtool_loopback_entry *entry) +{ + struct ethtool_loopback_entry host, media; + bool has_host, has_media; + int ret; + + ret = cmis_loopback_read(dev, &host, &media, &has_host, &has_media); + if (ret) + return ret; + + if (has_host && !strcmp(name, CMIS_LB_NAME_HOST)) { + memcpy(entry, &host, sizeof(*entry)); + return 0; + } + + if (has_media && !strcmp(name, CMIS_LB_NAME_MEDIA)) { + memcpy(entry, &media, sizeof(*entry)); + return 0; + } + + return -EOPNOTSUPP; +} + +/** + * ethtool_cmis_set_loopback - Apply one MODULE loopback entry to CMIS + * @dev: Network device with get/set_module_eeprom_by_page support + * @entry: Loopback entry to apply (must be MODULE component) + * @extack: Netlink extended ack for error reporting + * + * Matches the entry against CMIS loopback points by name and + * direction, then reads, modifies, and writes the corresponding Page + * 13h control byte (0xff for all-lanes enable, 0x00 for disable). + * + * When disabling (direction == 0), all loopback points matching the + * name are disabled regardless of their direction. When enabling, + * only the specific direction is activated. + * + * Return: 1 if hardware state changed, 0 if already in requested state, + * negative errno on failure. + */ +int ethtool_cmis_set_loopback(struct net_device *dev, + const struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack) +{ + struct ethtool_module_eeprom page = {}; + u8 ctrl[CMIS_LB_CTRL_LEN]; + int near_idx, far_idx; + u8 near_cap, far_cap; + bool mod = false; + int caps, ret; + + if (!dev->ethtool_ops->set_module_eeprom_by_page) { + NL_SET_ERR_MSG(extack, + "Module EEPROM write access not supported"); + return -EOPNOTSUPP; + } + + if (dev->ethtool->module_fw_flash_in_progress) { + NL_SET_ERR_MSG(extack, + "Module firmware flashing is in progress"); + return -EBUSY; + } + + if (dev->flags & IFF_UP) { + NL_SET_ERR_MSG(extack, + "Netdevice is up, module loopback change not permitted"); + return -EBUSY; + } + + if (entry->direction && !is_power_of_2(entry->direction)) { + NL_SET_ERR_MSG(extack, + "Only one loopback direction may be enabled at a time"); + return -EINVAL; + } + + if (!strcmp(entry->name, CMIS_LB_NAME_HOST)) { + near_idx = CMIS_LB_CTRL_IDX_HOST_INPUT; + far_idx = CMIS_LB_CTRL_IDX_HOST_OUTPUT; + near_cap = CMIS_LB_CAP_HOST_INPUT; + far_cap = CMIS_LB_CAP_HOST_OUTPUT; + } else if (!strcmp(entry->name, CMIS_LB_NAME_MEDIA)) { + near_idx = CMIS_LB_CTRL_IDX_MEDIA_INPUT; + far_idx = CMIS_LB_CTRL_IDX_MEDIA_OUTPUT; + near_cap = CMIS_LB_CAP_MEDIA_INPUT; + far_cap = CMIS_LB_CAP_MEDIA_OUTPUT; + } else { + NL_SET_ERR_MSG(extack, "Unknown CMIS loopback name"); + return -EINVAL; + } + + caps = cmis_loopback_caps(dev); + if (caps < 0) + return caps; + if (!caps) { + NL_SET_ERR_MSG(extack, "Module does not support CMIS loopback"); + return -EOPNOTSUPP; + } + + /* Read current control bytes */ + ethtool_cmis_page_init(&page, CMIS_LB_CTRL_PAGE, + CMIS_LB_CTRL_OFFSET, sizeof(ctrl)); + page.data = ctrl; + ret = dev->ethtool_ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + + if (!entry->direction) { + /* Disable both directions */ + if (ctrl[near_idx]) { + ctrl[near_idx] = 0x00; + mod = true; + } + if (ctrl[far_idx]) { + ctrl[far_idx] = 0x00; + mod = true; + } + } else { + int enable_idx, disable_idx; + u8 enable_cap; + + if (entry->direction & ETHTOOL_LOOPBACK_DIRECTION_NEAR_END) { + enable_idx = near_idx; + enable_cap = near_cap; + disable_idx = far_idx; + } else { + enable_idx = far_idx; + enable_cap = far_cap; + disable_idx = near_idx; + } + + if (!(caps & enable_cap)) { + NL_SET_ERR_MSG(extack, + "Loopback mode not supported by module"); + return -EOPNOTSUPP; + } + + /* Disable opposite direction first (mutual exclusivity) */ + if (ctrl[disable_idx]) { + ctrl[disable_idx] = 0x00; + ret = dev->ethtool_ops->set_module_eeprom_by_page(dev, + &page, extack); + if (ret < 0) + return ret; + mod = true; + } + + if (ctrl[enable_idx] != 0xff) { + ctrl[enable_idx] = 0xff; + mod = true; + } + } + + if (!mod) + return 0; + + ret = dev->ethtool_ops->set_module_eeprom_by_page(dev, &page, extack); + + return ret < 0 ? ret : 1; +}
diff --git a/net/ethtool/loopback.c b/net/ethtool/loopback.c
index 8907dd147404..ed184f45322e 100644
--- a/net/ethtool/loopback.c
+++ b/net/ethtool/loopback.c@@ -86,6 +86,8 @@ static int loopback_get(struct net_device *dev, struct ethtool_loopback_entry *entry) { switch (component) { + case ETHTOOL_LOOPBACK_COMPONENT_MODULE: + return ethtool_cmis_get_loopback(dev, name, entry); default: return -EOPNOTSUPP; }
@@ -94,7 +96,7 @@ static int loopback_get(struct net_device *dev, static int loopback_get_by_index(struct net_device *dev, u32 index, struct ethtool_loopback_entry *entry) { - return -EOPNOTSUPP; + return ethtool_cmis_get_loopback_by_index(dev, index, entry); } static int loopback_prepare_data(const struct ethnl_req_info *req_base,
@@ -223,6 +225,8 @@ static int __loopback_set(struct net_device *dev, struct netlink_ext_ack *extack) { switch (entry->component) { + case ETHTOOL_LOOPBACK_COMPONENT_MODULE: + return ethtool_cmis_set_loopback(dev, entry, extack); default: return -EOPNOTSUPP; }
diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h
index 2320760f931e..5aec1b73da7a 100644
--- a/net/ethtool/netlink.h
+++ b/net/ethtool/netlink.h@@ -551,6 +551,14 @@ int ethnl_perphy_dump_one_dev(struct sk_buff *skb, struct ethnl_dump_ctx *ctx, unsigned long *pos_sub, const struct genl_info *info); +int ethtool_cmis_get_loopback_by_index(struct net_device *dev, u32 index, + struct ethtool_loopback_entry *entry); +int ethtool_cmis_get_loopback(struct net_device *dev, + const char *name, + struct ethtool_loopback_entry *entry); +int ethtool_cmis_set_loopback(struct net_device *dev, + const struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack); extern const char stats_std_names[__ETHTOOL_STATS_CNT][ETH_GSTRING_LEN]; extern const char stats_eth_phy_names[__ETHTOOL_A_STATS_ETH_PHY_CNT][ETH_GSTRING_LEN];
--
2.53.0