BPF内核函数(kfuncs)

1. 简介

BPF内核函数,更常被称为kfuncs,是Linux内核中暴露给BPF程序使用的函数。与普通的BPF辅助函数不同,kfuncs没有稳定的接口,并且可能在一个内核版本到另一个内核版本之间发生变化。因此,BPF程序需要根据内核中的更改进行更新。有关更多信息,请参见3. kfunc生命周期预期

2. 定义kfunc

有两种方法可以将内核函数暴露给BPF程序,一种是使内核中现有的函数可见,另一种是为BPF添加新的包装器。在这两种情况下,都必须注意BPF程序只能在有效的上下文中调用此类函数。为了强制执行此操作,kfunc的可见性可以是每个程序类型。

如果您没有为现有的内核函数创建BPF包装器,请跳到2.3 使用现有的内核函数

2.1 创建包装器kfunc

在定义包装器kfunc时,包装器函数应具有外部链接。这可以防止编译器优化掉死代码,因为此包装器kfunc不会在内核本身中被调用。没有必要在头文件中为包装器kfunc提供原型。

下面给出一个示例

/* Disables missing prototype warnings */
__bpf_kfunc_start_defs();

__bpf_kfunc struct task_struct *bpf_find_get_task_by_vpid(pid_t nr)
{
        return find_get_task_by_vpid(nr);
}

__bpf_kfunc_end_defs();

当我们需要注释kfunc的参数时,通常需要包装器kfunc。否则,可以通过向BPF子系统注册来直接使kfunc对BPF程序可见。请参见2.3 使用现有的内核函数

2.2 注释kfunc参数

与BPF辅助函数类似,有时需要验证器所需的其他上下文,以使内核函数的使用更安全,更有用。因此,我们可以通过在kfunc的参数名称后加上__tag来注释参数,其中tag可以是受支持的注释之一。

2.2.1 __sz注释

此注释用于指示参数列表中的内存和大小对。下面给出一个示例

__bpf_kfunc void bpf_memzero(void *mem, int mem__sz)
{
...
}

在这里,验证器会将第一个参数视为PTR_TO_MEM,将第二个参数视为其大小。默认情况下,如果没有__sz注释,则使用指针的类型的大小。如果没有__sz注释,则kfunc不能接受void指针。

2.2.2 __k注释

此注释仅适用于标量参数,它表示验证器必须检查标量参数是否为已知常量,该常量不表示大小参数,并且常量的值与程序的安全性相关。

下面给出一个示例

__bpf_kfunc void *bpf_obj_new(u32 local_type_id__k, ...)
{
...
}

在这里,bpf_obj_new使用local_type_id参数来查找程序BTF中该类型ID的大小,并返回指向它的大小指针。每个类型ID将具有不同的大小,因此至关重要的是,在验证器状态修剪检查期间值不匹配时,将每个此类调用视为不同的调用。

因此,只要kfunc接受的常量标量参数不是大小参数,并且常量的值对于程序安全性很重要,则应使用__k后缀。

2.2.3 __uninit注释

此注释用于指示该参数将被视为未初始化。

下面给出一个示例

__bpf_kfunc int bpf_dynptr_from_skb(..., struct bpf_dynptr_kern *ptr__uninit)
{
...
}

在这里,dynptr将被视为未初始化的dynptr。如果没有此注释,如果传入的dynptr未初始化,验证器将拒绝该程序。

2.2.4 __opt注释

此注释用于指示与__sz或__szk参数关联的缓冲区可能为空。如果该函数被传递了nullptr来代替缓冲区,则验证器将不会检查该长度是否适合该缓冲区。kfunc负责在使用此缓冲区之前检查它是否为空。

下面给出一个示例

__bpf_kfunc void *bpf_dynptr_slice(..., void *buffer__opt, u32 buffer__szk)
{
...
}

在这里,缓冲区可能为空。如果缓冲区不为空,则它至少是buffer_szk的大小。无论哪种方式,返回的缓冲区要么是NULL,要么是buffer_szk的大小。如果没有此注释,如果传入一个非零大小的空指针,验证器将拒绝该程序。

2.2.5 __str注释

