| // SPDX-License-Identifier: GPL-2.0-only |
| |
| #include <linux/net_tstamp.h> |
| #include <linux/ptp_clock_kernel.h> |
| |
| #include "netlink.h" |
| #include "common.h" |
| #include "bitset.h" |
| #include "../core/dev.h" |
| #include "ts.h" |
| |
| struct tsconfig_req_info { |
| struct ethnl_req_info base; |
| }; |
| |
| struct tsconfig_reply_data { |
| struct ethnl_reply_data base; |
| struct hwtstamp_provider_desc hwprov_desc; |
| struct { |
| u32 tx_type; |
| u32 rx_filter; |
| u32 flags; |
| } hwtst_config; |
| }; |
| |
| #define TSCONFIG_REPDATA(__reply_base) \ |
| container_of(__reply_base, struct tsconfig_reply_data, base) |
| |
| const struct nla_policy ethnl_tsconfig_get_policy[ETHTOOL_A_TSCONFIG_HEADER + 1] = { |
| [ETHTOOL_A_TSCONFIG_HEADER] = |
| NLA_POLICY_NESTED(ethnl_header_policy), |
| }; |
| |
| static int tsconfig_prepare_data(const struct ethnl_req_info *req_base, |
| struct ethnl_reply_data *reply_base, |
| const struct genl_info *info) |
| { |
| struct tsconfig_reply_data *data = TSCONFIG_REPDATA(reply_base); |
| struct hwtstamp_provider *hwprov = NULL; |
| struct net_device *dev = reply_base->dev; |
| struct kernel_hwtstamp_config cfg = {}; |
| int ret; |
| |
| if (!dev->netdev_ops->ndo_hwtstamp_get) |
| return -EOPNOTSUPP; |
| |
| ret = ethnl_ops_begin(dev); |
| if (ret < 0) |
| return ret; |
| |
| ret = dev_get_hwtstamp_phylib(dev, &cfg); |
| if (ret) |
| goto out; |
| |
| data->hwtst_config.tx_type = BIT(cfg.tx_type); |
| data->hwtst_config.rx_filter = BIT(cfg.rx_filter); |
| data->hwtst_config.flags = cfg.flags; |
| |
| data->hwprov_desc.index = -1; |
| hwprov = rtnl_dereference(dev->hwprov); |
| if (hwprov) { |
| data->hwprov_desc.index = hwprov->desc.index; |
| data->hwprov_desc.qualifier = hwprov->desc.qualifier; |
| } else { |
| struct kernel_ethtool_ts_info ts_info = {}; |
| |
| ts_info.phc_index = -1; |
| ret = __ethtool_get_ts_info(dev, &ts_info); |
| if (ret) |
| goto out; |
| |
| if (ts_info.phc_index == -1) |
| return -ENODEV; |
| |
| data->hwprov_desc.index = ts_info.phc_index; |
| data->hwprov_desc.qualifier = ts_info.phc_qualifier; |
| } |
| |
| out: |
| ethnl_ops_complete(dev); |
| return ret; |
| } |
| |
| static int tsconfig_reply_size(const struct ethnl_req_info *req_base, |
| const struct ethnl_reply_data *reply_base) |
| { |
| const struct tsconfig_reply_data *data = TSCONFIG_REPDATA(reply_base); |
| bool compact = req_base->flags & ETHTOOL_FLAG_COMPACT_BITSETS; |
| int len = 0; |
| int ret; |
| |
| BUILD_BUG_ON(__HWTSTAMP_TX_CNT > 32); |
| BUILD_BUG_ON(__HWTSTAMP_FILTER_CNT > 32); |
| BUILD_BUG_ON(__HWTSTAMP_FLAG_CNT > 32); |
| |
| if (data->hwtst_config.flags) { |
| ret = ethnl_bitset32_size(&data->hwtst_config.flags, |
| NULL, __HWTSTAMP_FLAG_CNT, |
| ts_flags_names, compact); |
| if (ret < 0) |
| return ret; |
| len += ret; /* _TSCONFIG_HWTSTAMP_FLAGS */ |
| } |
| |
| if (data->hwtst_config.tx_type) { |
| ret = ethnl_bitset32_size(&data->hwtst_config.tx_type, |
| NULL, __HWTSTAMP_TX_CNT, |
| ts_tx_type_names, compact); |
| if (ret < 0) |
| return ret; |
| len += ret; /* _TSCONFIG_TX_TYPES */ |
| } |
| if (data->hwtst_config.rx_filter) { |
| ret = ethnl_bitset32_size(&data->hwtst_config.rx_filter, |
| NULL, __HWTSTAMP_FILTER_CNT, |
| ts_rx_filter_names, compact); |
| if (ret < 0) |
| return ret; |
| len += ret; /* _TSCONFIG_RX_FILTERS */ |
| } |
| |
| if (data->hwprov_desc.index >= 0) |
| /* _TSCONFIG_HWTSTAMP_PROVIDER */ |
| len += nla_total_size(0) + |
| 2 * nla_total_size(sizeof(u32)); |
| |
| return len; |
| } |
| |
| static int tsconfig_fill_reply(struct sk_buff *skb, |
| const struct ethnl_req_info *req_base, |
| const struct ethnl_reply_data *reply_base) |
| { |
| const struct tsconfig_reply_data *data = TSCONFIG_REPDATA(reply_base); |
| bool compact = req_base->flags & ETHTOOL_FLAG_COMPACT_BITSETS; |
| int ret; |
| |
| if (data->hwtst_config.flags) { |
| ret = ethnl_put_bitset32(skb, ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS, |
| &data->hwtst_config.flags, NULL, |
| __HWTSTAMP_FLAG_CNT, |
| ts_flags_names, compact); |
| if (ret < 0) |
| return ret; |
| } |
| |
| if (data->hwtst_config.tx_type) { |
| ret = ethnl_put_bitset32(skb, ETHTOOL_A_TSCONFIG_TX_TYPES, |
| &data->hwtst_config.tx_type, NULL, |
| __HWTSTAMP_TX_CNT, |
| ts_tx_type_names, compact); |
| if (ret < 0) |
| return ret; |
| } |
| |
| if (data->hwtst_config.rx_filter) { |
| ret = ethnl_put_bitset32(skb, ETHTOOL_A_TSCONFIG_RX_FILTERS, |
| &data->hwtst_config.rx_filter, |
| NULL, __HWTSTAMP_FILTER_CNT, |
| ts_rx_filter_names, compact); |
| if (ret < 0) |
| return ret; |
| } |
| |
| if (data->hwprov_desc.index >= 0) { |
| struct nlattr *nest; |
| |
| nest = nla_nest_start(skb, ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER); |
| if (!nest) |
| return -EMSGSIZE; |
| |
| if (nla_put_u32(skb, ETHTOOL_A_TS_HWTSTAMP_PROVIDER_INDEX, |
| data->hwprov_desc.index) || |
| nla_put_u32(skb, |
| ETHTOOL_A_TS_HWTSTAMP_PROVIDER_QUALIFIER, |
| data->hwprov_desc.qualifier)) { |
| nla_nest_cancel(skb, nest); |
| return -EMSGSIZE; |
| } |
| |
| nla_nest_end(skb, nest); |
| } |
| return 0; |
| } |
| |
| /* TSCONFIG_SET */ |
| const struct nla_policy ethnl_tsconfig_set_policy[ETHTOOL_A_TSCONFIG_MAX + 1] = { |
| [ETHTOOL_A_TSCONFIG_HEADER] = NLA_POLICY_NESTED(ethnl_header_policy), |
| [ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER] = |
| NLA_POLICY_NESTED(ethnl_ts_hwtst_prov_policy), |
| [ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS] = { .type = NLA_NESTED }, |
| [ETHTOOL_A_TSCONFIG_RX_FILTERS] = { .type = NLA_NESTED }, |
| [ETHTOOL_A_TSCONFIG_TX_TYPES] = { .type = NLA_NESTED }, |
| }; |
| |
| static int tsconfig_send_reply(struct net_device *dev, struct genl_info *info) |
| { |
| struct tsconfig_reply_data *reply_data; |
| struct tsconfig_req_info *req_info; |
| struct sk_buff *rskb; |
| void *reply_payload; |
| int reply_len = 0; |
| int ret; |
| |
| req_info = kzalloc(sizeof(*req_info), GFP_KERNEL); |
| if (!req_info) |
| return -ENOMEM; |
| reply_data = kmalloc(sizeof(*reply_data), GFP_KERNEL); |
| if (!reply_data) { |
| kfree(req_info); |
| return -ENOMEM; |
| } |
| |
| ASSERT_RTNL(); |
| reply_data->base.dev = dev; |
| ret = tsconfig_prepare_data(&req_info->base, &reply_data->base, info); |
| if (ret < 0) |
| goto err_cleanup; |
| |
| ret = tsconfig_reply_size(&req_info->base, &reply_data->base); |
| if (ret < 0) |
| goto err_cleanup; |
| |
| reply_len = ret + ethnl_reply_header_size(); |
| rskb = ethnl_reply_init(reply_len, dev, ETHTOOL_MSG_TSCONFIG_SET_REPLY, |
| ETHTOOL_A_TSCONFIG_HEADER, info, &reply_payload); |
| if (!rskb) |
| goto err_cleanup; |
| |
| ret = tsconfig_fill_reply(rskb, &req_info->base, &reply_data->base); |
| if (ret < 0) |
| goto err_cleanup; |
| |
| genlmsg_end(rskb, reply_payload); |
| ret = genlmsg_reply(rskb, info); |
| |
| err_cleanup: |
| kfree(reply_data); |
| kfree(req_info); |
| return ret; |
| } |
| |
| static int ethnl_set_tsconfig_validate(struct ethnl_req_info *req_base, |
| struct genl_info *info) |
| { |
| const struct net_device_ops *ops = req_base->dev->netdev_ops; |
| |
| if (!ops->ndo_hwtstamp_set || !ops->ndo_hwtstamp_get) |
| return -EOPNOTSUPP; |
| |
| return 1; |
| } |
| |
| static struct hwtstamp_provider * |
| tsconfig_set_hwprov_from_desc(struct net_device *dev, |
| struct genl_info *info, |
| struct hwtstamp_provider_desc *hwprov_desc) |
| { |
| struct kernel_ethtool_ts_info ts_info; |
| struct hwtstamp_provider *hwprov; |
| struct nlattr **tb = info->attrs; |
| struct phy_device *phy = NULL; |
| enum hwtstamp_source source; |
| int ret; |
| |
| ret = ethtool_net_get_ts_info_by_phc(dev, &ts_info, hwprov_desc); |
| if (!ret) { |
| /* Found */ |
| source = HWTSTAMP_SOURCE_NETDEV; |
| } else { |
| phy = ethtool_phy_get_ts_info_by_phc(dev, &ts_info, hwprov_desc); |
| if (IS_ERR(phy)) { |
| if (PTR_ERR(phy) == -ENODEV) |
| NL_SET_ERR_MSG_ATTR(info->extack, |
| tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER], |
| "phc not in this net device topology"); |
| return ERR_CAST(phy); |
| } |
| |
| source = HWTSTAMP_SOURCE_PHYLIB; |
| } |
| |
| hwprov = kzalloc(sizeof(*hwprov), GFP_KERNEL); |
| if (!hwprov) |
| return ERR_PTR(-ENOMEM); |
| |
| hwprov->desc.index = hwprov_desc->index; |
| hwprov->desc.qualifier = hwprov_desc->qualifier; |
| hwprov->source = source; |
| hwprov->phydev = phy; |
| |
| return hwprov; |
| } |
| |
| static int ethnl_set_tsconfig(struct ethnl_req_info *req_base, |
| struct genl_info *info) |
| { |
| struct kernel_hwtstamp_config hwtst_config = {0}; |
| bool hwprov_mod = false, config_mod = false; |
| struct hwtstamp_provider *hwprov = NULL; |
| struct net_device *dev = req_base->dev; |
| struct nlattr **tb = info->attrs; |
| int ret; |
| |
| BUILD_BUG_ON(__HWTSTAMP_TX_CNT >= 32); |
| BUILD_BUG_ON(__HWTSTAMP_FILTER_CNT >= 32); |
| BUILD_BUG_ON(__HWTSTAMP_FLAG_CNT > 32); |
| |
| if (!netif_device_present(dev)) |
| return -ENODEV; |
| |
| if (tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER]) { |
| struct hwtstamp_provider_desc __hwprov_desc = {.index = -1}; |
| struct hwtstamp_provider *__hwprov; |
| |
| __hwprov = rtnl_dereference(dev->hwprov); |
| if (__hwprov) { |
| __hwprov_desc.index = __hwprov->desc.index; |
| __hwprov_desc.qualifier = __hwprov->desc.qualifier; |
| } |
| |
| ret = ts_parse_hwtst_provider(tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER], |
| &__hwprov_desc, info->extack, |
| &hwprov_mod); |
| if (ret < 0) |
| return ret; |
| |
| if (hwprov_mod) { |
| hwprov = tsconfig_set_hwprov_from_desc(dev, info, |
| &__hwprov_desc); |
| if (IS_ERR(hwprov)) |
| return PTR_ERR(hwprov); |
| } |
| } |
| |
| /* Get current hwtstamp config if we are not changing the |
| * hwtstamp source. It will be zeroed in the other case. |
| */ |
| if (!hwprov_mod) { |
| ret = dev_get_hwtstamp_phylib(dev, &hwtst_config); |
| if (ret < 0 && ret != -EOPNOTSUPP) |
| goto err_free_hwprov; |
| } |
| |
| /* Get the hwtstamp config from netlink */ |
| if (tb[ETHTOOL_A_TSCONFIG_TX_TYPES]) { |
| u32 req_tx_type; |
| |
| req_tx_type = BIT(hwtst_config.tx_type); |
| ret = ethnl_update_bitset32(&req_tx_type, |
| __HWTSTAMP_TX_CNT, |
| tb[ETHTOOL_A_TSCONFIG_TX_TYPES], |
| ts_tx_type_names, info->extack, |
| &config_mod); |
| if (ret < 0) |
| goto err_free_hwprov; |
| |
| /* Select only one tx type at a time */ |
| if (ffs(req_tx_type) != fls(req_tx_type)) { |
| ret = -EINVAL; |
| goto err_free_hwprov; |
| } |
| |
| hwtst_config.tx_type = ffs(req_tx_type) - 1; |
| } |
| |
| if (tb[ETHTOOL_A_TSCONFIG_RX_FILTERS]) { |
| u32 req_rx_filter; |
| |
| req_rx_filter = BIT(hwtst_config.rx_filter); |
| ret = ethnl_update_bitset32(&req_rx_filter, |
| __HWTSTAMP_FILTER_CNT, |
| tb[ETHTOOL_A_TSCONFIG_RX_FILTERS], |
| ts_rx_filter_names, info->extack, |
| &config_mod); |
| if (ret < 0) |
| goto err_free_hwprov; |
| |
| /* Select only one rx filter at a time */ |
| if (ffs(req_rx_filter) != fls(req_rx_filter)) { |
| ret = -EINVAL; |
| goto err_free_hwprov; |
| } |
| |
| hwtst_config.rx_filter = ffs(req_rx_filter) - 1; |
| } |
| |
| if (tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS]) { |
| ret = ethnl_update_bitset32(&hwtst_config.flags, |
| __HWTSTAMP_FLAG_CNT, |
| tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS], |
| ts_flags_names, info->extack, |
| &config_mod); |
| if (ret < 0) |
| goto err_free_hwprov; |
| } |
| |
| ret = net_hwtstamp_validate(&hwtst_config); |
| if (ret) |
| goto err_free_hwprov; |
| |
| if (hwprov_mod) { |
| struct kernel_hwtstamp_config zero_config = {0}; |
| struct hwtstamp_provider *__hwprov; |
| |
| /* Disable current time stamping if we try to enable |
| * another one |
| */ |
| ret = dev_set_hwtstamp_phylib(dev, &zero_config, info->extack); |
| if (ret < 0) |
| goto err_free_hwprov; |
| |
| /* Change the selected hwtstamp source */ |
| __hwprov = rcu_replace_pointer_rtnl(dev->hwprov, hwprov); |
| if (__hwprov) |
| kfree_rcu(__hwprov, rcu_head); |
| } |
| |
| if (config_mod) { |
| ret = dev_set_hwtstamp_phylib(dev, &hwtst_config, |
| info->extack); |
| if (ret < 0) |
| return ret; |
| } |
| |
| if (hwprov_mod || config_mod) { |
| ret = tsconfig_send_reply(dev, info); |
| if (ret && ret != -EOPNOTSUPP) { |
| NL_SET_ERR_MSG(info->extack, |
| "error while reading the new configuration set"); |
| return ret; |
| } |
| } |
| |
| /* tsconfig has no notification */ |
| return 0; |
| |
| err_free_hwprov: |
| kfree(hwprov); |
| |
| return ret; |
| } |
| |
| const struct ethnl_request_ops ethnl_tsconfig_request_ops = { |
| .request_cmd = ETHTOOL_MSG_TSCONFIG_GET, |
| .reply_cmd = ETHTOOL_MSG_TSCONFIG_GET_REPLY, |
| .hdr_attr = ETHTOOL_A_TSCONFIG_HEADER, |
| .req_info_size = sizeof(struct tsconfig_req_info), |
| .reply_data_size = sizeof(struct tsconfig_reply_data), |
| |
| .prepare_data = tsconfig_prepare_data, |
| .reply_size = tsconfig_reply_size, |
| .fill_reply = tsconfig_fill_reply, |
| |
| .set_validate = ethnl_set_tsconfig_validate, |
| .set = ethnl_set_tsconfig, |
| }; |