| // SPDX-License-Identifier: GPL-2.0 | |
| #include <linux/mutex.h> | |
| #include <linux/netdevice.h> | |
| #include <linux/xarray.h> | |
| #include <net/busy_poll.h> | |
| #include <net/net_debug.h> | |
| #include <net/netdev_rx_queue.h> | |
| #include <net/page_pool/helpers.h> | |
| #include <net/page_pool/types.h> | |
| #include <net/page_pool/memory_provider.h> | |
| #include <net/sock.h> | |
| #include "page_pool_priv.h" | |
| #include "netdev-genl-gen.h" | |
| static DEFINE_XARRAY_FLAGS(page_pools, XA_FLAGS_ALLOC1); | |
| /* Protects: page_pools, netdevice->page_pools, pool->p.napi, pool->slow.netdev, | |
| * pool->user. | |
| * Ordering: inside rtnl_lock | |
| */ | |
| DEFINE_MUTEX(page_pools_lock); | |
| /* Page pools are only reachable from user space (via netlink) if they are | |
| * linked to a netdev at creation time. Following page pool "visibility" | |
| * states are possible: | |
| * - normal | |
| * - user.list: linked to real netdev, netdev: real netdev | |
| * - orphaned - real netdev has disappeared | |
| * - user.list: linked to lo, netdev: lo | |
| * - invisible - either (a) created without netdev linking, (b) unlisted due | |
| * to error, or (c) the entire namespace which owned this pool disappeared | |
| * - user.list: unhashed, netdev: unknown | |
| */ | |
| typedef int (*pp_nl_fill_cb)(struct sk_buff *rsp, const struct page_pool *pool, | |
| const struct genl_info *info); | |
| static int | |
| netdev_nl_page_pool_get_do(struct genl_info *info, u32 id, pp_nl_fill_cb fill) | |
| { | |
| struct page_pool *pool; | |
| struct sk_buff *rsp; | |
| int err; | |
| mutex_lock(&page_pools_lock); | |
| pool = xa_load(&page_pools, id); | |
| if (!pool || hlist_unhashed(&pool->user.list) || | |
| !net_eq(dev_net(pool->slow.netdev), genl_info_net(info))) { | |
| err = -ENOENT; | |
| goto err_unlock; | |
| } | |
| rsp = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL); | |
| if (!rsp) { | |
| err = -ENOMEM; | |
| goto err_unlock; | |
| } | |
| err = fill(rsp, pool, info); | |
| if (err) | |
| goto err_free_msg; | |
| mutex_unlock(&page_pools_lock); | |
| return genlmsg_reply(rsp, info); | |
| err_free_msg: | |
| nlmsg_free(rsp); | |
| err_unlock: | |
| mutex_unlock(&page_pools_lock); | |
| return err; | |
| } | |
| struct page_pool_dump_cb { | |
| unsigned long ifindex; | |
| u32 pp_id; | |
| }; | |
| static int | |
| netdev_nl_page_pool_get_dump(struct sk_buff *skb, struct netlink_callback *cb, | |
| pp_nl_fill_cb fill, struct nlattr *ifindex_attr) | |
| { | |
| struct page_pool_dump_cb *state = (void *)cb->ctx; | |
| const struct genl_info *info = genl_info_dump(cb); | |
| struct net *net = sock_net(skb->sk); | |
| struct net_device *netdev; | |
| struct page_pool *pool; | |
| int err = 0; | |
| if (ifindex_attr) | |
| state->ifindex = nla_get_u32(ifindex_attr); | |
| rtnl_lock(); | |
| mutex_lock(&page_pools_lock); | |
| for_each_netdev_dump(net, netdev, state->ifindex) { | |
| /* Either the provided ifindex doesn't exist or done dumping */ | |
| if (ifindex_attr && | |
| netdev->ifindex != nla_get_u32(ifindex_attr)) | |
| break; | |
| hlist_for_each_entry(pool, &netdev->page_pools, user.list) { | |
| if (state->pp_id && state->pp_id < pool->user.id) | |
| continue; | |
| state->pp_id = pool->user.id; | |
| err = fill(skb, pool, info); | |
| if (err) | |
| goto out; | |
| } | |
| state->pp_id = 0; | |
| } | |
| out: | |
| mutex_unlock(&page_pools_lock); | |
| rtnl_unlock(); | |
| return err; | |
| } | |
| static int | |
| page_pool_nl_stats_fill(struct sk_buff *rsp, const struct page_pool *pool, | |
| const struct genl_info *info) | |
| { | |
| #ifdef CONFIG_PAGE_POOL_STATS | |
| struct page_pool_stats stats = {}; | |
| struct nlattr *nest; | |
| void *hdr; | |
| page_pool_get_stats(pool, &stats); | |
| hdr = genlmsg_iput(rsp, info); | |
| if (!hdr) | |
| return -EMSGSIZE; | |
| nest = nla_nest_start(rsp, NETDEV_A_PAGE_POOL_STATS_INFO); | |
| if (!nest) | |
| goto err_cancel_msg; | |
| if (nla_put_uint(rsp, NETDEV_A_PAGE_POOL_ID, pool->user.id) || | |
| (pool->slow.netdev->ifindex != LOOPBACK_IFINDEX && | |
| nla_put_u32(rsp, NETDEV_A_PAGE_POOL_IFINDEX, | |
| pool->slow.netdev->ifindex))) | |
| goto err_cancel_nest; | |
| nla_nest_end(rsp, nest); | |
| if (nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_FAST, | |
| stats.alloc_stats.fast) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_SLOW, | |
| stats.alloc_stats.slow) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_SLOW_HIGH_ORDER, | |
| stats.alloc_stats.slow_high_order) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_EMPTY, | |
| stats.alloc_stats.empty) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_REFILL, | |
| stats.alloc_stats.refill) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_ALLOC_WAIVE, | |
| stats.alloc_stats.waive) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_RECYCLE_CACHED, | |
| stats.recycle_stats.cached) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_RECYCLE_CACHE_FULL, | |
| stats.recycle_stats.cache_full) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_RECYCLE_RING, | |
| stats.recycle_stats.ring) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_RECYCLE_RING_FULL, | |
| stats.recycle_stats.ring_full) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_STATS_RECYCLE_RELEASED_REFCNT, | |
| stats.recycle_stats.released_refcnt)) | |
| goto err_cancel_msg; | |
| genlmsg_end(rsp, hdr); | |
| return 0; | |
| err_cancel_nest: | |
| nla_nest_cancel(rsp, nest); | |
| err_cancel_msg: | |
| genlmsg_cancel(rsp, hdr); | |
| return -EMSGSIZE; | |
| #else | |
| GENL_SET_ERR_MSG(info, "kernel built without CONFIG_PAGE_POOL_STATS"); | |
| return -EOPNOTSUPP; | |
| #endif | |
| } | |
| int netdev_nl_page_pool_stats_get_doit(struct sk_buff *skb, | |
| struct genl_info *info) | |
| { | |
| struct nlattr *tb[ARRAY_SIZE(netdev_page_pool_info_nl_policy)]; | |
| struct nlattr *nest; | |
| int err; | |
| u32 id; | |
| if (GENL_REQ_ATTR_CHECK(info, NETDEV_A_PAGE_POOL_STATS_INFO)) | |
| return -EINVAL; | |
| nest = info->attrs[NETDEV_A_PAGE_POOL_STATS_INFO]; | |
| err = nla_parse_nested(tb, ARRAY_SIZE(tb) - 1, nest, | |
| netdev_page_pool_info_nl_policy, | |
| info->extack); | |
| if (err) | |
| return err; | |
| if (NL_REQ_ATTR_CHECK(info->extack, nest, tb, NETDEV_A_PAGE_POOL_ID)) | |
| return -EINVAL; | |
| if (tb[NETDEV_A_PAGE_POOL_IFINDEX]) { | |
| NL_SET_ERR_MSG_ATTR(info->extack, | |
| tb[NETDEV_A_PAGE_POOL_IFINDEX], | |
| "selecting by ifindex not supported"); | |
| return -EINVAL; | |
| } | |
| id = nla_get_uint(tb[NETDEV_A_PAGE_POOL_ID]); | |
| return netdev_nl_page_pool_get_do(info, id, page_pool_nl_stats_fill); | |
| } | |
| static const struct netlink_range_validation page_pool_ifindex_range = { | |
| .min = 1ULL, | |
| .max = S32_MAX, | |
| }; | |
| static const struct nla_policy | |
| page_pool_stat_info_policy[NETDEV_A_PAGE_POOL_IFINDEX + 1] = { | |
| [NETDEV_A_PAGE_POOL_IFINDEX] = | |
| NLA_POLICY_FULL_RANGE(NLA_U32, &page_pool_ifindex_range), | |
| }; | |
| int netdev_nl_page_pool_stats_get_dumpit(struct sk_buff *skb, | |
| struct netlink_callback *cb) | |
| { | |
| struct nlattr *tb[ARRAY_SIZE(page_pool_stat_info_policy)]; | |
| const struct genl_info *info = genl_info_dump(cb); | |
| struct nlattr *ifindex_attr = NULL; | |
| if (info->attrs[NETDEV_A_PAGE_POOL_STATS_INFO]) { | |
| struct nlattr *nest; | |
| int err; | |
| nest = info->attrs[NETDEV_A_PAGE_POOL_STATS_INFO]; | |
| err = nla_parse_nested(tb, ARRAY_SIZE(tb) - 1, nest, | |
| page_pool_stat_info_policy, | |
| info->extack); | |
| if (err) | |
| return err; | |
| ifindex_attr = tb[NETDEV_A_PAGE_POOL_IFINDEX]; | |
| } | |
| return netdev_nl_page_pool_get_dump(skb, cb, page_pool_nl_stats_fill, | |
| ifindex_attr); | |
| } | |
| static int | |
| page_pool_nl_fill(struct sk_buff *rsp, const struct page_pool *pool, | |
| const struct genl_info *info) | |
| { | |
| size_t inflight, refsz; | |
| unsigned int napi_id; | |
| void *hdr; | |
| hdr = genlmsg_iput(rsp, info); | |
| if (!hdr) | |
| return -EMSGSIZE; | |
| if (nla_put_uint(rsp, NETDEV_A_PAGE_POOL_ID, pool->user.id)) | |
| goto err_cancel; | |
| if (pool->slow.netdev->ifindex != LOOPBACK_IFINDEX && | |
| nla_put_u32(rsp, NETDEV_A_PAGE_POOL_IFINDEX, | |
| pool->slow.netdev->ifindex)) | |
| goto err_cancel; | |
| napi_id = pool->p.napi ? READ_ONCE(pool->p.napi->napi_id) : 0; | |
| if (napi_id_valid(napi_id) && | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_NAPI_ID, napi_id)) | |
| goto err_cancel; | |
| inflight = page_pool_inflight(pool, false); | |
| refsz = PAGE_SIZE << pool->p.order; | |
| if (nla_put_uint(rsp, NETDEV_A_PAGE_POOL_INFLIGHT, inflight) || | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_INFLIGHT_MEM, | |
| inflight * refsz)) | |
| goto err_cancel; | |
| if (pool->user.detach_time && | |
| nla_put_uint(rsp, NETDEV_A_PAGE_POOL_DETACH_TIME, | |
| ktime_divns(pool->user.detach_time, NSEC_PER_SEC))) | |
| goto err_cancel; | |
| if (pool->mp_ops && pool->mp_ops->nl_fill(pool->mp_priv, rsp, NULL)) | |
| goto err_cancel; | |
| genlmsg_end(rsp, hdr); | |
| return 0; | |
| err_cancel: | |
| genlmsg_cancel(rsp, hdr); | |
| return -EMSGSIZE; | |
| } | |
| static void netdev_nl_page_pool_event(const struct page_pool *pool, u32 cmd) | |
| { | |
| struct genl_info info; | |
| struct sk_buff *ntf; | |
| struct net *net; | |
| lockdep_assert_held(&page_pools_lock); | |
| /* 'invisible' page pools don't matter */ | |
| if (hlist_unhashed(&pool->user.list)) | |
| return; | |
| net = dev_net(pool->slow.netdev); | |
| if (!genl_has_listeners(&netdev_nl_family, net, NETDEV_NLGRP_PAGE_POOL)) | |
| return; | |
| genl_info_init_ntf(&info, &netdev_nl_family, cmd); | |
| ntf = genlmsg_new(GENLMSG_DEFAULT_SIZE, GFP_KERNEL); | |
| if (!ntf) | |
| return; | |
| if (page_pool_nl_fill(ntf, pool, &info)) { | |
| nlmsg_free(ntf); | |
| return; | |
| } | |
| genlmsg_multicast_netns(&netdev_nl_family, net, ntf, | |
| 0, NETDEV_NLGRP_PAGE_POOL, GFP_KERNEL); | |
| } | |
| int netdev_nl_page_pool_get_doit(struct sk_buff *skb, struct genl_info *info) | |
| { | |
| u32 id; | |
| if (GENL_REQ_ATTR_CHECK(info, NETDEV_A_PAGE_POOL_ID)) | |
| return -EINVAL; | |
| id = nla_get_uint(info->attrs[NETDEV_A_PAGE_POOL_ID]); | |
| return netdev_nl_page_pool_get_do(info, id, page_pool_nl_fill); | |
| } | |
| int netdev_nl_page_pool_get_dumpit(struct sk_buff *skb, | |
| struct netlink_callback *cb) | |
| { | |
| const struct genl_info *info = genl_info_dump(cb); | |
| return netdev_nl_page_pool_get_dump(skb, cb, page_pool_nl_fill, | |
| info->attrs[NETDEV_A_PAGE_POOL_IFINDEX]); | |
| } | |
| int page_pool_list(struct page_pool *pool) | |
| { | |
| static u32 id_alloc_next; | |
| int err; | |
| mutex_lock(&page_pools_lock); | |
| err = xa_alloc_cyclic(&page_pools, &pool->user.id, pool, xa_limit_32b, | |
| &id_alloc_next, GFP_KERNEL); | |
| if (err < 0) | |
| goto err_unlock; | |
| INIT_HLIST_NODE(&pool->user.list); | |
| if (pool->slow.netdev) { | |
| hlist_add_head(&pool->user.list, | |
| &pool->slow.netdev->page_pools); | |
| netdev_nl_page_pool_event(pool, NETDEV_CMD_PAGE_POOL_ADD_NTF); | |
| } | |
| mutex_unlock(&page_pools_lock); | |
| return 0; | |
| err_unlock: | |
| mutex_unlock(&page_pools_lock); | |
| return err; | |
| } | |
| void page_pool_detached(struct page_pool *pool) | |
| { | |
| mutex_lock(&page_pools_lock); | |
| pool->user.detach_time = ktime_get_boottime(); | |
| netdev_nl_page_pool_event(pool, NETDEV_CMD_PAGE_POOL_CHANGE_NTF); | |
| mutex_unlock(&page_pools_lock); | |
| } | |
| void page_pool_unlist(struct page_pool *pool) | |
| { | |
| mutex_lock(&page_pools_lock); | |
| netdev_nl_page_pool_event(pool, NETDEV_CMD_PAGE_POOL_DEL_NTF); | |
| xa_erase(&page_pools, pool->user.id); | |
| if (!hlist_unhashed(&pool->user.list)) | |
| hlist_del(&pool->user.list); | |
| mutex_unlock(&page_pools_lock); | |
| } | |
| int page_pool_check_memory_provider(struct net_device *dev, | |
| struct netdev_rx_queue *rxq) | |
| { | |
| void *binding = rxq->mp_params.mp_priv; | |
| struct page_pool *pool; | |
| struct hlist_node *n; | |
| if (!binding) | |
| return 0; | |
| mutex_lock(&page_pools_lock); | |
| hlist_for_each_entry_safe(pool, n, &dev->page_pools, user.list) { | |
| if (pool->mp_priv != binding) | |
| continue; | |
| if (pool->slow.queue_idx == get_netdev_rx_queue_index(rxq)) { | |
| mutex_unlock(&page_pools_lock); | |
| return 0; | |
| } | |
| } | |
| mutex_unlock(&page_pools_lock); | |
| return -ENODATA; | |
| } | |
| static void page_pool_unreg_netdev_wipe(struct net_device *netdev) | |
| { | |
| struct page_pool *pool; | |
| struct hlist_node *n; | |
| mutex_lock(&page_pools_lock); | |
| hlist_for_each_entry_safe(pool, n, &netdev->page_pools, user.list) { | |
| hlist_del_init(&pool->user.list); | |
| pool->slow.netdev = NET_PTR_POISON; | |
| } | |
| mutex_unlock(&page_pools_lock); | |
| } | |
| static void page_pool_unreg_netdev(struct net_device *netdev) | |
| { | |
| struct page_pool *pool, *last; | |
| struct net_device *lo; | |
| lo = dev_net(netdev)->loopback_dev; | |
| mutex_lock(&page_pools_lock); | |
| last = NULL; | |
| hlist_for_each_entry(pool, &netdev->page_pools, user.list) { | |
| pool->slow.netdev = lo; | |
| netdev_nl_page_pool_event(pool, | |
| NETDEV_CMD_PAGE_POOL_CHANGE_NTF); | |
| last = pool; | |
| } | |
| if (last) | |
| hlist_splice_init(&netdev->page_pools, &last->user.list, | |
| &lo->page_pools); | |
| mutex_unlock(&page_pools_lock); | |
| } | |
| static int | |
| page_pool_netdevice_event(struct notifier_block *nb, | |
| unsigned long event, void *ptr) | |
| { | |
| struct net_device *netdev = netdev_notifier_info_to_dev(ptr); | |
| if (event != NETDEV_UNREGISTER) | |
| return NOTIFY_DONE; | |
| if (hlist_empty(&netdev->page_pools)) | |
| return NOTIFY_OK; | |
| if (netdev->ifindex != LOOPBACK_IFINDEX) | |
| page_pool_unreg_netdev(netdev); | |
| else | |
| page_pool_unreg_netdev_wipe(netdev); | |
| return NOTIFY_OK; | |
| } | |
| static struct notifier_block page_pool_netdevice_nb = { | |
| .notifier_call = page_pool_netdevice_event, | |
| }; | |
| static int __init page_pool_user_init(void) | |
| { | |
| return register_netdevice_notifier(&page_pool_netdevice_nb); | |
| } | |
| subsys_initcall(page_pool_user_init); |