此注释用于指示该参数是常量字符串。

下面给出一个示例

__bpf_kfunc bpf_get_file_xattr(..., const char *name__str, ...)
{
...
}

在这种情况下,可以像这样调用bpf_get_file_xattr()

bpf_get_file_xattr(..., "xattr_name", ...);

或者

const char name[] = "xattr_name";  /* This need to be global */
int BPF_PROG(...)
{
        ...
        bpf_get_file_xattr(..., name, ...);
        ...
}

2.2.6 __prog注释

此注释用于指示需要将该参数修复为调用方BPF程序的bpf_prog_aux。传递给此参数的任何值都将被忽略,并由验证器重写。

下面给出一个示例

__bpf_kfunc int bpf_wq_set_callback_impl(struct bpf_wq *wq,
                                         int (callback_fn)(void *map, int *key, void *value),
                                         unsigned int flags,
                                         void *aux__prog)
 {
        struct bpf_prog_aux *aux = aux__prog;
        ...
 }

2.3 使用现有的内核函数

当内核中的现有函数适合由BPF程序使用时,可以直接将其注册到BPF子系统。但是,仍然必须注意查看BPF程序将调用它的上下文,以及这样做是否安全。

2.4 注释kfuncs

除了kfuncs的参数之外,验证器可能还需要有关注册到BPF子系统的kfunc类型的更多信息。为此,我们在一组kfuncs上定义标志,如下所示

BTF_KFUNCS_START(bpf_task_set)
BTF_ID_FLAGS(func, bpf_get_task_pid, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_put_pid, KF_RELEASE)
BTF_KFUNCS_END(bpf_task_set)

此集合编码了上面列出的每个kfunc的BTF ID,并将标志与其一起编码。当然,也可以指定不带标志。

kfunc定义也应始终用__bpf_kfunc宏进行注释。这可以防止编译器内联kfunc(如果它是静态内核函数)或该函数在LTO构建中被省略(因为它没有在内核的其余部分中使用)之类的问题。开发人员不应手动将注释添加到其kfunc以防止这些问题。如果需要注释来防止此类kfunc出现问题,则这是一个错误,应将其添加到宏定义中,以便其他kfuncs受到类似的保护。下面给出一个示例

__bpf_kfunc struct task_struct *bpf_get_task_pid(s32 pid)
{
...
}

2.4.1 KF_ACQUIRE标志

KF_ACQUIRE标志用于指示kfunc返回指向引用计数对象的指针。然后,验证器将确保使用release kfunc最终释放指向该对象的指针,或通过调用bpf_kptr_xchg将其传输到映射中使用引用kptr。否则,验证器将无法加载BPF程序,直到程序的所有可能探索状态中都没有残留的引用为止。

2.4.2 KF_RET_NULL标志

KF_RET_NULL标志用于指示kfunc返回的指针可能为NULL。因此,它强制用户在使用(取消引用或传递给另一个辅助函数)从kfunc返回的指针之前,对该指针执行NULL检查。此标志通常与KF_ACQUIRE标志配对使用,但两者彼此正交。

2.4.3 KF_RELEASE标志

KF_RELEASE标志用于指示kfunc释放传递给它的指针。只能传递一个被引用的指针。通过使用此标志调用kfunc,释放指针的所有副本都将失效。KF_RELEASE kfuncs会自动获得下面描述的KF_TRUSTED_ARGS标志提供的保护。

2.4.4 KF_TRUSTED_ARGS标志

KF_TRUSTED_ARGS标志用于采用指针参数的kfuncs。它指示所有指针参数都是有效的,并且所有指向BTF对象的指针都已以其未修改的形式传递(即,零偏移量,并且没有从遍历另一个指针获得,下面描述的一个例外)。

有两种类型的指向内核对象的指针被认为是“有效”的

  1. 作为跟踪点或struct_ops回调参数传递的指针。

  2. 从KF_ACQUIRE kfunc返回的指针。

指向非BTF对象的指针(例如标量指针)也可以传递给KF_TRUSTED_ARGS kfuncs,并且可以具有非零偏移量。

“有效”指针的定义随时可能更改,并且绝对没有任何ABI稳定性保证。

