| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Copyright 2023 Bootlin | |
| * | |
| */ | |
| #include "common.h" | |
| #include "netlink.h" | |
| #include <linux/phy.h> | |
| #include <linux/phy_link_topology.h> | |
| #include <linux/sfp.h> | |
| struct phy_req_info { | |
| struct ethnl_req_info base; | |
| struct phy_device_node *pdn; | |
| }; | |
| #define PHY_REQINFO(__req_base) \ | |
| container_of(__req_base, struct phy_req_info, base) | |
| const struct nla_policy ethnl_phy_get_policy[ETHTOOL_A_PHY_HEADER + 1] = { | |
| [ETHTOOL_A_PHY_HEADER] = NLA_POLICY_NESTED(ethnl_header_policy), | |
| }; | |
| /* Caller holds rtnl */ | |
| static ssize_t | |
| ethnl_phy_reply_size(const struct ethnl_req_info *req_base, | |
| struct netlink_ext_ack *extack) | |
| { | |
| struct phy_req_info *req_info = PHY_REQINFO(req_base); | |
| struct phy_device_node *pdn = req_info->pdn; | |
| struct phy_device *phydev = pdn->phy; | |
| size_t size = 0; | |
| ASSERT_RTNL(); | |
| /* ETHTOOL_A_PHY_INDEX */ | |
| size += nla_total_size(sizeof(u32)); | |
| /* ETHTOOL_A_DRVNAME */ | |
| if (phydev->drv) | |
| size += nla_total_size(strlen(phydev->drv->name) + 1); | |
| /* ETHTOOL_A_NAME */ | |
| size += nla_total_size(strlen(dev_name(&phydev->mdio.dev)) + 1); | |
| /* ETHTOOL_A_PHY_UPSTREAM_TYPE */ | |
| size += nla_total_size(sizeof(u32)); | |
| if (phy_on_sfp(phydev)) { | |
| const char *upstream_sfp_name = sfp_get_name(pdn->parent_sfp_bus); | |
| /* ETHTOOL_A_PHY_UPSTREAM_SFP_NAME */ | |
| if (upstream_sfp_name) | |
| size += nla_total_size(strlen(upstream_sfp_name) + 1); | |
| /* ETHTOOL_A_PHY_UPSTREAM_INDEX */ | |
| size += nla_total_size(sizeof(u32)); | |
| } | |
| /* ETHTOOL_A_PHY_DOWNSTREAM_SFP_NAME */ | |
| if (phydev->sfp_bus) { | |
| const char *sfp_name = sfp_get_name(phydev->sfp_bus); | |
| if (sfp_name) | |
| size += nla_total_size(strlen(sfp_name) + 1); | |
| } | |
| return size; | |
| } | |
| static int | |
| ethnl_phy_fill_reply(const struct ethnl_req_info *req_base, struct sk_buff *skb) | |
| { | |
| struct phy_req_info *req_info = PHY_REQINFO(req_base); | |
| struct phy_device_node *pdn = req_info->pdn; | |
| struct phy_device *phydev = pdn->phy; | |
| enum phy_upstream ptype; | |
| ptype = pdn->upstream_type; | |
| if (nla_put_u32(skb, ETHTOOL_A_PHY_INDEX, phydev->phyindex) || | |
| nla_put_string(skb, ETHTOOL_A_PHY_NAME, dev_name(&phydev->mdio.dev)) || | |
| nla_put_u32(skb, ETHTOOL_A_PHY_UPSTREAM_TYPE, ptype)) | |
| return -EMSGSIZE; | |
| if (phydev->drv && | |
| nla_put_string(skb, ETHTOOL_A_PHY_DRVNAME, phydev->drv->name)) | |
| return -EMSGSIZE; | |
| if (ptype == PHY_UPSTREAM_PHY) { | |
| struct phy_device *upstream = pdn->upstream.phydev; | |
| const char *sfp_upstream_name; | |
| /* Parent index */ | |
| if (nla_put_u32(skb, ETHTOOL_A_PHY_UPSTREAM_INDEX, upstream->phyindex)) | |
| return -EMSGSIZE; | |
| if (pdn->parent_sfp_bus) { | |
| sfp_upstream_name = sfp_get_name(pdn->parent_sfp_bus); | |
| if (sfp_upstream_name && | |
| nla_put_string(skb, ETHTOOL_A_PHY_UPSTREAM_SFP_NAME, | |
| sfp_upstream_name)) | |
| return -EMSGSIZE; | |
| } | |
| } | |
| if (phydev->sfp_bus) { | |
| const char *sfp_name = sfp_get_name(phydev->sfp_bus); | |
| if (sfp_name && | |
| nla_put_string(skb, ETHTOOL_A_PHY_DOWNSTREAM_SFP_NAME, | |
| sfp_name)) | |
| return -EMSGSIZE; | |
| } | |
| return 0; | |
| } | |
| static int ethnl_phy_parse_request(struct ethnl_req_info *req_base, | |
| struct nlattr **tb, | |
| struct netlink_ext_ack *extack) | |
| { | |
| struct phy_link_topology *topo = req_base->dev->link_topo; | |
| struct phy_req_info *req_info = PHY_REQINFO(req_base); | |
| struct phy_device *phydev; | |
| phydev = ethnl_req_get_phydev(req_base, tb[ETHTOOL_A_PHY_HEADER], | |
| extack); | |
| if (!phydev) | |
| return 0; | |
| if (IS_ERR(phydev)) | |
| return PTR_ERR(phydev); | |
| if (!topo) | |
| return 0; | |
| req_info->pdn = xa_load(&topo->phys, phydev->phyindex); | |
| return 0; | |
| } | |
| int ethnl_phy_doit(struct sk_buff *skb, struct genl_info *info) | |
| { | |
| struct phy_req_info req_info = {}; | |
| struct nlattr **tb = info->attrs; | |
| struct sk_buff *rskb; | |
| void *reply_payload; | |
| int reply_len; | |
| int ret; | |
| ret = ethnl_parse_header_dev_get(&req_info.base, | |
| tb[ETHTOOL_A_PHY_HEADER], | |
| genl_info_net(info), info->extack, | |
| true); | |
| if (ret < 0) | |
| return ret; | |
| rtnl_lock(); | |
| ret = ethnl_phy_parse_request(&req_info.base, tb, info->extack); | |
| if (ret < 0) | |
| goto err_unlock_rtnl; | |
| /* No PHY, return early */ | |
| if (!req_info.pdn) | |
| goto err_unlock_rtnl; | |
| ret = ethnl_phy_reply_size(&req_info.base, info->extack); | |
| if (ret < 0) | |
| goto err_unlock_rtnl; | |
| reply_len = ret + ethnl_reply_header_size(); | |
| rskb = ethnl_reply_init(reply_len, req_info.base.dev, | |
| ETHTOOL_MSG_PHY_GET_REPLY, | |
| ETHTOOL_A_PHY_HEADER, | |
| info, &reply_payload); | |
| if (!rskb) { | |
| ret = -ENOMEM; | |
| goto err_unlock_rtnl; | |
| } | |
| ret = ethnl_phy_fill_reply(&req_info.base, rskb); | |
| if (ret) | |
| goto err_free_msg; | |
| rtnl_unlock(); | |
| ethnl_parse_header_dev_put(&req_info.base); | |
| genlmsg_end(rskb, reply_payload); | |
| return genlmsg_reply(rskb, info); | |
| err_free_msg: | |
| nlmsg_free(rskb); | |
| err_unlock_rtnl: | |
| rtnl_unlock(); | |
| ethnl_parse_header_dev_put(&req_info.base); | |
| return ret; | |
| } | |
| struct ethnl_phy_dump_ctx { | |
| struct phy_req_info *phy_req_info; | |
| unsigned long ifindex; | |
| unsigned long phy_index; | |
| }; | |
| int ethnl_phy_start(struct netlink_callback *cb) | |
| { | |
| const struct genl_info *info = genl_info_dump(cb); | |
| struct ethnl_phy_dump_ctx *ctx = (void *)cb->ctx; | |
| int ret; | |
| BUILD_BUG_ON(sizeof(*ctx) > sizeof(cb->ctx)); | |
| ctx->phy_req_info = kzalloc(sizeof(*ctx->phy_req_info), GFP_KERNEL); | |
| if (!ctx->phy_req_info) | |
| return -ENOMEM; | |
| ret = ethnl_parse_header_dev_get(&ctx->phy_req_info->base, | |
| info->attrs[ETHTOOL_A_PHY_HEADER], | |
| sock_net(cb->skb->sk), cb->extack, | |
| false); | |
| ctx->ifindex = 0; | |
| ctx->phy_index = 0; | |
| if (ret) | |
| kfree(ctx->phy_req_info); | |
| return ret; | |
| } | |
| int ethnl_phy_done(struct netlink_callback *cb) | |
| { | |
| struct ethnl_phy_dump_ctx *ctx = (void *)cb->ctx; | |
| if (ctx->phy_req_info->base.dev) | |
| ethnl_parse_header_dev_put(&ctx->phy_req_info->base); | |
| kfree(ctx->phy_req_info); | |
| return 0; | |
| } | |
| static int ethnl_phy_dump_one_dev(struct sk_buff *skb, struct net_device *dev, | |
| struct netlink_callback *cb) | |
| { | |
| struct ethnl_phy_dump_ctx *ctx = (void *)cb->ctx; | |
| struct phy_req_info *pri = ctx->phy_req_info; | |
| struct phy_device_node *pdn; | |
| int ret = 0; | |
| void *ehdr; | |
| if (!dev->link_topo) | |
| return 0; | |
| xa_for_each_start(&dev->link_topo->phys, ctx->phy_index, pdn, ctx->phy_index) { | |
| ehdr = ethnl_dump_put(skb, cb, ETHTOOL_MSG_PHY_GET_REPLY); | |
| if (!ehdr) { | |
| ret = -EMSGSIZE; | |
| break; | |
| } | |
| ret = ethnl_fill_reply_header(skb, dev, ETHTOOL_A_PHY_HEADER); | |
| if (ret < 0) { | |
| genlmsg_cancel(skb, ehdr); | |
| break; | |
| } | |
| pri->pdn = pdn; | |
| ret = ethnl_phy_fill_reply(&pri->base, skb); | |
| if (ret < 0) { | |
| genlmsg_cancel(skb, ehdr); | |
| break; | |
| } | |
| genlmsg_end(skb, ehdr); | |
| } | |
| return ret; | |
| } | |
| int ethnl_phy_dumpit(struct sk_buff *skb, struct netlink_callback *cb) | |
| { | |
| struct ethnl_phy_dump_ctx *ctx = (void *)cb->ctx; | |
| struct net *net = sock_net(skb->sk); | |
| struct net_device *dev; | |
| int ret = 0; | |
| rtnl_lock(); | |
| if (ctx->phy_req_info->base.dev) { | |
| ret = ethnl_phy_dump_one_dev(skb, ctx->phy_req_info->base.dev, cb); | |
| } else { | |
| for_each_netdev_dump(net, dev, ctx->ifindex) { | |
| ret = ethnl_phy_dump_one_dev(skb, dev, cb); | |
| if (ret) | |
| break; | |
| ctx->phy_index = 0; | |
| } | |
| } | |
| rtnl_unlock(); | |
| return ret; | |
| } |