|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  |  | 
|  | #include <linux/ethtool.h> | 
|  | #include <linux/phy.h> | 
|  | #include "netlink.h" | 
|  | #include "common.h" | 
|  |  | 
|  | struct strset_info { | 
|  | bool per_dev; | 
|  | bool free_strings; | 
|  | unsigned int count; | 
|  | const char (*strings)[ETH_GSTRING_LEN]; | 
|  | }; | 
|  |  | 
|  | static const struct strset_info info_template[] = { | 
|  | [ETH_SS_TEST] = { | 
|  | .per_dev	= true, | 
|  | }, | 
|  | [ETH_SS_STATS] = { | 
|  | .per_dev	= true, | 
|  | }, | 
|  | [ETH_SS_PRIV_FLAGS] = { | 
|  | .per_dev	= true, | 
|  | }, | 
|  | [ETH_SS_FEATURES] = { | 
|  | .per_dev	= false, | 
|  | .count		= ARRAY_SIZE(netdev_features_strings), | 
|  | .strings	= netdev_features_strings, | 
|  | }, | 
|  | [ETH_SS_RSS_HASH_FUNCS] = { | 
|  | .per_dev	= false, | 
|  | .count		= ARRAY_SIZE(rss_hash_func_strings), | 
|  | .strings	= rss_hash_func_strings, | 
|  | }, | 
|  | [ETH_SS_TUNABLES] = { | 
|  | .per_dev	= false, | 
|  | .count		= ARRAY_SIZE(tunable_strings), | 
|  | .strings	= tunable_strings, | 
|  | }, | 
|  | [ETH_SS_PHY_STATS] = { | 
|  | .per_dev	= true, | 
|  | }, | 
|  | [ETH_SS_PHY_TUNABLES] = { | 
|  | .per_dev	= false, | 
|  | .count		= ARRAY_SIZE(phy_tunable_strings), | 
|  | .strings	= phy_tunable_strings, | 
|  | }, | 
|  | [ETH_SS_LINK_MODES] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_LINK_MODE_MASK_NBITS, | 
|  | .strings	= link_mode_names, | 
|  | }, | 
|  | [ETH_SS_MSG_CLASSES] = { | 
|  | .per_dev	= false, | 
|  | .count		= NETIF_MSG_CLASS_COUNT, | 
|  | .strings	= netif_msg_class_names, | 
|  | }, | 
|  | [ETH_SS_WOL_MODES] = { | 
|  | .per_dev	= false, | 
|  | .count		= WOL_MODE_COUNT, | 
|  | .strings	= wol_mode_names, | 
|  | }, | 
|  | [ETH_SS_SOF_TIMESTAMPING] = { | 
|  | .per_dev	= false, | 
|  | .count		= __SOF_TIMESTAMPING_CNT, | 
|  | .strings	= sof_timestamping_names, | 
|  | }, | 
|  | [ETH_SS_TS_TX_TYPES] = { | 
|  | .per_dev	= false, | 
|  | .count		= __HWTSTAMP_TX_CNT, | 
|  | .strings	= ts_tx_type_names, | 
|  | }, | 
|  | [ETH_SS_TS_RX_FILTERS] = { | 
|  | .per_dev	= false, | 
|  | .count		= __HWTSTAMP_FILTER_CNT, | 
|  | .strings	= ts_rx_filter_names, | 
|  | }, | 
|  | [ETH_SS_TS_FLAGS] = { | 
|  | .per_dev	= false, | 
|  | .count		= __HWTSTAMP_FLAG_CNT, | 
|  | .strings	= ts_flags_names, | 
|  | }, | 
|  | [ETH_SS_UDP_TUNNEL_TYPES] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_UDP_TUNNEL_TYPE_CNT, | 
|  | .strings	= udp_tunnel_type_names, | 
|  | }, | 
|  | [ETH_SS_STATS_STD] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_STATS_CNT, | 
|  | .strings	= stats_std_names, | 
|  | }, | 
|  | [ETH_SS_STATS_ETH_PHY] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_A_STATS_ETH_PHY_CNT, | 
|  | .strings	= stats_eth_phy_names, | 
|  | }, | 
|  | [ETH_SS_STATS_ETH_MAC] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_A_STATS_ETH_MAC_CNT, | 
|  | .strings	= stats_eth_mac_names, | 
|  | }, | 
|  | [ETH_SS_STATS_ETH_CTRL] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_A_STATS_ETH_CTRL_CNT, | 
|  | .strings	= stats_eth_ctrl_names, | 
|  | }, | 
|  | [ETH_SS_STATS_RMON] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_A_STATS_RMON_CNT, | 
|  | .strings	= stats_rmon_names, | 
|  | }, | 
|  | [ETH_SS_STATS_PHY] = { | 
|  | .per_dev	= false, | 
|  | .count		= __ETHTOOL_A_STATS_PHY_CNT, | 
|  | .strings	= stats_phy_names, | 
|  | }, | 
|  | }; | 
|  |  | 
|  | struct strset_req_info { | 
|  | struct ethnl_req_info		base; | 
|  | u32				req_ids; | 
|  | bool				counts_only; | 
|  | }; | 
|  |  | 
|  | #define STRSET_REQINFO(__req_base) \ | 
|  | container_of(__req_base, struct strset_req_info, base) | 
|  |  | 
|  | struct strset_reply_data { | 
|  | struct ethnl_reply_data		base; | 
|  | struct strset_info		sets[ETH_SS_COUNT]; | 
|  | }; | 
|  |  | 
|  | #define STRSET_REPDATA(__reply_base) \ | 
|  | container_of(__reply_base, struct strset_reply_data, base) | 
|  |  | 
|  | const struct nla_policy ethnl_strset_get_policy[] = { | 
|  | [ETHTOOL_A_STRSET_HEADER]	= | 
|  | NLA_POLICY_NESTED(ethnl_header_policy_phy), | 
|  | [ETHTOOL_A_STRSET_STRINGSETS]	= { .type = NLA_NESTED }, | 
|  | [ETHTOOL_A_STRSET_COUNTS_ONLY]	= { .type = NLA_FLAG }, | 
|  | }; | 
|  |  | 
|  | static const struct nla_policy get_stringset_policy[] = { | 
|  | [ETHTOOL_A_STRINGSET_ID]	= { .type = NLA_U32 }, | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * strset_include() - test if a string set should be included in reply | 
|  | * @info: parsed client request | 
|  | * @data: pointer to request data structure | 
|  | * @id:   id of string set to check (ETH_SS_* constants) | 
|  | */ | 
|  | static bool strset_include(const struct strset_req_info *info, | 
|  | const struct strset_reply_data *data, u32 id) | 
|  | { | 
|  | bool per_dev; | 
|  |  | 
|  | BUILD_BUG_ON(ETH_SS_COUNT >= BITS_PER_BYTE * sizeof(info->req_ids)); | 
|  |  | 
|  | if (info->req_ids) | 
|  | return info->req_ids & (1U << id); | 
|  | per_dev = data->sets[id].per_dev; | 
|  | if (!per_dev && !data->sets[id].strings) | 
|  | return false; | 
|  |  | 
|  | return data->base.dev ? per_dev : !per_dev; | 
|  | } | 
|  |  | 
|  | static int strset_get_id(const struct nlattr *nest, u32 *val, | 
|  | struct netlink_ext_ack *extack) | 
|  | { | 
|  | struct nlattr *tb[ARRAY_SIZE(get_stringset_policy)]; | 
|  | int ret; | 
|  |  | 
|  | ret = nla_parse_nested(tb, ARRAY_SIZE(get_stringset_policy) - 1, nest, | 
|  | get_stringset_policy, extack); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  | if (NL_REQ_ATTR_CHECK(extack, nest, tb, ETHTOOL_A_STRINGSET_ID)) | 
|  | return -EINVAL; | 
|  |  | 
|  | *val = nla_get_u32(tb[ETHTOOL_A_STRINGSET_ID]); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static const struct nla_policy strset_stringsets_policy[] = { | 
|  | [ETHTOOL_A_STRINGSETS_STRINGSET]	= { .type = NLA_NESTED }, | 
|  | }; | 
|  |  | 
|  | static int strset_parse_request(struct ethnl_req_info *req_base, | 
|  | struct nlattr **tb, | 
|  | struct netlink_ext_ack *extack) | 
|  | { | 
|  | struct strset_req_info *req_info = STRSET_REQINFO(req_base); | 
|  | struct nlattr *nest = tb[ETHTOOL_A_STRSET_STRINGSETS]; | 
|  | struct nlattr *attr; | 
|  | int rem, ret; | 
|  |  | 
|  | if (!nest) | 
|  | return 0; | 
|  | ret = nla_validate_nested(nest, | 
|  | ARRAY_SIZE(strset_stringsets_policy) - 1, | 
|  | strset_stringsets_policy, extack); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  |  | 
|  | req_info->counts_only = tb[ETHTOOL_A_STRSET_COUNTS_ONLY]; | 
|  | nla_for_each_nested(attr, nest, rem) { | 
|  | u32 id; | 
|  |  | 
|  | if (WARN_ONCE(nla_type(attr) != ETHTOOL_A_STRINGSETS_STRINGSET, | 
|  | "unexpected attrtype %u in ETHTOOL_A_STRSET_STRINGSETS\n", | 
|  | nla_type(attr))) | 
|  | return -EINVAL; | 
|  |  | 
|  | ret = strset_get_id(attr, &id, extack); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  | if (id >= ETH_SS_COUNT) { | 
|  | NL_SET_ERR_MSG_ATTR(extack, attr, | 
|  | "unknown string set id"); | 
|  | return -EOPNOTSUPP; | 
|  | } | 
|  |  | 
|  | req_info->req_ids |= (1U << id); | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static void strset_cleanup_data(struct ethnl_reply_data *reply_base) | 
|  | { | 
|  | struct strset_reply_data *data = STRSET_REPDATA(reply_base); | 
|  | unsigned int i; | 
|  |  | 
|  | for (i = 0; i < ETH_SS_COUNT; i++) | 
|  | if (data->sets[i].free_strings) { | 
|  | kfree(data->sets[i].strings); | 
|  | data->sets[i].strings = NULL; | 
|  | data->sets[i].free_strings = false; | 
|  | } | 
|  | } | 
|  |  | 
|  | static int strset_prepare_set(struct strset_info *info, struct net_device *dev, | 
|  | struct phy_device *phydev, unsigned int id, | 
|  | bool counts_only) | 
|  | { | 
|  | const struct ethtool_phy_ops *phy_ops = ethtool_phy_ops; | 
|  | const struct ethtool_ops *ops = dev->ethtool_ops; | 
|  | void *strings; | 
|  | int count, ret; | 
|  |  | 
|  | if (id == ETH_SS_PHY_STATS && phydev && | 
|  | !ops->get_ethtool_phy_stats && phy_ops && | 
|  | phy_ops->get_sset_count) | 
|  | ret = phy_ops->get_sset_count(phydev); | 
|  | else if (ops->get_sset_count && ops->get_strings) | 
|  | ret = ops->get_sset_count(dev, id); | 
|  | else | 
|  | ret = -EOPNOTSUPP; | 
|  | if (ret <= 0) { | 
|  | info->count = 0; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | count = ret; | 
|  | if (!counts_only) { | 
|  | strings = kcalloc(count, ETH_GSTRING_LEN, GFP_KERNEL); | 
|  | if (!strings) | 
|  | return -ENOMEM; | 
|  | if (id == ETH_SS_PHY_STATS && phydev && | 
|  | !ops->get_ethtool_phy_stats && phy_ops && | 
|  | phy_ops->get_strings) | 
|  | phy_ops->get_strings(phydev, strings); | 
|  | else | 
|  | ops->get_strings(dev, id, strings); | 
|  | info->strings = strings; | 
|  | info->free_strings = true; | 
|  | } | 
|  | info->count = count; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int strset_prepare_data(const struct ethnl_req_info *req_base, | 
|  | struct ethnl_reply_data *reply_base, | 
|  | const struct genl_info *info) | 
|  | { | 
|  | const struct strset_req_info *req_info = STRSET_REQINFO(req_base); | 
|  | struct strset_reply_data *data = STRSET_REPDATA(reply_base); | 
|  | struct net_device *dev = reply_base->dev; | 
|  | struct nlattr **tb = info->attrs; | 
|  | struct phy_device *phydev; | 
|  | unsigned int i; | 
|  | int ret; | 
|  |  | 
|  | BUILD_BUG_ON(ARRAY_SIZE(info_template) != ETH_SS_COUNT); | 
|  | memcpy(&data->sets, &info_template, sizeof(data->sets)); | 
|  |  | 
|  | if (!dev) { | 
|  | for (i = 0; i < ETH_SS_COUNT; i++) { | 
|  | if ((req_info->req_ids & (1U << i)) && | 
|  | data->sets[i].per_dev) { | 
|  | GENL_SET_ERR_MSG(info, "requested per device strings without dev"); | 
|  | return -EINVAL; | 
|  | } | 
|  | } | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | phydev = ethnl_req_get_phydev(req_base, tb, ETHTOOL_A_HEADER_FLAGS, | 
|  | info->extack); | 
|  |  | 
|  | /* phydev can be NULL, check for errors only */ | 
|  | if (IS_ERR(phydev)) | 
|  | return PTR_ERR(phydev); | 
|  |  | 
|  | ret = ethnl_ops_begin(dev); | 
|  | if (ret < 0) | 
|  | goto err_strset; | 
|  | for (i = 0; i < ETH_SS_COUNT; i++) { | 
|  | if (!strset_include(req_info, data, i) || | 
|  | !data->sets[i].per_dev) | 
|  | continue; | 
|  |  | 
|  | ret = strset_prepare_set(&data->sets[i], dev, phydev, i, | 
|  | req_info->counts_only); | 
|  | if (ret < 0) | 
|  | goto err_ops; | 
|  | } | 
|  | ethnl_ops_complete(dev); | 
|  |  | 
|  | return 0; | 
|  | err_ops: | 
|  | ethnl_ops_complete(dev); | 
|  | err_strset: | 
|  | strset_cleanup_data(reply_base); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | /* calculate size of ETHTOOL_A_STRSET_STRINGSET nest for one string set */ | 
|  | static int strset_set_size(const struct strset_info *info, bool counts_only) | 
|  | { | 
|  | unsigned int len = 0; | 
|  | unsigned int i; | 
|  |  | 
|  | if (info->count == 0) | 
|  | return 0; | 
|  | if (counts_only) | 
|  | return nla_total_size(2 * nla_total_size(sizeof(u32))); | 
|  |  | 
|  | for (i = 0; i < info->count; i++) { | 
|  | const char *str = info->strings[i]; | 
|  |  | 
|  | /* ETHTOOL_A_STRING_INDEX, ETHTOOL_A_STRING_VALUE, nest */ | 
|  | len += nla_total_size(nla_total_size(sizeof(u32)) + | 
|  | ethnl_strz_size(str)); | 
|  | } | 
|  | /* ETHTOOL_A_STRINGSET_ID, ETHTOOL_A_STRINGSET_COUNT */ | 
|  | len = 2 * nla_total_size(sizeof(u32)) + nla_total_size(len); | 
|  |  | 
|  | return nla_total_size(len); | 
|  | } | 
|  |  | 
|  | static int strset_reply_size(const struct ethnl_req_info *req_base, | 
|  | const struct ethnl_reply_data *reply_base) | 
|  | { | 
|  | const struct strset_req_info *req_info = STRSET_REQINFO(req_base); | 
|  | const struct strset_reply_data *data = STRSET_REPDATA(reply_base); | 
|  | unsigned int i; | 
|  | int len = 0; | 
|  | int ret; | 
|  |  | 
|  | len += nla_total_size(0); /* ETHTOOL_A_STRSET_STRINGSETS */ | 
|  |  | 
|  | for (i = 0; i < ETH_SS_COUNT; i++) { | 
|  | const struct strset_info *set_info = &data->sets[i]; | 
|  |  | 
|  | if (!strset_include(req_info, data, i)) | 
|  | continue; | 
|  |  | 
|  | ret = strset_set_size(set_info, req_info->counts_only); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  | len += ret; | 
|  | } | 
|  |  | 
|  | return len; | 
|  | } | 
|  |  | 
|  | /* fill one string into reply */ | 
|  | static int strset_fill_string(struct sk_buff *skb, | 
|  | const struct strset_info *set_info, u32 idx) | 
|  | { | 
|  | struct nlattr *string_attr; | 
|  | const char *value; | 
|  |  | 
|  | value = set_info->strings[idx]; | 
|  |  | 
|  | string_attr = nla_nest_start(skb, ETHTOOL_A_STRINGS_STRING); | 
|  | if (!string_attr) | 
|  | return -EMSGSIZE; | 
|  | if (nla_put_u32(skb, ETHTOOL_A_STRING_INDEX, idx) || | 
|  | ethnl_put_strz(skb, ETHTOOL_A_STRING_VALUE, value)) | 
|  | goto nla_put_failure; | 
|  | nla_nest_end(skb, string_attr); | 
|  |  | 
|  | return 0; | 
|  | nla_put_failure: | 
|  | nla_nest_cancel(skb, string_attr); | 
|  | return -EMSGSIZE; | 
|  | } | 
|  |  | 
|  | /* fill one string set into reply */ | 
|  | static int strset_fill_set(struct sk_buff *skb, | 
|  | const struct strset_info *set_info, u32 id, | 
|  | bool counts_only) | 
|  | { | 
|  | struct nlattr *stringset_attr; | 
|  | struct nlattr *strings_attr; | 
|  | unsigned int i; | 
|  |  | 
|  | if (!set_info->per_dev && !set_info->strings) | 
|  | return -EOPNOTSUPP; | 
|  | if (set_info->count == 0) | 
|  | return 0; | 
|  | stringset_attr = nla_nest_start(skb, ETHTOOL_A_STRINGSETS_STRINGSET); | 
|  | if (!stringset_attr) | 
|  | return -EMSGSIZE; | 
|  |  | 
|  | if (nla_put_u32(skb, ETHTOOL_A_STRINGSET_ID, id) || | 
|  | nla_put_u32(skb, ETHTOOL_A_STRINGSET_COUNT, set_info->count)) | 
|  | goto nla_put_failure; | 
|  |  | 
|  | if (!counts_only) { | 
|  | strings_attr = nla_nest_start(skb, ETHTOOL_A_STRINGSET_STRINGS); | 
|  | if (!strings_attr) | 
|  | goto nla_put_failure; | 
|  | for (i = 0; i < set_info->count; i++) { | 
|  | if (strset_fill_string(skb, set_info, i) < 0) | 
|  | goto nla_put_failure; | 
|  | } | 
|  | nla_nest_end(skb, strings_attr); | 
|  | } | 
|  |  | 
|  | nla_nest_end(skb, stringset_attr); | 
|  | return 0; | 
|  |  | 
|  | nla_put_failure: | 
|  | nla_nest_cancel(skb, stringset_attr); | 
|  | return -EMSGSIZE; | 
|  | } | 
|  |  | 
|  | static int strset_fill_reply(struct sk_buff *skb, | 
|  | const struct ethnl_req_info *req_base, | 
|  | const struct ethnl_reply_data *reply_base) | 
|  | { | 
|  | const struct strset_req_info *req_info = STRSET_REQINFO(req_base); | 
|  | const struct strset_reply_data *data = STRSET_REPDATA(reply_base); | 
|  | struct nlattr *nest; | 
|  | unsigned int i; | 
|  | int ret; | 
|  |  | 
|  | nest = nla_nest_start(skb, ETHTOOL_A_STRSET_STRINGSETS); | 
|  | if (!nest) | 
|  | return -EMSGSIZE; | 
|  |  | 
|  | for (i = 0; i < ETH_SS_COUNT; i++) { | 
|  | if (strset_include(req_info, data, i)) { | 
|  | ret = strset_fill_set(skb, &data->sets[i], i, | 
|  | req_info->counts_only); | 
|  | if (ret < 0) | 
|  | goto nla_put_failure; | 
|  | } | 
|  | } | 
|  |  | 
|  | nla_nest_end(skb, nest); | 
|  | return 0; | 
|  |  | 
|  | nla_put_failure: | 
|  | nla_nest_cancel(skb, nest); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | const struct ethnl_request_ops ethnl_strset_request_ops = { | 
|  | .request_cmd		= ETHTOOL_MSG_STRSET_GET, | 
|  | .reply_cmd		= ETHTOOL_MSG_STRSET_GET_REPLY, | 
|  | .hdr_attr		= ETHTOOL_A_STRSET_HEADER, | 
|  | .req_info_size		= sizeof(struct strset_req_info), | 
|  | .reply_data_size	= sizeof(struct strset_reply_data), | 
|  | .allow_nodev_do		= true, | 
|  |  | 
|  | .parse_request		= strset_parse_request, | 
|  | .prepare_data		= strset_prepare_data, | 
|  | .reply_size		= strset_reply_size, | 
|  | .fill_reply		= strset_fill_reply, | 
|  | .cleanup_data		= strset_cleanup_data, | 
|  | }; |