如上所述,从遍历受信任指针获得的嵌套指针不再受信任,但有一个例外。如果结构类型具有一个字段,只要其父指针有效,就可以保证该字段有效(受信任或rcu,如KF_RCU描述中所述),则可以使用以下宏向验证器表达这一点

  • BTF_TYPE_SAFE_TRUSTED

  • BTF_TYPE_SAFE_RCU

  • BTF_TYPE_SAFE_RCU_OR_NULL

例如,

BTF_TYPE_SAFE_TRUSTED(struct socket) {
        struct sock *sk;
};

BTF_TYPE_SAFE_RCU(struct task_struct) {
        const cpumask_t *cpus_ptr;
        struct css_set __rcu *cgroups;
        struct task_struct __rcu *real_parent;
        struct task_struct *group_leader;
};

换句话说,你必须

  1. 将有效的指针类型包装在BTF_TYPE_SAFE_*宏中。

  2. 指定有效嵌套字段的类型和名称。此字段必须与原始类型定义中的字段完全匹配。

BTF_TYPE_SAFE_*宏声明的新类型也需要发出,以便它出现在BTF中。例如,BTF_TYPE_SAFE_TRUSTED(struct socket)type_is_trusted()函数中发出,如下所示

BTF_TYPE_EMIT(BTF_TYPE_SAFE_TRUSTED(struct socket));

2.4.5 KF_SLEEPABLE标志

KF_SLEEPABLE标志用于可能休眠的kfuncs。此类kfuncs只能由可休眠的BPF程序(BPF_F_SLEEPABLE)调用。

2.4.6 KF_DESTRUCTIVE标志

KF_DESTRUCTIVE标志用于指示调用哪些函数会对系统造成破坏。例如,这样的调用可能会导致系统重新启动或崩溃。由于此原因,这些调用适用其他限制。目前,它们仅需要CAP_SYS_BOOT功能,但以后可以添加更多功能。

2.4.7 KF_RCU标志

KF_RCU标志是KF_TRUSTED_ARGS的较弱版本。标有KF_RCU的kfuncs期望PTR_TRUSTED或MEM_RCU参数。验证器保证对象有效,并且不存在use-after-free。指针不为NULL,但是对象的引用计数可能已达到零。kfuncs需要考虑执行refcnt != 0检查,尤其是在返回KF_ACQUIRE指针时。还要注意,KF_ACQUIRE kfunc为KF_RCU时,很可能也应该为KF_RET_NULL。

2.4.8 KF_DEPRECATED标志

KF_DEPRECATED标志用于计划在后续内核版本中更改或删除的kfuncs。标有KF_DEPRECATED的kfunc也应在其内核文档中捕获任何相关信息。此类信息通常包括kfunc的预期剩余寿命,对可以替换它的新功能的建议(如果有),以及删除它的理由。

请注意,尽管在某些情况下,可能会继续支持KF_DEPRECATED kfunc并删除其KF_DEPRECATED标志,但在添加KF_DEPRECATED标志之后删除它可能比一开始就阻止添加它要困难得多。如3. kfunc生命周期预期中所述,鼓励依赖特定kfuncs的用户尽早了解其用例,并参与有关是否保留、更改、弃用或删除这些kfuncs的上游讨论(如果并且在发生此类讨论时)。

2.5 注册kfuncs

一旦kfunc准备好使用,使其可见的最后一步是将其注册到BPF子系统。注册是按BPF程序类型完成的。下面显示一个示例

BTF_KFUNCS_START(bpf_task_set)
BTF_ID_FLAGS(func, bpf_get_task_pid, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_put_pid, KF_RELEASE)
BTF_KFUNCS_END(bpf_task_set)

static const struct btf_kfunc_id_set bpf_task_kfunc_set = {
        .owner = THIS_MODULE,
        .set   = &bpf_task_set,
};

static int init_subsystem(void)
{
        return register_btf_kfunc_id_set(BPF_PROG_TYPE_TRACING, &bpf_task_kfunc_set);
}
late_initcall(init_subsystem);

2.6 使用___init指定no-cast别名

