| // 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; |
| } |