Files
Nicolai Buchwitz 036778a4fa net: ethtool: let tsconfig reach a PHY-only timestamp provider
TSCONFIG_GET and TSCONFIG_SET reject a device that implements neither
hwtstamp NDO, even when its PHY can serve the request. The ioctls they
meant to replace handle it, so the two interfaces disagree on the same
hardware and user space has to pick one.

Accept the default timestamping PHY and an already installed provider on
both sides. On the set side the test moves into ethnl_set_tsconfig() as
the validate callback runs without rtnl. On the get side it stays ahead
of ethnl_ops_begin(), so a device that can serve nothing keeps failing
with EOPNOTSUPP and a dump still skips it rather than stopping there.

A netdev provider needs ndo_hwtstamp_set to be programmed at all, so
don't pick that source without it, and test for ndo_hwtstamp_get before
calling it.

Fixes: 6e9e2eed4f ("net: ethtool: Add support for tsconfig command to get/set hwtstamp config")
Signed-off-by: Nicolai Buchwitz <[email protected]>
Reviewed-by: Maxime Chevallier <[email protected]>
Reviewed-by: Kory Maincent <[email protected]>
Link: https://patch.msgid.link/[email protected]
Signed-off-by: Jakub Kicinski <[email protected]>
2026-09-28 19:26:41 -07:00

461 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
#include <linux/net_tstamp.h>
#include <linux/phy.h>
#include <linux/ptp_clock_kernel.h>
#include <net/netdev_lock.h>
#include "bitset.h"
#include "common.h"
#include "netlink.h"
#include "ts.h"
#include "../core/dev.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 &&
!phy_is_default_hwtstamp(dev->phydev) &&
!netdev_ops_lock_dereference(dev->hwprov, dev))
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 = netdev_ops_lock_dereference(dev->hwprov, dev);
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) {
ret = -ENODEV;
goto out;
}
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_obj(*req_info);
if (!req_info)
return -ENOMEM;
reply_data = kmalloc_obj(*reply_data);
if (!reply_data) {
kfree(req_info);
return -ENOMEM;
}
netdev_assert_locked_ops_compat(dev);
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) {
ret = -ENOMEM;
goto err_cleanup;
}
ret = tsconfig_fill_reply(rskb, &req_info->base, &reply_data->base);
if (ret < 0)
goto err_free_msg;
genlmsg_end(rskb, reply_payload);
ret = genlmsg_reply(rskb, info);
rskb = NULL;
err_free_msg:
nlmsg_free(rskb);
err_cleanup:
kfree(reply_data);
kfree(req_info);
return ret;
}
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 && dev->netdev_ops->ndo_hwtstamp_set) {
/* 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_obj(*hwprov);
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;
if (!netif_device_present(dev))
return -ENODEV;
if (!dev->netdev_ops->ndo_hwtstamp_set &&
!phy_is_default_hwtstamp(dev->phydev) &&
!netdev_ops_lock_dereference(dev->hwprov, dev))
return -EOPNOTSUPP;
if (tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER]) {
struct hwtstamp_provider_desc __hwprov_desc = {.index = -1};
struct hwtstamp_provider *__hwprov;
__hwprov = netdev_ops_lock_dereference(dev->hwprov, dev);
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 exactly one tx type at a time */
if (hweight32(req_tx_type) != 1) {
NL_SET_BAD_ATTR(info->extack,
tb[ETHTOOL_A_TSCONFIG_TX_TYPES]);
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 exactly one rx filter at a time */
if (hweight32(req_rx_filter) != 1) {
NL_SET_BAD_ATTR(info->extack,
tb[ETHTOOL_A_TSCONFIG_RX_FILTERS]);
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(dev->hwprov, hwprov,
netdev_is_locked_ops_compat(dev));
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;
}
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 = ethnl_set_tsconfig,
};