验证器将始终强制执行由BPF程序传递给kfunc的指针的BTF类型与kfunc定义中指定的指针类型匹配。但是,即使它们的BTF_IDs不同,验证器也允许根据C标准被认为是等效的类型传递给同一个kfunc参数。

例如,对于以下类型定义

struct bpf_cpumask {
        cpumask_t cpumask;
        refcount_t usage;
};

验证器将允许将struct bpf_cpumask *传递给采用cpumask_t *的kfunc(它是struct cpumask *的typedef)。例如,struct cpumask *struct bpf_cpmuask *都可以传递给bpf_cpumask_test_cpu()

在某些情况下,不希望使用此类型别名行为。struct nf_conn___init就是一个这样的例子

struct nf_conn___init {
        struct nf_conn ct;
};

C标准会将这些类型视为等效的,但将任一类型传递给受信任的kfunc并不总是安全的。struct nf_conn___init表示已分配的struct nf_conn对象,该对象尚未初始化,因此将struct nf_conn___init *传递给期望完全初始化的struct nf_conn *(例如bpf_ct_change_timeout())的kfunc是不安全的。

为了满足这些要求,如果两种类型具有完全相同的名称,并且其中一种类型的后缀为___init,则验证器将强制执行严格的PTR_TO_BTF_ID类型匹配。

3. kfunc生命周期预期

kfuncs提供了一个内核 <-> 内核 API,因此不受与内核 <-> 用户 UAPI 关联的任何严格的稳定性限制的约束。这意味着可以将它们视为类似于 EXPORT_SYMBOL_GPL,因此可以由定义它们的子系统的维护人员在认为必要时修改或删除。

与对内核的任何其他更改一样,维护人员不会在没有合理的理由的情况下更改或删除 kfunc。他们是否选择更改 kfunc 最终取决于各种因素,例如 kfunc 的使用范围、kfunc 在内核中的时间、是否存在替代 kfunc、相关子系统的稳定性规范,以及当然继续支持 kfunc 的技术成本。

这有几个含义

  1. 被广泛使用或在内核中存在很长时间的 kfuncs 将更难以证明维护人员可以更改或删除。换句话说,已知有很多用户并且提供重要价值的 kfuncs 为维护人员投入时间和复杂性来支持它们提供了更强的动力。因此,在其 BPF 程序中使用 kfuncs 的开发人员沟通和解释如何以及为什么要使用这些 kfuncs,并在上游讨论发生时参与有关这些 kfuncs 的讨论非常重要。

  2. 与标有 EXPORT_SYMBOL_GPL 的常规内核符号不同,调用 kfuncs 的 BPF 程序通常不是内核树的一部分。这意味着当 kfunc 更改时,重构通常无法就地更改调用者,就像当内核符号更改时就地更新上游驱动程序一样。

    与常规内核符号不同,这是 BPF 符号的预期行为,并且使用 kfuncs 的树外 BPF 程序应被认为与修改和删除这些 kfuncs 的讨论和决策相关。BPF 社区将在必要时积极参与上游讨论,以确保考虑到此类用户的观点。

  3. kfunc 永远不会有任何硬性稳定性保证。BPF API 不能也不会仅仅出于稳定性原因而硬性阻止内核中的更改。也就是说,kfuncs 是旨在解决问题并为用户提供价值的功能。是否更改或删除 kfunc 的决定是一个多变量技术决策,该决策是根据具体情况做出的,并且由上述数据点等信息告知。预计在没有警告的情况下删除或更改 kfunc 不会是一个常见的事件或在没有充分理由的情况下发生,但是如果一个人要使用 kfuncs,则必须接受这种可能性。

3.1 kfunc 弃用

如上所述,虽然有时维护人员可能会发现必须立即更改或删除 kfunc 以适应其子系统中的某些更改,但通常 kfuncs 将能够适应更长和更谨慎的弃用过程。例如,如果出现一个新的 kfunc,它为现有的 kfunc 提供了卓越的功能,则现有的 kfunc 可能会被弃用一段时间,以允许用户迁移其 BPF 程序以使用新的 kfunc。或者,如果 kfunc 没有已知用户,则可能会决定在某个弃用期后删除该 kfunc(不提供替代 API),以便为用户提供一个窗口来通知 kfunc 维护人员,如果事实证明 kfunc 实际上正在使用。

