From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AF8DC33D50A; Sun, 8 Mar 2026 12:40:28 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973628; cv=none; b=olbt91niXYADIgqexG1niobYE5VbyyznzkiNpWd5JztHL8NQhhE3KSZPNGKuJlOm/n5bXOyvlvyAVZqGHNIP9gXGY6HSJDgDdJyD4Yx/z/sINe7cc3HJPzzsPIxfnvgk3UIeIOOm8FW3PdLpckMuE+woIljHkM6Wt6+N6D4QSIs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973628; c=relaxed/simple; bh=FR6NpueWudgTakaY7rbuQAm3x59L7+NYp2I+aLCk5/M=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=DbFx1lgk3zo5SvNHZQGzME3XbeppcF2av7YIBVZKGPFB7ai8N/6o77vYkT3qiUXzyplXoBF5uAd0KJGQr9EkdOqFkL1liU9stQCc+7TMn7hrtMvUMMbdFNpX9LV7zd0EwSmABf+4N4l2bknnl4ajgqvtb+ShlpYx3Iwq9i92Wb4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=R81jSO5o; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="R81jSO5o" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 3D57BC116C6; Sun, 8 Mar 2026 12:40:24 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973628; bh=FR6NpueWudgTakaY7rbuQAm3x59L7+NYp2I+aLCk5/M=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=R81jSO5ovA4ne6310LCU/3LsLwSrwelKuascupdv/kEu0G7J4l0NDabw/4sXBtNPj Txar7JxETO8SQqaeKbSgEwWwHY1kyDSJZvDJ7C+mmB5LQ7uOWrUOjHCYZMNl83h6Ie LSWyFYJ6ZNtL91XuFQ07Ir6xua6QkJCmMvd00nwC/01rFdgsl2Z67axA9UPydXdd7L leWfBxfxvnYypqGXGawdCbR4aQ5/lpddDiz8u6nloQgWAApqiNb8cmI16gqcCG3pi5 V+wmVPPtX/BHFVbpBAsi/DPe42R8X0j8B5QYTgD2E0CA+2zX5fE2cPgaE5PbGbgZLS bRnnbmuPd9BNg== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 1/6] ethtool: Add loopback netlink UAPI definitions Date: Sun, 8 Mar 2026 13:40:07 +0100 Message-ID: <20260308124016.3134012-2-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add the netlink YAML spec and auto-generated UAPI header for a unified loopback interface covering MAC, PCS, PHY, and pluggable module components. Each loopback point is described by a nested entry attribute containing: - component where in the path (MAC, PCS, PHY, MODULE) - name subsystem label, e.g. "cmis-host" or "cmis-media" - id optional instance selector (e.g. PHY id, port id) - supported bitmask of supported directions - direction NEAR_END, FAR_END, or 0 (disabled) Signed-off-by: Bj=C3=B6rn T=C3=B6pel --- Documentation/netlink/specs/ethtool.yaml | 115 ++++++++++++++++++ .../uapi/linux/ethtool_netlink_generated.h | 52 ++++++++ 2 files changed, 167 insertions(+) diff --git a/Documentation/netlink/specs/ethtool.yaml b/Documentation/netli= nk/specs/ethtool.yaml index 4707063af3b4..05ebad6ae4e0 100644 --- a/Documentation/netlink/specs/ethtool.yaml +++ b/Documentation/netlink/specs/ethtool.yaml @@ -211,6 +211,39 @@ definitions: name: discard value: 31 =20 + - + name: loopback-component + type: enum + doc: | + Loopback component. Identifies where in the network path the + loopback is applied. + entries: + - + name: mac + doc: MAC loopback + - + name: pcs + doc: PCS loopback + - + name: phy + doc: PHY loopback + - + name: module + doc: Pluggable module (e.g. CMIS (Q)SFP) loopback + - + name: loopback-direction + type: flags + doc: | + Loopback direction flags. Used as a bitmask in supported, and as + a single value in direction. + entries: + - + name: near-end + doc: Near-end loopback; host-loop-host + - + name: far-end + doc: Far-end loopback; line-loop-line + attribute-sets: - name: header @@ -1903,6 +1936,60 @@ attribute-sets: name: link type: nest nested-attributes: mse-snapshot + - + name: loopback-entry + doc: Per-component loopback configuration entry. + attr-cnt-name: __ethtool-a-loopback-entry-cnt + attributes: + - + name: unspec + type: unused + value: 0 + - + name: component + type: u32 + enum: loopback-component + doc: Loopback component + - + name: id + type: u32 + doc: | + Optional component instance identifier. Required for PHY, + optional for MODULE, omitted for MAC and PCS. + - + name: name + type: string + doc: | + Subsystem-specific name for the loopback point within the + component. + - + name: supported + type: u32 + enum: loopback-direction + enum-as-flags: true + doc: Bitmask of supported loopback directions + - + name: direction + type: u32 + enum: loopback-direction + doc: Current loopback direction, 0 means disabled + - + name: loopback + attr-cnt-name: __ethtool-a-loopback-cnt + attributes: + - + name: unspec + type: unused + value: 0 + - + name: header + type: nest + nested-attributes: header + - + name: entry + type: nest + multi-attr: true + nested-attributes: loopback-entry =20 operations: enum-model: directional @@ -2855,6 +2942,34 @@ operations: - worst-channel - link dump: *mse-get-op + - + name: loopback-get + doc: Get loopback configuration and capabilities. + + attribute-set: loopback + + do: &loopback-get-op + request: + attributes: + - header + reply: + attributes: &loopback + - header + - entry + dump: *loopback-get-op + - + name: loopback-set + doc: Set loopback configuration. + + attribute-set: loopback + + do: + request: + attributes: *loopback + - + name: loopback-ntf + doc: Notification for change in loopback configuration. + notify: loopback-get =20 mcast-groups: list: diff --git a/include/uapi/linux/ethtool_netlink_generated.h b/include/uapi/= linux/ethtool_netlink_generated.h index 114b83017297..83fb17dd5daf 100644 --- a/include/uapi/linux/ethtool_netlink_generated.h +++ b/include/uapi/linux/ethtool_netlink_generated.h @@ -78,6 +78,33 @@ enum ethtool_pse_event { ETHTOOL_PSE_EVENT_SW_PW_CONTROL_ERROR =3D 64, }; =20 +/** + * enum ethtool_loopback_component - Loopback component. Identifies where = in + * the network path the loopback is applied. + * @ETHTOOL_LOOPBACK_COMPONENT_MAC: MAC loopback + * @ETHTOOL_LOOPBACK_COMPONENT_PCS: PCS loopback + * @ETHTOOL_LOOPBACK_COMPONENT_PHY: PHY loopback + * @ETHTOOL_LOOPBACK_COMPONENT_MODULE: Pluggable module (e.g. CMIS (Q)SFP) + * loopback + */ +enum ethtool_loopback_component { + ETHTOOL_LOOPBACK_COMPONENT_MAC, + ETHTOOL_LOOPBACK_COMPONENT_PCS, + ETHTOOL_LOOPBACK_COMPONENT_PHY, + ETHTOOL_LOOPBACK_COMPONENT_MODULE, +}; + +/** + * enum ethtool_loopback_direction - Loopback direction flags. Used as a + * bitmask in supported, and as a single value in direction. + * @ETHTOOL_LOOPBACK_DIRECTION_NEAR_END: Near-end loopback; host-loop-host + * @ETHTOOL_LOOPBACK_DIRECTION_FAR_END: Far-end loopback; line-loop-line + */ +enum ethtool_loopback_direction { + ETHTOOL_LOOPBACK_DIRECTION_NEAR_END =3D 1, + ETHTOOL_LOOPBACK_DIRECTION_FAR_END =3D 2, +}; + enum { ETHTOOL_A_HEADER_UNSPEC, ETHTOOL_A_HEADER_DEV_INDEX, @@ -838,6 +865,27 @@ enum { ETHTOOL_A_MSE_MAX =3D (__ETHTOOL_A_MSE_CNT - 1) }; =20 +enum { + ETHTOOL_A_LOOPBACK_ENTRY_UNSPEC, + ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT, + ETHTOOL_A_LOOPBACK_ENTRY_ID, + ETHTOOL_A_LOOPBACK_ENTRY_NAME, + ETHTOOL_A_LOOPBACK_ENTRY_SUPPORTED, + ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION, + + __ETHTOOL_A_LOOPBACK_ENTRY_CNT, + ETHTOOL_A_LOOPBACK_ENTRY_MAX =3D (__ETHTOOL_A_LOOPBACK_ENTRY_CNT - 1) +}; + +enum { + ETHTOOL_A_LOOPBACK_UNSPEC, + ETHTOOL_A_LOOPBACK_HEADER, + ETHTOOL_A_LOOPBACK_ENTRY, + + __ETHTOOL_A_LOOPBACK_CNT, + ETHTOOL_A_LOOPBACK_MAX =3D (__ETHTOOL_A_LOOPBACK_CNT - 1) +}; + enum { ETHTOOL_MSG_USER_NONE =3D 0, ETHTOOL_MSG_STRSET_GET =3D 1, @@ -891,6 +939,8 @@ enum { ETHTOOL_MSG_RSS_CREATE_ACT, ETHTOOL_MSG_RSS_DELETE_ACT, ETHTOOL_MSG_MSE_GET, + ETHTOOL_MSG_LOOPBACK_GET, + ETHTOOL_MSG_LOOPBACK_SET, =20 __ETHTOOL_MSG_USER_CNT, ETHTOOL_MSG_USER_MAX =3D (__ETHTOOL_MSG_USER_CNT - 1) @@ -952,6 +1002,8 @@ enum { ETHTOOL_MSG_RSS_CREATE_NTF, ETHTOOL_MSG_RSS_DELETE_NTF, ETHTOOL_MSG_MSE_GET_REPLY, + ETHTOOL_MSG_LOOPBACK_GET_REPLY, + ETHTOOL_MSG_LOOPBACK_NTF, =20 __ETHTOOL_MSG_KERNEL_CNT, ETHTOOL_MSG_KERNEL_MAX =3D (__ETHTOOL_MSG_KERNEL_CNT - 1) --=20 2.53.0 From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E334C33D50A; Sun, 8 Mar 2026 12:40:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973634; cv=none; b=Ygk11H0gDNDtFuDU28eNOV5ALSsCxoyh3Y/Gx53zT2JaXqBW8akEEYQ3CRuh3QmyuE1RzVkUgqlJXjXrgJ2w5aAKL10JP1wS/3RPZDpsY9B40ZVLu0jy36GFmH4Xeflh70t1PSRb5dqYBIBYpKJ5peCi8FJil5LUDUb5VHhpwXI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973634; c=relaxed/simple; bh=QZ6V/K2U5F+/TrGWi0L8Ef0LSlOYY9J/mYNVvgnlP60=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=L38Ckzgpw0MggKZ5XLhotseZjc+rWb4Z1wMIgrjnXHNpbWU9w+yC0fp2F7YZSeJH1IgTNJHZOlXZNv5bShV/8NrG6CHG9fnsJCKxQS9ns1rNFwU9euBJgTgTbveXrUTIOaTRS7eEb7dVWKMOacsGuQo7ycpQ6ASS2fPdOt+noUM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=qkF6dkq1; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="qkF6dkq1" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 0EF82C19423; Sun, 8 Mar 2026 12:40:28 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973633; bh=QZ6V/K2U5F+/TrGWi0L8Ef0LSlOYY9J/mYNVvgnlP60=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=qkF6dkq1fRPeKYrmo0N4OEYuZNpSyz35fUKMx2vVrZu184/T3qzNiBCANNtpSccbB 9kjK4UYO5vNYtkUgluusiMZsdTZmrpp+ADkAYC0SjU2J85I1zqIAlulYGrsxGIfkwG 1e/FJ8NbGgzLYBc+aQZAb4Nt3+2ySezS7GL2H85BqywdoKa7QoEUetDk6SD/O2AobO JlmJDz2vXunzPu2yDvcPgqrAKEmvgdJtJ7aOW0mw2nIXOUBoOHhS5IhvkBwuxjly4X WIlROSxriuOAlI9CInawgvLAOWI4FtXf3auXUm66VKDfBPK8JQcqTWZapuE0V8lsp1 Y/gfieldkBfDQ== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 2/6] ethtool: Add loopback GET/SET netlink implementation Date: Sun, 8 Mar 2026 13:40:08 +0100 Message-ID: <20260308124016.3134012-3-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add the kernel-side ETHTOOL_MSG_LOOPBACK_GET, ETHTOOL_MSG_LOOPBACK_SET, and ETHTOOL_MSG_LOOPBACK_NTF handlers using the standard ethnl_request_ops infrastructure. GET collects loopback entries from per-component helpers via loopback_get_entries(). SET parses the nested entry attributes, dispatches each to loopback_set_one(), and only sends a notification when the state is changed. No components are wired yet. Signed-off-by: Bj=C3=B6rn T=C3=B6pel --- include/linux/ethtool.h | 28 +++++ net/ethtool/Makefile | 2 +- net/ethtool/loopback.c | 246 ++++++++++++++++++++++++++++++++++++++++ net/ethtool/netlink.c | 20 ++++ net/ethtool/netlink.h | 3 + 5 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 net/ethtool/loopback.c diff --git a/include/linux/ethtool.h b/include/linux/ethtool.h index 83c375840835..b1ebfe22c355 100644 --- a/include/linux/ethtool.h +++ b/include/linux/ethtool.h @@ -846,6 +846,34 @@ void ethtool_mmsv_set_mm(struct ethtool_mmsv *mmsv, st= ruct ethtool_mm_cfg *cfg); void ethtool_mmsv_init(struct ethtool_mmsv *mmsv, struct net_device *dev, const struct ethtool_mmsv_ops *ops); =20 +/** + * struct ethtool_loopback_entry - Per-component loopback configuration + * @id: Optional component instance identifier, 0 means not specified + * @supported: Bitmask of supported directions + * @component: Loopback component + * @direction: Current loopback direction, 0 means disabled + * @name: Subsystem-specific name for the loopback point + */ +struct ethtool_loopback_entry { + enum ethtool_loopback_component component; + u32 id; + u32 supported; + u32 direction; + char name[ETH_GSTRING_LEN]; +}; + +#define ETHTOOL_LOOPBACK_MAX_ENTRIES 16 + +/** + * struct ethtool_loopback_cfg - Loopback configuration + * @entries: Array of per-component loopback configurations + * @n_entries: Number of valid entries in the array + */ +struct ethtool_loopback_cfg { + struct ethtool_loopback_entry entries[ETHTOOL_LOOPBACK_MAX_ENTRIES]; + u32 n_entries; +}; + /** * struct ethtool_rxfh_param - RXFH (RSS) parameters * @hfunc: Defines the current RSS hash function used by HW (or to be set = to). diff --git a/net/ethtool/Makefile b/net/ethtool/Makefile index 629c10916670..ef534b55d724 100644 --- a/net/ethtool/Makefile +++ b/net/ethtool/Makefile @@ -9,4 +9,4 @@ ethtool_nl-y :=3D netlink.o bitset.o strset.o linkinfo.o li= nkmodes.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 + phy.o tsconfig.o mse.o loopback.o diff --git a/net/ethtool/loopback.c b/net/ethtool/loopback.c new file mode 100644 index 000000000000..1c6d27857f8a --- /dev/null +++ b/net/ethtool/loopback.c @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-2.0-only + +#include "netlink.h" +#include "common.h" + +struct loopback_req_info { + struct ethnl_req_info base; +}; + +struct loopback_reply_data { + struct ethnl_reply_data base; + struct ethtool_loopback_cfg cfg; +}; + +#define LOOPBACK_REPDATA(__reply_base) \ + container_of(__reply_base, struct loopback_reply_data, base) + +/* GET */ + +static const struct nla_policy +ethnl_loopback_entry_policy[ETHTOOL_A_LOOPBACK_ENTRY_MAX + 1] =3D { + [ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT] =3D + NLA_POLICY_MAX(NLA_U32, ETHTOOL_LOOPBACK_COMPONENT_MODULE), + [ETHTOOL_A_LOOPBACK_ENTRY_ID] =3D + NLA_POLICY_MIN(NLA_U32, 1), + [ETHTOOL_A_LOOPBACK_ENTRY_NAME] =3D + { .type =3D NLA_NUL_STRING, .len =3D ETH_GSTRING_LEN - 1 }, + [ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION] =3D + NLA_POLICY_MASK(NLA_U32, ETHTOOL_LOOPBACK_DIRECTION_NEAR_END | + ETHTOOL_LOOPBACK_DIRECTION_FAR_END), +}; + +const struct nla_policy ethnl_loopback_get_policy[] =3D { + [ETHTOOL_A_LOOPBACK_HEADER] =3D NLA_POLICY_NESTED(ethnl_header_policy), +}; + +static int loopback_get_entries(struct net_device *dev, + struct ethtool_loopback_cfg *cfg) +{ + return 0; +} + +static int loopback_prepare_data(const struct ethnl_req_info *req_base, + struct ethnl_reply_data *reply_base, + const struct genl_info *info) +{ + struct loopback_reply_data *data =3D LOOPBACK_REPDATA(reply_base); + struct net_device *dev =3D reply_base->dev; + int ret; + + ret =3D ethnl_ops_begin(dev); + if (ret < 0) + return ret; + + ret =3D loopback_get_entries(dev, &data->cfg); + + ethnl_ops_complete(dev); + + return ret; +} + +static int loopback_reply_size(const struct ethnl_req_info *req_base, + const struct ethnl_reply_data *reply_base) +{ + const struct loopback_reply_data *data =3D LOOPBACK_REPDATA(reply_base); + int entry_size; + + /* Per-entry: nest + component + id + name + supported + direction */ + entry_size =3D nla_total_size(0) + /* nest */ + nla_total_size(sizeof(u32)) + /* component */ + nla_total_size(sizeof(u32)) + /* id */ + nla_total_size(sizeof(u32)) + /* supported */ + nla_total_size(sizeof(u32)) + /* direction */ + nla_total_size(ETH_GSTRING_LEN); /* name */ + + return data->cfg.n_entries * entry_size; +} + +static int loopback_fill_entry(struct sk_buff *skb, + const struct ethtool_loopback_entry *entry) +{ + struct nlattr *nest; + + nest =3D nla_nest_start(skb, ETHTOOL_A_LOOPBACK_ENTRY); + if (!nest) + return -EMSGSIZE; + + if (nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT, + entry->component)) + goto err_cancel; + + if (entry->id && + nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_ID, entry->id)) + goto err_cancel; + + if (nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_SUPPORTED, + entry->supported) || + nla_put_u32(skb, ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION, + entry->direction) || + nla_put_string(skb, ETHTOOL_A_LOOPBACK_ENTRY_NAME, + entry->name)) + goto err_cancel; + + nla_nest_end(skb, nest); + return 0; + +err_cancel: + nla_nest_cancel(skb, nest); + return -EMSGSIZE; +} + +static int loopback_fill_reply(struct sk_buff *skb, + const struct ethnl_req_info *req_base, + const struct ethnl_reply_data *reply_base) +{ + const struct loopback_reply_data *data =3D LOOPBACK_REPDATA(reply_base); + const struct ethtool_loopback_cfg *cfg =3D &data->cfg; + u32 i; + + for (i =3D 0; i < cfg->n_entries; i++) { + int ret =3D loopback_fill_entry(skb, &cfg->entries[i]); + + if (ret < 0) + return ret; + } + + return 0; +} + +/* SET */ + +const struct nla_policy ethnl_loopback_set_policy[ETHTOOL_A_LOOPBACK_ENTRY= + 1] =3D { + [ETHTOOL_A_LOOPBACK_HEADER] =3D NLA_POLICY_NESTED(ethnl_header_policy), + [ETHTOOL_A_LOOPBACK_ENTRY] =3D NLA_POLICY_NESTED(ethnl_loopback_entry_po= licy), +}; + +static int loopback_parse_entry(struct nlattr *attr, + struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack) +{ + struct nlattr *tb[ETHTOOL_A_LOOPBACK_ENTRY_MAX + 1]; + int ret; + + ret =3D nla_parse_nested(tb, ETHTOOL_A_LOOPBACK_ENTRY_MAX, attr, + ethnl_loopback_entry_policy, extack); + if (ret < 0) + return ret; + + if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT]) { + NL_SET_ERR_MSG_ATTR(extack, attr, + "loopback component is required"); + return -EINVAL; + } + + entry->component =3D nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_COMPONENT]); + + if (tb[ETHTOOL_A_LOOPBACK_ENTRY_ID]) + entry->id =3D nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_ID]); + + if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_NAME]) { + NL_SET_ERR_MSG_ATTR(extack, attr, + "loopback name is required"); + return -EINVAL; + } + nla_strscpy(entry->name, tb[ETHTOOL_A_LOOPBACK_ENTRY_NAME], + sizeof(entry->name)); + + if (!tb[ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION]) { + NL_SET_ERR_MSG_ATTR(extack, attr, + "loopback direction is required"); + return -EINVAL; + } + + entry->direction =3D nla_get_u32(tb[ETHTOOL_A_LOOPBACK_ENTRY_DIRECTION]); + + return 0; +} + +static int loopback_set_one(struct net_device *dev, + const struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack) +{ + switch (entry->component) { + default: + return -EOPNOTSUPP; + } +} + +static int ethnl_set_loopback(struct ethnl_req_info *req_info, + struct genl_info *info) +{ + struct net_device *dev =3D req_info->dev; + struct ethtool_loopback_cfg cfg =3D {}; + int rem, ret, mod =3D 0; + struct nlattr *attr; + u32 i; + + nla_for_each_attr(attr, genlmsg_data(info->genlhdr), + genlmsg_len(info->genlhdr), rem) { + if (nla_type(attr) !=3D ETHTOOL_A_LOOPBACK_ENTRY) + continue; + + if (cfg.n_entries >=3D ETHTOOL_LOOPBACK_MAX_ENTRIES) { + NL_SET_ERR_MSG(info->extack, + "too many loopback entries"); + return -EINVAL; + } + + ret =3D loopback_parse_entry(attr, &cfg.entries[cfg.n_entries], + info->extack); + if (ret < 0) + return ret; + + cfg.n_entries++; + } + + if (!cfg.n_entries) { + NL_SET_ERR_MSG(info->extack, "no loopback entries specified"); + return -EINVAL; + } + + for (i =3D 0; i < cfg.n_entries; i++) { + ret =3D loopback_set_one(dev, &cfg.entries[i], info->extack); + if (ret < 0) + return ret; + if (ret > 0) + mod =3D 1; + } + + return mod; +} + +const struct ethnl_request_ops ethnl_loopback_request_ops =3D { + .request_cmd =3D ETHTOOL_MSG_LOOPBACK_GET, + .reply_cmd =3D ETHTOOL_MSG_LOOPBACK_GET_REPLY, + .hdr_attr =3D ETHTOOL_A_LOOPBACK_HEADER, + .req_info_size =3D sizeof(struct loopback_req_info), + .reply_data_size =3D sizeof(struct loopback_reply_data), + + .prepare_data =3D loopback_prepare_data, + .reply_size =3D loopback_reply_size, + .fill_reply =3D loopback_fill_reply, + + .set =3D ethnl_set_loopback, + .set_ntf_cmd =3D ETHTOOL_MSG_LOOPBACK_NTF, +}; diff --git a/net/ethtool/netlink.c b/net/ethtool/netlink.c index 6e5f0f4f815a..c438828ea072 100644 --- a/net/ethtool/netlink.c +++ b/net/ethtool/netlink.c @@ -421,6 +421,8 @@ ethnl_default_requests[__ETHTOOL_MSG_USER_CNT] =3D { [ETHTOOL_MSG_TSCONFIG_SET] =3D ðnl_tsconfig_request_ops, [ETHTOOL_MSG_PHY_GET] =3D ðnl_phy_request_ops, [ETHTOOL_MSG_MSE_GET] =3D ðnl_mse_request_ops, + [ETHTOOL_MSG_LOOPBACK_GET] =3D ðnl_loopback_request_ops, + [ETHTOOL_MSG_LOOPBACK_SET] =3D ðnl_loopback_request_ops, }; =20 static struct ethnl_dump_ctx *ethnl_dump_context(struct netlink_callback *= cb) @@ -962,6 +964,7 @@ ethnl_default_notify_ops[ETHTOOL_MSG_KERNEL_MAX + 1] = =3D { [ETHTOOL_MSG_MM_NTF] =3D ðnl_mm_request_ops, [ETHTOOL_MSG_RSS_NTF] =3D ðnl_rss_request_ops, [ETHTOOL_MSG_RSS_CREATE_NTF] =3D ðnl_rss_request_ops, + [ETHTOOL_MSG_LOOPBACK_NTF] =3D ðnl_loopback_request_ops, }; =20 /* default notification handler */ @@ -1070,6 +1073,7 @@ static const ethnl_notify_handler_t ethnl_notify_hand= lers[] =3D { [ETHTOOL_MSG_MM_NTF] =3D ethnl_default_notify, [ETHTOOL_MSG_RSS_NTF] =3D ethnl_default_notify, [ETHTOOL_MSG_RSS_CREATE_NTF] =3D ethnl_default_notify, + [ETHTOOL_MSG_LOOPBACK_NTF] =3D ethnl_default_notify, }; =20 void ethnl_notify(struct net_device *dev, unsigned int cmd, @@ -1544,6 +1548,22 @@ static const struct genl_ops ethtool_genl_ops[] =3D { .policy =3D ethnl_mse_get_policy, .maxattr =3D ARRAY_SIZE(ethnl_mse_get_policy) - 1, }, + { + .cmd =3D ETHTOOL_MSG_LOOPBACK_GET, + .doit =3D ethnl_default_doit, + .start =3D ethnl_default_start, + .dumpit =3D ethnl_default_dumpit, + .done =3D ethnl_default_done, + .policy =3D ethnl_loopback_get_policy, + .maxattr =3D ARRAY_SIZE(ethnl_loopback_get_policy) - 1, + }, + { + .cmd =3D ETHTOOL_MSG_LOOPBACK_SET, + .flags =3D GENL_UNS_ADMIN_PERM, + .doit =3D ethnl_default_set_doit, + .policy =3D ethnl_loopback_set_policy, + .maxattr =3D ARRAY_SIZE(ethnl_loopback_set_policy) - 1, + }, }; =20 static const struct genl_multicast_group ethtool_nl_mcgrps[] =3D { diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h index 89010eaa67df..5660ce494916 100644 --- a/net/ethtool/netlink.h +++ b/net/ethtool/netlink.h @@ -443,6 +443,7 @@ extern const struct ethnl_request_ops ethnl_mm_request_= ops; extern const struct ethnl_request_ops ethnl_phy_request_ops; extern const struct ethnl_request_ops ethnl_tsconfig_request_ops; extern const struct ethnl_request_ops ethnl_mse_request_ops; +extern const struct ethnl_request_ops ethnl_loopback_request_ops; =20 extern const struct nla_policy ethnl_header_policy[ETHTOOL_A_HEADER_FLAGS = + 1]; extern const struct nla_policy ethnl_header_policy_stats[ETHTOOL_A_HEADER_= FLAGS + 1]; @@ -499,6 +500,8 @@ extern const struct nla_policy ethnl_phy_get_policy[ETH= TOOL_A_PHY_HEADER + 1]; extern const struct nla_policy ethnl_tsconfig_get_policy[ETHTOOL_A_TSCONFI= G_HEADER + 1]; extern const struct nla_policy ethnl_tsconfig_set_policy[ETHTOOL_A_TSCONFI= G_MAX + 1]; extern const struct nla_policy ethnl_mse_get_policy[ETHTOOL_A_MSE_HEADER += 1]; +extern const struct nla_policy ethnl_loopback_get_policy[ETHTOOL_A_LOOPBAC= K_HEADER + 1]; +extern const struct nla_policy ethnl_loopback_set_policy[ETHTOOL_A_LOOPBAC= K_ENTRY + 1]; =20 int ethnl_set_features(struct sk_buff *skb, struct genl_info *info); int ethnl_act_cable_test(struct sk_buff *skb, struct genl_info *info); --=20 2.53.0 From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C0ED2314B76; Sun, 8 Mar 2026 12:40:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973638; cv=none; b=QxZR+KGuuyHKBh7uLnBBB3xA657ihWzidMf91Dz/mbp50tXgBoX8YMufzUGEiJsdhcpiqh3io/+F18Hezvh7Yd6CBDLobuRQ26J8hmvnJRQtx16QaKJA0b4yWkUhaA2PPKCjx/VbgrnRaTLr1KK6SqScajuRi2Us74W9Al43ulU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973638; c=relaxed/simple; bh=n0zmjGWD/AHlp3pJeJha/3C6wqT5Iw+GfJKYJAZPf/s=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=UJCSx43JeQXViEDMMIz6iK+7fHHhWDb7ipwaqrrt5CDanIBTkbTq1cWsZvyD7IPLoBR5ZDvHB3aSg3G+wqppILfmEu0X/2ZDvq+Pag6Xxt6crc2BC1dpKSFSgxcdknd5efA0YJz0600gBEY3n84/SuKwJBh6FZbPILMklQvqyts= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=Yr9WkOYr; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="Yr9WkOYr" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 10F48C116C6; Sun, 8 Mar 2026 12:40:33 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973638; bh=n0zmjGWD/AHlp3pJeJha/3C6wqT5Iw+GfJKYJAZPf/s=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=Yr9WkOYrzEoWNCFvChWYOtbE8WjQg5ttn+4w5/SsU7EvtteVSSi9gBxUTWlE0XRHt uIwbvqnJxS7UAupXiUl66igQ2GLxj9vjZnkwxEQQBk04ye6SOg59zlcNQtl4M3Trkf UL3orFkNx3kwo2+pa4loPbDEt4oG5/LhBCt4b2wgXA5dj0LKLcoscBXUXI25J+rT08 khf/NECx/XPzEMpD89OvLUPz/tHxblhRBFCUZObL3kfDPC7ni/qXgC0YXrrVHhNusS kZ1o5DmLRCGEmzzNiPCPK/QehgUfpdXagthf83bydAHOrdjSFhMLvqQR1vwRcwmPo/ 4JAdoS66QpV+A== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 3/6] ethtool: add CMIS loopback helpers for module loopback control Date: Sun, 8 Mar 2026 13:40:09 +0100 Message-ID: <20260308124016.3134012-4-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable 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_set_loopback_one(): resolves name to a pair of control byte indices, validates direction, and writes the Page 13h control bytes (0xFF =3D all lanes on, 0x00 =3D 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=C3=B6rn T=C3=B6pel --- net/ethtool/Makefile | 2 +- net/ethtool/cmis_loopback.c | 338 ++++++++++++++++++++++++++++++++++++ net/ethtool/loopback.c | 4 +- net/ethtool/netlink.h | 5 + 4 files changed, 347 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 :=3D netlink.o bitset.o strset.o linkinfo.o li= nkmodes.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..2114c85f507f --- /dev/null +++ b/net/ethtool/cmis_loopback.c @@ -0,0 +1,338 @@ +// 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 +#include + +#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 =3D dev->ethtool_ops; + struct ethtool_module_eeprom page =3D {}; + 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 =3D &val; + ret =3D 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 =3D &val; + ret =3D 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 =3D &val; + ret =3D 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); +} + +/** + * ethtool_cmis_get_loopback - Append CMIS module loopback entries to cfg + * @dev: Network device with get_module_eeprom_by_page support + * @cfg: Loopback configuration; MODULE entries are appended + * + * Reads CMIS module capabilities and current loopback state from Page + * 13h, then appends one entry for each supported loopback point. + * Returns 0 without adding entries if the module is not CMIS or does + * not advertise loopback support. + * + * Return: 0 on success, negative errno on failure. + */ +int ethtool_cmis_get_loopback(struct net_device *dev, + struct ethtool_loopback_cfg *cfg) +{ + const struct ethtool_ops *ops =3D dev->ethtool_ops; + struct ethtool_module_eeprom page =3D {}; + struct ethtool_loopback_entry host =3D { + .component =3D ETHTOOL_LOOPBACK_COMPONENT_MODULE, + .name =3D CMIS_LB_NAME_HOST, + }; + struct ethtool_loopback_entry media =3D { + .component =3D ETHTOOL_LOOPBACK_COMPONENT_MODULE, + .name =3D CMIS_LB_NAME_MEDIA, + }; + int caps, ret, h =3D 0, m =3D 0; + u8 ctrl[CMIS_LB_CTRL_LEN]; + + if (dev->ethtool->module_fw_flash_in_progress) + return -EBUSY; + + caps =3D cmis_loopback_caps(dev); + if (caps <=3D 0) + return caps; + + /* Read all four control bytes in one access */ + ethtool_cmis_page_init(&page, CMIS_LB_CTRL_PAGE, + CMIS_LB_CTRL_OFFSET, sizeof(ctrl)); + page.data =3D ctrl; + ret =3D ops->get_module_eeprom_by_page(dev, &page, NULL); + if (ret < 0) + return ret; + + if (caps & CMIS_LB_CAP_HOST_INPUT) { + h =3D 1; + host.supported |=3D ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_HOST_INPUT]) + host.direction |=3D ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + } + if (caps & CMIS_LB_CAP_HOST_OUTPUT) { + h =3D 1; + host.supported |=3D ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_HOST_OUTPUT]) + host.direction |=3D ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + } + if (caps & CMIS_LB_CAP_MEDIA_INPUT) { + m =3D 1; + media.supported |=3D ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_MEDIA_INPUT]) + media.direction |=3D ETHTOOL_LOOPBACK_DIRECTION_NEAR_END; + } + if (caps & CMIS_LB_CAP_MEDIA_OUTPUT) { + m =3D 1; + media.supported |=3D ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + if (ctrl[CMIS_LB_CTRL_IDX_MEDIA_OUTPUT]) + media.direction |=3D ETHTOOL_LOOPBACK_DIRECTION_FAR_END; + } + + if (cfg->n_entries + h + m > ETHTOOL_LOOPBACK_MAX_ENTRIES) + return -ENOMEM; + + if (h) { + memcpy(&cfg->entries[cfg->n_entries], &host, sizeof(host)); + cfg->n_entries++; + } + + if (m) { + memcpy(&cfg->entries[cfg->n_entries], &media, sizeof(media)); + cfg->n_entries++; + } + + return 0; +} + +/** + * ethtool_cmis_set_loopback_one - 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 =3D=3D 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_one(struct net_device *dev, + const struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack) +{ + struct ethtool_module_eeprom page =3D {}; + u8 ctrl[CMIS_LB_CTRL_LEN]; + int near_idx, far_idx; + u8 near_cap, far_cap; + bool mod =3D 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) =3D=3D 0) { + near_idx =3D CMIS_LB_CTRL_IDX_HOST_INPUT; + far_idx =3D CMIS_LB_CTRL_IDX_HOST_OUTPUT; + near_cap =3D CMIS_LB_CAP_HOST_INPUT; + far_cap =3D CMIS_LB_CAP_HOST_OUTPUT; + } else if (strcmp(entry->name, CMIS_LB_NAME_MEDIA) =3D=3D 0) { + near_idx =3D CMIS_LB_CTRL_IDX_MEDIA_INPUT; + far_idx =3D CMIS_LB_CTRL_IDX_MEDIA_OUTPUT; + near_cap =3D CMIS_LB_CAP_MEDIA_INPUT; + far_cap =3D CMIS_LB_CAP_MEDIA_OUTPUT; + } else { + NL_SET_ERR_MSG(extack, "Unknown CMIS loopback name"); + return -EINVAL; + } + + caps =3D 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 =3D ctrl; + ret =3D 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] =3D 0x00; + mod =3D true; + } + if (ctrl[far_idx]) { + ctrl[far_idx] =3D 0x00; + mod =3D true; + } + } else { + int enable_idx, disable_idx; + u8 enable_cap; + + if (entry->direction & ETHTOOL_LOOPBACK_DIRECTION_NEAR_END) { + enable_idx =3D near_idx; + enable_cap =3D near_cap; + disable_idx =3D far_idx; + } else { + enable_idx =3D far_idx; + enable_cap =3D far_cap; + disable_idx =3D 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] =3D 0x00; + ret =3D dev->ethtool_ops->set_module_eeprom_by_page(dev, + &page, + extack); + if (ret < 0) + return ret; + mod =3D true; + } + + if (ctrl[enable_idx] !=3D 0xFF) { + ctrl[enable_idx] =3D 0xFF; + mod =3D true; + } + } + + if (!mod) + return 0; + + ret =3D 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 1c6d27857f8a..8a6f14f4b8cb 100644 --- a/net/ethtool/loopback.c +++ b/net/ethtool/loopback.c @@ -37,7 +37,7 @@ const struct nla_policy ethnl_loopback_get_policy[] =3D { static int loopback_get_entries(struct net_device *dev, struct ethtool_loopback_cfg *cfg) { - return 0; + return ethtool_cmis_get_loopback(dev, cfg); } =20 static int loopback_prepare_data(const struct ethnl_req_info *req_base, @@ -181,6 +181,8 @@ static int loopback_set_one(struct net_device *dev, struct netlink_ext_ack *extack) { switch (entry->component) { + case ETHTOOL_LOOPBACK_COMPONENT_MODULE: + return ethtool_cmis_set_loopback_one(dev, entry, extack); default: return -EOPNOTSUPP; } diff --git a/net/ethtool/netlink.h b/net/ethtool/netlink.h index 5660ce494916..707363462c12 100644 --- a/net/ethtool/netlink.h +++ b/net/ethtool/netlink.h @@ -517,6 +517,11 @@ int ethnl_tsinfo_dumpit(struct sk_buff *skb, struct ne= tlink_callback *cb); int ethnl_tsinfo_done(struct netlink_callback *cb); int ethnl_rss_create_doit(struct sk_buff *skb, struct genl_info *info); int ethnl_rss_delete_doit(struct sk_buff *skb, struct genl_info *info); +int ethtool_cmis_get_loopback(struct net_device *dev, + struct ethtool_loopback_cfg *cfg); +int ethtool_cmis_set_loopback_one(struct net_device *dev, + const struct ethtool_loopback_entry *entry, + struct netlink_ext_ack *extack); =20 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_G= STRING_LEN]; --=20 2.53.0 From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8594A27F749; Sun, 8 Mar 2026 12:40:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973643; cv=none; b=X/QTkY1fl/rQWTKaZyF6pRYL6mTUvXodDjXWdY+e2IeKOp/+TJAoIJOLZU206Mhi/rV5/5Luz6xj9Yc5BKQTOvwNfTP2T0oSk5JT9ubVIVA9gH+hp93otSlJ5xXHbeR/7Dwa2Cuu/sk6JSsRHCVAvj0btryT6KEr+fW9hF95Vs8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973643; c=relaxed/simple; bh=kvew3wDTsNSCELg3wrpyIX+U/cTVAQVLMk15hqq6t5I=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=sUPYETEJtcBBunY7d7aYCUIrQUMOvrdWvjWrTGOI+zfd9pPC1xMhS0mMXxVqszgFMaD/7edbT6O32hoBKTp/v9DvrKFyMR/8oX1eOwOycxFUSJPHw5wniCd8UFrvyOBg+WfcudtAfPTy9bp+Wc0/ReXgff627R84AE56pESKNLc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=gBAT6vxV; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="gBAT6vxV" Received: by smtp.kernel.org (Postfix) with ESMTPSA id D79F3C19423; Sun, 8 Mar 2026 12:40:38 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973643; bh=kvew3wDTsNSCELg3wrpyIX+U/cTVAQVLMk15hqq6t5I=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=gBAT6vxVeulyApxqYSRbLOm9NeIOua42QEwq+cs6Az17tmyyI7QGCD6rA2br91e6P 4VJ2NJAiaw66kcaaxjSrOZg+EQoqGYMJklKmvKoa+l9egGUyiRVaxe83eejoBtj6HT SO/dJIyVYrqSFzouuNynfykPAERR9rsT61x1Z9lIU2ikA+wuswyg0LMx6mVfBBToOU HloX3DNR/U5Jvvda9f5BaMLOUlFws2EC239fcdBz/xX9aVox8ZJqiqHxr/0Mot26b0 wxwkRdlVWtPqxpJLY3YUfbEcjVc2O29g85Y1wj3jpogpzeqMrSBAKcaWZYazM4eLu5 eZy0zHeN3yabA== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 4/6] selftests: drv-net: Add loopback driver test Date: Sun, 8 Mar 2026 13:40:10 +0100 Message-ID: <20260308124016.3134012-5-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add a selftest for the ethtool loopback UAPI exercising module loopback via the loopback GET/SET netlink commands. Works on any device that reports module loopback entries. Tests cover enable near-end and far-end, disable, direction switching (mutual exclusivity), idempotent enable, and rejection while interface is up. Devices without module loopback support are skipped. Signed-off-by: Bj=C3=B6rn T=C3=B6pel --- .../selftests/drivers/net/hw/loopback_drv.py | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100755 tools/testing/selftests/drivers/net/hw/loopback_drv.py diff --git a/tools/testing/selftests/drivers/net/hw/loopback_drv.py b/tools= /testing/selftests/drivers/net/hw/loopback_drv.py new file mode 100755 index 000000000000..ab105664e07e --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/loopback_drv.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""Tests for ethtool loopback GET/SET with CMIS modules. + +Works on any device that reports module loopback entries. On devices +without CMIS loopback support, tests are skipped. +""" + +import errno + +from lib.py import ksft_run, ksft_exit, ksft_eq +from lib.py import KsftSkipEx, KsftFailEx, ksft_disruptive +from lib.py import EthtoolFamily, NlError +from lib.py import NetDrvEnv, ip, defer + +# Direction flags as YNL returns them (sets of flag name strings) +DIR_NONE =3D set() +DIR_NEAR_END =3D {'near-end'} +DIR_FAR_END =3D {'far-end'} + + +def _get_loopback(cfg): + """GET loopback and return the list of entries.""" + result =3D cfg.ethnl.loopback_get({ + 'header': {'dev-index': cfg.ifindex} + }) + return result.get('entry', []) + + +def _set_loopback(cfg, component, name, direction): + """SET loopback for a single entry.""" + cfg.ethnl.loopback_set({ + 'header': {'dev-index': cfg.ifindex}, + 'entry': [{ + 'component': component, + 'name': name, + 'direction': direction, + }] + }) + + +def _require_module_entries(cfg): + """Return module loopback entries, skip if none available.""" + try: + entries =3D _get_loopback(cfg) + except NlError as e: + if e.error =3D=3D errno.EOPNOTSUPP: + raise KsftSkipEx("Device does not support loopback") + raise + mod_entries =3D [e for e in entries if e['component'] =3D=3D 'module'] + if not mod_entries: + raise KsftSkipEx("No module loopback entries") + return mod_entries + + +@ksft_disruptive +def test_set_near_end(cfg): + """SET a module entry to near-end and verify via GET.""" + mod_entries =3D _require_module_entries(cfg) + + near =3D [e for e in mod_entries + if 'near-end' in e['supported']] + if not near: + raise KsftSkipEx("No near-end capable module entry") + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + target =3D near[0] + _set_loopback(cfg, 'module', target['name'], 'near-end') + defer(_set_loopback, cfg, 'module', target['name'], 0) + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries + if e['name'] =3D=3D target['name'] + and 'near-end' in e['supported']] + ksft_eq(len(updated), 1) + ksft_eq(updated[0]['direction'], DIR_NEAR_END) + + +@ksft_disruptive +def test_set_far_end(cfg): + """SET a module entry to far-end and verify via GET.""" + mod_entries =3D _require_module_entries(cfg) + + far =3D [e for e in mod_entries + if 'far-end' in e['supported']] + if not far: + raise KsftSkipEx("No far-end capable module entry") + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + target =3D far[0] + _set_loopback(cfg, 'module', target['name'], 'far-end') + defer(_set_loopback, cfg, 'module', target['name'], 0) + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries + if e['name'] =3D=3D target['name'] + and 'far-end' in e['supported']] + ksft_eq(len(updated), 1) + ksft_eq(updated[0]['direction'], DIR_FAR_END) + + +@ksft_disruptive +def test_set_disable(cfg): + """Enable then disable loopback and verify.""" + mod_entries =3D _require_module_entries(cfg) + + near =3D [e for e in mod_entries + if 'near-end' in e['supported']] + if not near: + raise KsftSkipEx("No near-end capable module entry") + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + target =3D near[0] + _set_loopback(cfg, 'module', target['name'], 'near-end') + defer(_set_loopback, cfg, 'module', target['name'], 0) + + # Disable + _set_loopback(cfg, 'module', target['name'], 0) + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries if e['name'] =3D=3D target['name']] + ksft_eq(updated[0]['direction'], DIR_NONE, + "Direction should be off after disable") + + +@ksft_disruptive +def test_set_direction_switch(cfg): + """Enable near-end, then switch to far-end. The kernel must disable + near-end before enabling far-end (mutual exclusivity). + """ + mod_entries =3D _require_module_entries(cfg) + + both =3D [e for e in mod_entries + if 'near-end' in e['supported'] and 'far-end' in e['supported'= ]] + if not both: + raise KsftSkipEx("No entry with both near-end and far-end support") + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + target =3D both[0] + _set_loopback(cfg, 'module', target['name'], 'near-end') + defer(_set_loopback, cfg, 'module', target['name'], 0) + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries if e['name'] =3D=3D target['name']] + ksft_eq(updated[0]['direction'], DIR_NEAR_END) + + # Switch to far-end + _set_loopback(cfg, 'module', target['name'], 'far-end') + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries if e['name'] =3D=3D target['name']] + ksft_eq(updated[0]['direction'], DIR_FAR_END, + "Should have switched to far-end") + + +@ksft_disruptive +def test_set_idempotent(cfg): + """Enable the same direction twice. Second call should not fail.""" + mod_entries =3D _require_module_entries(cfg) + + near =3D [e for e in mod_entries + if 'near-end' in e['supported']] + if not near: + raise KsftSkipEx("No near-end capable module entry") + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + target =3D near[0] + _set_loopback(cfg, 'module', target['name'], 'near-end') + defer(_set_loopback, cfg, 'module', target['name'], 0) + + # Second enable of the same direction should succeed + _set_loopback(cfg, 'module', target['name'], 'near-end') + + entries =3D _get_loopback(cfg) + updated =3D [e for e in entries + if e['name'] =3D=3D target['name'] + and 'near-end' in e['supported']] + ksft_eq(updated[0]['direction'], DIR_NEAR_END, + "Direction should still be near-end") + + +@ksft_disruptive +def test_set_while_up(cfg): + """SET while interface is UP should fail.""" + mod_entries =3D _require_module_entries(cfg) + + target =3D mod_entries[0] + direction =3D 'near-end' + if direction not in target['supported']: + direction =3D 'far-end' + + try: + _set_loopback(cfg, 'module', target['name'], direction) + raise KsftFailEx("Should have rejected SET while interface is up") + except NlError as e: + ksft_eq(e.error, errno.EBUSY, + "Expected EBUSY when interface is up") + + +def main() -> None: + with NetDrvEnv(__file__, nsim_test=3DFalse) as cfg: + cfg.ethnl =3D EthtoolFamily() + + ksft_run([ + test_set_near_end, + test_set_far_end, + test_set_disable, + test_set_direction_switch, + test_set_idempotent, + test_set_while_up, + ], args=3D(cfg, )) + ksft_exit() + + +if __name__ =3D=3D "__main__": + main() --=20 2.53.0 From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8317C299A8A; Sun, 8 Mar 2026 12:40:48 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973648; cv=none; b=c/gqtTAOHgdB7JcPpMhhoH8hd8m2f5asR5UGfc/4S9jnLIFhKR1hbzJjBC079+61WZN/yrFC0d0PpIykbJqly/cwKt1kpQCi0wn4YPzGXqhU99G9ev1QkVg1FxFkfHroWnNciJI+eqh/qjEMB2K7Nl9yXWwyM+Ovd4ws0tEA3b4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973648; c=relaxed/simple; bh=U7ugx5XdRBV8q3eTJ32W5d+auEFcbPDhwzRLSP2M2Xg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=K/MaMVggqworlje9Wmu5Ot6bHDaBNVIUKnibB7cq09WN00Z922Bxs3f/SH2wnStoyLuEcJsCP/zeme6RJeyKiizCovb1TRJIZGRKr2wXhgQmvX12P1JRpimQQt8tLHpn+LAuXbTVaZOoSlGA7I3a0M8ImWL+c8uy4T4exondS+s= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=rC+WqX9X; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="rC+WqX9X" Received: by smtp.kernel.org (Postfix) with ESMTPSA id A961DC2BC9E; Sun, 8 Mar 2026 12:40:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973648; bh=U7ugx5XdRBV8q3eTJ32W5d+auEFcbPDhwzRLSP2M2Xg=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=rC+WqX9X5FXna4ZOpVYqzrc36VoodxM0gYRnimdrq1AMhzPt/F4bGQw9Fpmbo3EFt Jfab7OBwv2VLlUaSzyt2SqOZp0RCZFvPbSBsnLJmMTArymBetS410OdXlVWE5kr7wZ nGD3Scf35hVFfb43Rou4/8crbwiQOTD9kk4XY2sLgtehj+z6XEyUKQ7sQVS9z6uO0K b/SWb+IivrpJrdmS/R6lwGtU119rH7sXbfixGvqQF6BsFAVAV2czzy5HFGo6VCP86a RelN5h6hoBesl2lwEIPwVClbIxc+bkgdPAwOVJ2TdYjUl6XjrDUy07golMZqTJh45i eEw6xTydYOX9w== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 5/6] netdevsim: Add module EEPROM simulation via debugfs Date: Sun, 8 Mar 2026 13:40:11 +0100 Message-ID: <20260308124016.3134012-6-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add get/set_module_eeprom_by_page ethtool ops to netdevsim, enabling testing of kernel features that depend on module EEPROM access (e.g. CMIS loopback) without real hardware. The EEPROM is backed by a 256-page x 128-byte array exposed as binary debugfs files under ports//ethtool/module/pages/{0..255}. Offsets 0-127 map to page 0 (lower memory), 128-255 to the requested page's upper memory, following the CMIS layout. Error injection via get_err and set_err follows the existing netdevsim pattern. Signed-off-by: Bj=C3=B6rn T=C3=B6pel --- drivers/net/netdevsim/ethtool.c | 79 +++++++++++++++++++++++++++++++ drivers/net/netdevsim/netdevsim.h | 11 +++++ 2 files changed, 90 insertions(+) diff --git a/drivers/net/netdevsim/ethtool.c b/drivers/net/netdevsim/ethtoo= l.c index 36a201533aae..2145ccc8a9bd 100644 --- a/drivers/net/netdevsim/ethtool.c +++ b/drivers/net/netdevsim/ethtool.c @@ -195,6 +195,67 @@ nsim_get_fec_stats(struct net_device *dev, struct etht= ool_fec_stats *fec_stats, values[2].per_lane[3] =3D 0; } =20 +static u8 *nsim_module_eeprom_ptr(struct netdevsim *ns, + const struct ethtool_module_eeprom *page_data, u32 *len) +{ + u32 offset; + u8 page; + + if (page_data->offset < NSIM_MODULE_EEPROM_PAGE_LEN) { + page =3D 0; + offset =3D page_data->offset; + } else { + page =3D page_data->page; + offset =3D page_data->offset - NSIM_MODULE_EEPROM_PAGE_LEN; + } + + if (page >=3D NSIM_MODULE_EEPROM_PAGES) + return NULL; + + *len =3D min_t(u32, page_data->length, NSIM_MODULE_EEPROM_PAGE_LEN - offs= et); + return ns->ethtool.module.pages[page] + offset; +} + +static int nsim_get_module_eeprom_by_page(struct net_device *dev, + const struct ethtool_module_eeprom *page_data, + struct netlink_ext_ack *extack) +{ + struct netdevsim *ns =3D netdev_priv(dev); + u32 len; + u8 *ptr; + + if (ns->ethtool.module.get_err) + return -ns->ethtool.module.get_err; + + ptr =3D nsim_module_eeprom_ptr(ns, page_data, &len); + if (!ptr) + return -EINVAL; + + memcpy(page_data->data, ptr, len); + + return len; +} + +static int nsim_set_module_eeprom_by_page(struct net_device *dev, + const struct ethtool_module_eeprom *page_data, + struct netlink_ext_ack *extack) +{ + struct netdevsim *ns =3D netdev_priv(dev); + u32 len; + u8 *ptr; + + if (ns->ethtool.module.set_err) + return -ns->ethtool.module.set_err; + + ptr =3D nsim_module_eeprom_ptr(ns, page_data, &len); + if (!ptr) + return -EINVAL; + + memcpy(ptr, page_data->data, len); + + return 0; +} + static int nsim_get_ts_info(struct net_device *dev, struct kernel_ethtool_ts_info *info) { @@ -222,6 +283,8 @@ static const struct ethtool_ops nsim_ethtool_ops =3D { .set_fecparam =3D nsim_set_fecparam, .get_fec_stats =3D nsim_get_fec_stats, .get_ts_info =3D nsim_get_ts_info, + .get_module_eeprom_by_page =3D nsim_get_module_eeprom_by_page, + .set_module_eeprom_by_page =3D nsim_set_module_eeprom_by_page, }; =20 static void nsim_ethtool_ring_init(struct netdevsim *ns) @@ -237,6 +300,7 @@ static void nsim_ethtool_ring_init(struct netdevsim *ns) void nsim_ethtool_init(struct netdevsim *ns) { struct dentry *ethtool, *dir; + int i; =20 ns->netdev->ethtool_ops =3D &nsim_ethtool_ops; =20 @@ -270,4 +334,19 @@ void nsim_ethtool_init(struct netdevsim *ns) &ns->ethtool.ring.rx_mini_max_pending); debugfs_create_u32("tx_max_pending", 0600, dir, &ns->ethtool.ring.tx_max_pending); + + dir =3D debugfs_create_dir("module", ethtool); + debugfs_create_u32("get_err", 0600, dir, &ns->ethtool.module.get_err); + debugfs_create_u32("set_err", 0600, dir, &ns->ethtool.module.set_err); + + dir =3D debugfs_create_dir("pages", dir); + for (i =3D 0; i < NSIM_MODULE_EEPROM_PAGES; i++) { + char name[8]; + + ns->ethtool.module.page_blobs[i].data =3D ns->ethtool.module.pages[i]; + ns->ethtool.module.page_blobs[i].size =3D NSIM_MODULE_EEPROM_PAGE_LEN; + + snprintf(name, sizeof(name), "%u", i); + debugfs_create_blob(name, 0600, dir, &ns->ethtool.module.page_blobs[i]); + } } diff --git a/drivers/net/netdevsim/netdevsim.h b/drivers/net/netdevsim/netd= evsim.h index f767fc8a7505..965d0aa0940b 100644 --- a/drivers/net/netdevsim/netdevsim.h +++ b/drivers/net/netdevsim/netdevsim.h @@ -82,6 +82,16 @@ struct nsim_ethtool_pauseparam { bool report_stats_tx; }; =20 +#define NSIM_MODULE_EEPROM_PAGES 256 +#define NSIM_MODULE_EEPROM_PAGE_LEN 128 + +struct nsim_ethtool_module { + u32 get_err; + u32 set_err; + u8 pages[NSIM_MODULE_EEPROM_PAGES][NSIM_MODULE_EEPROM_PAGE_LEN]; + struct debugfs_blob_wrapper page_blobs[NSIM_MODULE_EEPROM_PAGES]; +}; + struct nsim_ethtool { u32 get_err; u32 set_err; @@ -90,6 +100,7 @@ struct nsim_ethtool { struct ethtool_coalesce coalesce; struct ethtool_ringparam ring; struct ethtool_fecparam fec; + struct nsim_ethtool_module module; }; =20 struct nsim_rq { --=20 2.53.0 From nobody Thu Apr 9 13:30:35 2026 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 32E5727F749; Sun, 8 Mar 2026 12:40:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973653; cv=none; b=tQoB+i7u80HgSKyjS2vfoKU12PauHbqWR9wKlupnJd852422n2jElc71d0nQIYHr9Zta18YQbhO49EFweGF0u0XKUFvPlE+WtNl+4aJ0Twd+2oTJzJ1uxcIqPpsMEgQqiQbGh8XF4OJnQVwaYKte7bHKb74I61btKRg11KEBM98= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1772973653; c=relaxed/simple; bh=2grjSFFYRY6f1rOWikPMtaX7YwYNLtL6HeCQhRoibfQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=P6PgMKLnD7NGNmkhn1nE0EFCbEzun3AOGf2+1F0EVCcybw2dBVntB6XMMXAOENgs7E6DShdrzHgOzL50AD/EJ8HEd6p4Gf8JsJzZyy1KKSC5LEP2lr1orC72tkTzNw7xRCsvZn6UZajB2uL86yoaZHBEI+rQIjir9UttNyPmSUw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=VNWbPla8; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="VNWbPla8" Received: by smtp.kernel.org (Postfix) with ESMTPSA id 7B2CBC116C6; Sun, 8 Mar 2026 12:40:48 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1772973652; bh=2grjSFFYRY6f1rOWikPMtaX7YwYNLtL6HeCQhRoibfQ=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=VNWbPla8yZEy1Eq0vjIjmJxKX+lF8V5Jdh7WD4wzjauSXu5kO6fzZo8nK1YOHIZ5J FMUF9TChOoW1ZcbyDiALYAdpzGHgL22fLA3P9dxB48Ur3UgN1mBQHeWOF90+jWHXWZ snQkZSJcMQwpONpDzcyBvFu9GeuCGmAhF/KYrln+O1pnm9DfgrIKlAwp/CQ5DJIAui cX7cP3nOoKVy+MmuTqXxjnQSPJ6uGSpmSG788N8UykH1BY6E3t77UicayHmxM1iou/ vYqz7v8gZ9xEgUzVnIw3TLgfb9d/z1jW0oBS/6O48NCn9t2MIW1i+mcOkTeSc46XWq RXDgbNf05bAQg== From: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= To: netdev@vger.kernel.org, Donald Hunter , Jakub Kicinski , "David S. Miller" , Eric Dumazet , Paolo Abeni , Simon Horman , Saeed Mahameed , Tariq Toukan , Leon Romanovsky , Andrew Lunn Cc: =?UTF-8?q?Bj=C3=B6rn=20T=C3=B6pel?= , Maxime Chevallier , Andrew Lunn , Michael Chan , Hariprasad Kelam , Ido Schimmel , Danielle Ratson , linux-kernel@vger.kernel.org, linux-rdma@vger.kernel.org, Russell King Subject: [RFC net-next v2 6/6] selftests: drv-net: Add CMIS loopback netdevsim test Date: Sun, 8 Mar 2026 13:40:12 +0100 Message-ID: <20260308124016.3134012-7-bjorn@kernel.org> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260308124016.3134012-1-bjorn@kernel.org> References: <20260308124016.3134012-1-bjorn@kernel.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Add loopback_nsim.py with netdevsim-specific tests for CMIS module loopback. These tests seed the EEPROM via debugfs and verify register-level behavior. Tests cover: no-module GET, all/partial capability reporting, EEPROM byte verification for enable/disable and direction switching, rejection of unsupported directions, and rejection without CMIS support. Signed-off-by: Bj=C3=B6rn T=C3=B6pel --- .../selftests/drivers/net/hw/loopback_nsim.py | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100755 tools/testing/selftests/drivers/net/hw/loopback_nsim.py diff --git a/tools/testing/selftests/drivers/net/hw/loopback_nsim.py b/tool= s/testing/selftests/drivers/net/hw/loopback_nsim.py new file mode 100755 index 000000000000..ae126da671bb --- /dev/null +++ b/tools/testing/selftests/drivers/net/hw/loopback_nsim.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 + +"""Netdevsim-specific tests for ethtool CMIS module loopback. + +Seeds the CMIS EEPROM via debugfs and verifies register-level +behavior that can only be checked with controlled EEPROM contents. +""" + +import errno +import os + +from lib.py import ksft_run, ksft_exit, ksft_eq +from lib.py import KsftSkipEx, KsftFailEx, ksft_disruptive +from lib.py import EthtoolFamily, NlError +from lib.py import NetDrvEnv, ip, defer + +# CMIS register constants matching net/ethtool/cmis_loopback.c +SFF8024_ID_QSFP_DD =3D 0x18 + +# Page 01h, Byte 142: bit 5 =3D Page 13h supported +CMIS_DIAG_PAGE13_BIT =3D 1 << 5 + +# Page 13h, Byte 128: loopback capability bits +CMIS_LB_CAP_MEDIA_OUTPUT =3D 1 << 0 +CMIS_LB_CAP_MEDIA_INPUT =3D 1 << 1 +CMIS_LB_CAP_HOST_OUTPUT =3D 1 << 2 +CMIS_LB_CAP_HOST_INPUT =3D 1 << 3 +CMIS_LB_CAP_ALL =3D (CMIS_LB_CAP_MEDIA_OUTPUT | CMIS_LB_CAP_MEDIA_INPUT | + CMIS_LB_CAP_HOST_OUTPUT | CMIS_LB_CAP_HOST_INPUT) + +# Direction flags as YNL returns them (sets of flag name strings) +DIR_NONE =3D set() +DIR_NEAR_END =3D {'near-end'} +DIR_FAR_END =3D {'far-end'} + + +def _nsim_dfs_path(cfg): + """Return the per-port debugfs path for the netdevsim device.""" + return cfg._ns.nsims[0].dfs_dir + + +def _nsim_write_page_byte(cfg, page, offset, value): + """Write a single byte to a netdevsim EEPROM page via debugfs.""" + if offset < 128: + page_file =3D os.path.join(_nsim_dfs_path(cfg), + "ethtool/module/pages/0") + file_offset =3D offset + else: + page_file =3D os.path.join(_nsim_dfs_path(cfg), + f"ethtool/module/pages/{page}") + file_offset =3D offset - 128 + + with open(page_file, "r+b") as f: + f.seek(file_offset) + f.write(bytes([value])) + + +def _nsim_read_page_byte(cfg, page, offset): + """Read a single byte from a netdevsim EEPROM page via debugfs.""" + if offset < 128: + page_file =3D os.path.join(_nsim_dfs_path(cfg), + "ethtool/module/pages/0") + file_offset =3D offset + else: + page_file =3D os.path.join(_nsim_dfs_path(cfg), + f"ethtool/module/pages/{page}") + file_offset =3D offset - 128 + + with open(page_file, "rb") as f: + f.seek(file_offset) + return f.read(1)[0] + + +def _nsim_seed_cmis(cfg, caps=3DCMIS_LB_CAP_ALL): + """Seed the netdevsim EEPROM with CMIS module identity and + loopback capabilities. + """ + _nsim_write_page_byte(cfg, 0x00, 0, SFF8024_ID_QSFP_DD) + _nsim_write_page_byte(cfg, 0x01, 0x8E, CMIS_DIAG_PAGE13_BIT) + _nsim_write_page_byte(cfg, 0x13, 0x80, caps) + + +def _nsim_clear_cmis(cfg): + """Clear CMIS identity bytes left by previous tests.""" + _nsim_write_page_byte(cfg, 0x00, 0, 0) + _nsim_write_page_byte(cfg, 0x01, 0x8E, 0) + _nsim_write_page_byte(cfg, 0x13, 0x80, 0) + + +def _get_loopback(cfg): + """GET loopback and return the list of entries.""" + result =3D cfg.ethnl.loopback_get({ + 'header': {'dev-index': cfg.ifindex} + }) + return result.get('entry', []) + + +def _set_loopback(cfg, component, name, direction): + """SET loopback for a single entry.""" + cfg.ethnl.loopback_set({ + 'header': {'dev-index': cfg.ifindex}, + 'entry': [{ + 'component': component, + 'name': name, + 'direction': direction, + }] + }) + + +def test_get_no_module(cfg): + """GET on a device with no CMIS module returns no entries.""" + _nsim_clear_cmis(cfg) + + try: + entries =3D _get_loopback(cfg) + ksft_eq(len(entries), 0, "Expected no entries without CMIS module") + except NlError as e: + ksft_eq(e.error, errno.EOPNOTSUPP, + "Expected EOPNOTSUPP without CMIS module") + + +def test_get_all_caps(cfg): + """GET with all four CMIS loopback capabilities seeded.""" + _nsim_seed_cmis(cfg, CMIS_LB_CAP_ALL) + + entries =3D _get_loopback(cfg) + mod_entries =3D [e for e in entries if e['component'] =3D=3D 'module'] + + # Expect 2 entries (one per name), each with both directions + ksft_eq(len(mod_entries), 2, "Expected 2 module loopback entries") + + host =3D [e for e in mod_entries if e['name'] =3D=3D 'cmis-host'] + media =3D [e for e in mod_entries if e['name'] =3D=3D 'cmis-media'] + ksft_eq(len(host), 1, "Expected 1 cmis-host entry") + ksft_eq(len(media), 1, "Expected 1 cmis-media entry") + + ksft_eq(host[0]['supported'], DIR_NEAR_END | DIR_FAR_END) + ksft_eq(media[0]['supported'], DIR_NEAR_END | DIR_FAR_END) + + for e in mod_entries: + ksft_eq(e['direction'], DIR_NONE, + f"Expected direction=3Doff for {e['name']}") + + +def test_get_partial_caps(cfg): + """GET with only host-input capability advertised.""" + _nsim_seed_cmis(cfg, CMIS_LB_CAP_HOST_INPUT) + + entries =3D _get_loopback(cfg) + mod_entries =3D [e for e in entries if e['component'] =3D=3D 'module'] + ksft_eq(len(mod_entries), 1, "Expected 1 module loopback entry") + ksft_eq(mod_entries[0]['name'], 'cmis-host') + ksft_eq(mod_entries[0]['supported'], DIR_NEAR_END) + + +@ksft_disruptive +def test_set_verify_eeprom(cfg): + """SET near-end and verify the EEPROM control byte directly.""" + _nsim_seed_cmis(cfg, CMIS_LB_CAP_ALL) + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + _set_loopback(cfg, 'module', 'cmis-host', 'near-end') + defer(_set_loopback, cfg, 'module', 'cmis-host', 0) + + # Host Side Input =3D Page 13h, Byte 183 + val =3D _nsim_read_page_byte(cfg, 0x13, 183) + ksft_eq(val, 0xFF, "Host Side Input control byte should be 0xFF") + + # Disable and verify + _set_loopback(cfg, 'module', 'cmis-host', 0) + val =3D _nsim_read_page_byte(cfg, 0x13, 183) + ksft_eq(val, 0x00, "Host Side Input should be 0x00 after disable") + + +@ksft_disruptive +def test_set_direction_switch_eeprom(cfg): + """Switch directions and verify both EEPROM bytes.""" + _nsim_seed_cmis(cfg, CMIS_LB_CAP_ALL) + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + _set_loopback(cfg, 'module', 'cmis-host', 'near-end') + defer(_set_loopback, cfg, 'module', 'cmis-host', 0) + + # Switch to far-end + _set_loopback(cfg, 'module', 'cmis-host', 'far-end') + + # Near-end (Host Input, Byte 183) should be disabled + val =3D _nsim_read_page_byte(cfg, 0x13, 183) + ksft_eq(val, 0x00, "Near-end should be disabled after switch") + # Far-end (Host Output, Byte 182) should be enabled + val =3D _nsim_read_page_byte(cfg, 0x13, 182) + ksft_eq(val, 0xFF, "Far-end should be enabled after switch") + + +@ksft_disruptive +def test_set_unsupported_direction(cfg): + """SET with unsupported direction should fail.""" + _nsim_seed_cmis(cfg, CMIS_LB_CAP_HOST_INPUT) # only near-end + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + try: + _set_loopback(cfg, 'module', 'cmis-host', 'far-end') + raise KsftFailEx("Should have rejected unsupported direction") + except NlError as e: + ksft_eq(e.error, errno.EOPNOTSUPP, + "Expected EOPNOTSUPP for unsupported direction") + + +@ksft_disruptive +def test_set_no_cmis(cfg): + """SET on a device without CMIS loopback support should fail.""" + _nsim_clear_cmis(cfg) + + ip(f"link set dev {cfg.ifname} down") + defer(ip, f"link set dev {cfg.ifname} up") + + try: + _set_loopback(cfg, 'module', 'cmis-host', 'near-end') + raise KsftFailEx("Should have rejected SET without CMIS support") + except NlError as e: + ksft_eq(e.error, errno.EOPNOTSUPP, + "Expected EOPNOTSUPP without CMIS support") + + +def main() -> None: + with NetDrvEnv(__file__) as cfg: + cfg.ethnl =3D EthtoolFamily() + + ksft_run([ + test_get_no_module, + test_get_all_caps, + test_get_partial_caps, + test_set_verify_eeprom, + test_set_direction_switch_eeprom, + test_set_unsupported_direction, + test_set_no_cmis, + ], args=3D(cfg, )) + ksft_exit() + + +if __name__ =3D=3D "__main__": + main() --=20 2.53.0