预计常见的情况是 kfuncs 将经历一个弃用期,而不是在没有警告的情况下被更改或删除。如2.4.8 KF_DEPRECATED标志中所述,kfunc 框架为 kfunc 开发人员提供了 KF_DEPRECATED 标志,以向用户发出 kfunc 已被弃用的信号。一旦 kfunc 标有 KF_DEPRECATED,则移除过程如下

  1. 弃用的 kfuncs 的任何相关信息都记录在 kfunc 的内核文档中。此文档通常包括 kfunc 的预期剩余寿命,对可以替换弃用函数的新功能的建议(或解释为什么不存在此类替换),等等。

  2. 弃用的 kfunc 在首次标记为弃用后会在内核中保留一段时间。此时间段将根据具体情况选择,并且通常取决于 kfunc 的使用范围、它在内核中的时间以及迁移到替代方案的难度。此弃用时间段是“尽最大努力”,并且如above中所述,有时情况可能表明必须在完整的预期弃用期过去之前删除 kfunc。

  3. 在弃用期之后,kfunc 将被删除。此时,调用 kfunc 的 BPF 程序将被验证器拒绝。

4. 核心 kfuncs

BPF 子系统提供了许多“核心” kfuncs,这些 kfuncs 可能适用于各种不同的可能用例和程序。这些 kfuncs 在此处记录。

4.1 struct task_struct * kfuncs

有许多 kfuncs 允许将struct task_struct *对象用作 kptrs

__bpf_kfunc struct task_struct *bpf_task_acquire(struct task_struct *p)

获取对任务的引用。由此 kfunc 获取的任务如果未作为 kptr 存储在映射中,则必须通过调用bpf_task_release()释放。

参数

struct task_struct *p

正在获取引用的任务。

__bpf_kfunc void bpf_task_release(struct task_struct *p)

释放在任务上获取的引用。

参数

struct task_struct *p

正在释放引用的任务。

当您想要获取或释放在struct task_struct *上获取的引用(例如作为跟踪点参数或 struct_ops 回调参数传递)时,这些 kfuncs 很有用。例如

/**
 * A trivial example tracepoint program that shows how to
 * acquire and release a struct task_struct * pointer.
 */
SEC("tp_btf/task_newtask")
int BPF_PROG(task_acquire_release_example, struct task_struct *task, u64 clone_flags)
{
        struct task_struct *acquired;

        acquired = bpf_task_acquire(task);
        if (acquired)
                /*
                 * In a typical program you'd do something like store
                 * the task in a map, and the map will automatically
                 * release it later. Here, we release it manually.
                 */
                bpf_task_release(acquired);
        return 0;
}

struct task_struct *对象上获取的引用受 RCU 保护。因此,当在 RCU 读取区域中时,您可以获得指向嵌入在映射值中的任务的指针,而无需获取引用

#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
private(TASK) static struct task_struct *global;

/**
 * A trivial example showing how to access a task stored
 * in a map using RCU.
 */
SEC("tp_btf/task_newtask")
int BPF_PROG(task_rcu_read_example, struct task_struct *task, u64 clone_flags)
{
        struct task_struct *local_copy;

        bpf_rcu_read_lock();
        local_copy = global;
        if (local_copy)
                /*
                 * We could also pass local_copy to kfuncs or helper functions here,
                 * as we're guaranteed that local_copy will be valid until we exit
                 * the RCU read region below.
                 */
                bpf_printk("Global task %s is valid", local_copy->comm);
        else
                bpf_printk("No global task found");
        bpf_rcu_read_unlock();

        /* At this point we can no longer reference local_copy. */

        return 0;
}

BPF 程序还可以从 pid 查找任务。如果调用方没有指向struct task_struct *对象的受信任指针,可以使用bpf_task_acquire()获取引用,这可能很有用。

__bpf_kfunc struct task_struct *bpf_task_from_pid(s32 pid)

通过在根 pid 命名空间 idr 中查找,从其 pid 查找 struct task_struct。如果返回任务,则必须将其存储在映射中,或者使用bpf_task_release()释放。

参数

s32 pid

正在查找的任务的 pid。

这是一个使用它的示例

SEC("tp_btf/task_newtask")
int BPF_PROG(task_get_pid_example, struct task_struct *task, u64 clone_flags)
{
        struct task_struct *lookup;

        lookup = bpf_task_from_pid(task->pid);
        if (!lookup)
                /* A task should always be found, as %task is a tracepoint arg. */
                return -ENOENT;

        if (lookup->pid != task->pid) {
                /* bpf_task_from_pid() looks up the task via its
                 * globally-unique pid from the init_pid_ns. Thus,
                 * the pid of the lookup task should always be the
                 * same as the input task.
                 */
                bpf_task_release(lookup);
                return -EINVAL;
        }

        /* bpf_task_from_pid() returns an acquired reference,
         * so it must be dropped before returning from the
         * tracepoint handler.
         */
        bpf_task_release(lookup);
        return 0;
}

4.2 struct cgroup * kfuncs

struct cgroup *对象也有获取和释放函数

__bpf_kfunc struct cgroup *bpf_cgroup_acquire(struct cgroup *cgrp)

获取对 cgroup 的引用。由此 kfunc 获取的 cgroup 如果未作为 kptr 存储在映射中,则必须通过调用bpf_cgroup_release()释放。

参数

struct cgroup *cgrp

正在获取引用的 cgroup。

__bpf_kfunc void bpf_cgroup_release(struct cgroup *cgrp)

释放在 cgroup 上获取的引用。如果在 RCU 读取区域中调用此 kfunc,则即使其引用计数降至 0,也保证 cgroup 在当前宽限期结束之前不会被释放。

参数

struct cgroup *cgrp

正在释放引用的 cgroup。

这些 kfuncs 的使用方式与bpf_task_acquire()bpf_task_release()分别完全相同,因此我们不会为它们提供示例。


其他可用于与struct cgroup *对象交互的 kfuncs 是bpf_cgroup_ancestor()bpf_cgroup_from_id(),分别允许调用方访问 cgroup 的祖先并按其 ID 查找 cgroup。两者都返回 cgroup kptr。

__bpf_kfunc struct cgroup *bpf_cgroup_ancestor(struct cgroup *cgrp, int level)

对 cgroup 的祖先数组中的条目执行查找。由此 kfunc 返回的 cgroup 如果未随后存储在映射中,则必须通过调用bpf_cgroup_release()释放。

参数

struct cgroup *cgrp

我们正在为其执行查找的 cgroup。

int level

要查找的祖先级别。

__bpf_kfunc struct cgroup *bpf_cgroup_from_id(u64 cgid)

通过 ID 查找 cgroup。此 kfunc 返回的 cgroup 如果没有随后存储在 map 中,则必须通过调用 bpf_cgroup_release() 来释放。

参数

u64 cgid

cgroup ID。

最终,应该更新 BPF 以允许通过程序本身中的普通内存加载来完成此操作。如果没有在验证器中做更多的工作,目前这是不可能的。 bpf_cgroup_ancestor() 可以如下使用

/**
 * Simple tracepoint example that illustrates how a cgroup's
 * ancestor can be accessed using bpf_cgroup_ancestor().
 */
SEC("tp_btf/cgroup_mkdir")
int BPF_PROG(cgrp_ancestor_example, struct cgroup *cgrp, const char *path)
{
        struct cgroup *parent;

        /* The parent cgroup resides at the level before the current cgroup's level. */
        parent = bpf_cgroup_ancestor(cgrp, cgrp->level - 1);
        if (!parent)
                return -ENOENT;

        bpf_printk("Parent id is %d", parent->self.id);

        /* Return the parent cgroup that was acquired above. */
        bpf_cgroup_release(parent);
        return 0;
}

4.3 struct cpumask * kfuncs

BPF 提供了一组 kfuncs,可以用于查询、分配、修改和销毁 struct cpumask * 对象。 请参阅 BPF cpumask kfuncs 了解更多详细信息。