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 时,包装器函数应具有 extern 链接。这可以防止编译器优化掉死代码,因为此包装器 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 来直接使 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 参数关联的缓冲区可能为 null。如果将 nullptr 传递给函数以代替缓冲区,则验证器将不会检查该长度是否适合该缓冲区。kfunc 负责在使用缓冲区之前检查此缓冲区是否为 null。
下面给出一个示例
__bpf_kfunc void *bpf_dynptr_slice(..., void *buffer__opt, u32 buffer__szk)
{
...
}
在这里,缓冲区可能为 null。如果缓冲区不为 null,则其大小至少为 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.3 使用现有的内核函数¶
当内核中的现有函数适合 BPF 程序使用时,可以直接将其注册到 BPF 子系统。但是,仍然必须注意检查 BPF 程序将调用它的上下文以及这样做是否安全。
2.4 注释 kfuncs¶
除了 kfunc 的参数之外,验证器可能还需要有关注册到 BPF 子系统的 kfunc 类型(或多个 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 是静态内核函数,则编译器会内联该 kfunc,或者该函数在 LTO 构建中会被省略,因为它没有在内核的其余部分中使用。开发人员不应手动向其 kfunc 添加注释以防止这些问题。如果需要注释来防止您的 kfunc 出现此类问题,则这是一个错误,应将其添加到宏的定义中,以便以相同方式保护其他 kfunc。下面给出一个示例
__bpf_kfunc struct task_struct *bpf_get_task_pid(s32 pid)
{
...
}
2.4.1 KF_ACQUIRE 标志¶
KF_ACQUIRE 标志用于指示 kfunc 返回指向引用计数对象的指针。然后,验证器将确保最终使用 release kfunc 释放对象指针,或者使用引用的 kptr(通过调用 bpf_kptr_xchg)将其传输到映射。否则,验证器将导致 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 kfunc 自动获得下面描述的 KF_TRUSTED_ARGS 标志提供的保护。
2.4.4 KF_TRUSTED_ARGS 标志¶
KF_TRUSTED_ARGS 标志用于采用指针参数的 kfunc。它指示所有指针参数都是有效的,并且所有指向 BTF 对象的指针都已以其未修改的形式传递(即,在零偏移量处,并且不是通过遍历另一个指针获得的,下面描述的一种例外)。
有两种类型的指向内核对象的指针被认为是“有效”的
作为跟踪点或 struct_ops 回调参数传递的指针。
从 KF_ACQUIRE kfunc 返回的指针。
指向非 BTF 对象(例如,标量指针)的指针也可以传递给 KF_TRUSTED_ARGS kfunc,并且可能具有非零偏移量。
“有效”指针的定义随时可能更改,并且绝对没有 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;
};
换句话说,您必须
将有效指针类型包装在
BTF_TYPE_SAFE_*
宏中。指定有效嵌套字段的类型和名称。此字段必须与原始类型定义中的字段完全匹配。
还需要发出由 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 标志用于可能休眠的 kfunc。只有可休眠 BPF 程序 (BPF_F_SLEEPABLE) 才能调用此类 kfunc。
2.4.6 KF_DESTRUCTIVE 标志¶
KF_DESTRUCTIVE 标志用于指示调用对系统具有破坏性的函数。例如,此类调用可能会导致系统重新启动或崩溃。由于这个原因,对这些调用施加了额外的限制。目前,它们只需要 CAP_SYS_BOOT 功能,但以后可能会添加更多功能。
2.4.7 KF_RCU 标志¶
KF_RCU 标志是 KF_TRUSTED_ARGS 的一个较弱版本。带有 KF_RCU 标记的 kfunc 期望 PTR_TRUSTED 或 MEM_RCU 参数。验证器保证对象有效,且不存在 use-after-free 的情况。指针不为 NULL,但对象的引用计数可能已达到零。kfunc 需要考虑进行 refcnt != 0 的检查,尤其是在返回 KF_ACQUIRE 指针时。请注意,一个 KF_ACQUIRE 且为 KF_RCU 的 kfunc 很可能也应该是 KF_RET_NULL。
2.4.8 KF_DEPRECATED 标志¶
KF_DEPRECATED 标志用于计划在后续内核版本中更改或删除的 kfunc。标记为 KF_DEPRECATED 的 kfunc 也应该在其内核文档中捕获任何相关信息。此类信息通常包括 kfunc 的预期剩余寿命,如果存在任何可用的替代功能,则建议使用新的功能,以及可能解释为何将其删除的原因。
请注意,虽然在某些情况下,KF_DEPRECATED kfunc 可能会继续受到支持并删除其 KF_DEPRECATED 标志,但删除已添加的 KF_DEPRECATED 标志可能比一开始就阻止添加它要困难得多。如 3. kfunc 生命周期期望 中所述,鼓励依赖特定 kfunc 的用户尽早公开其用例,并参与关于是否保留、更改、弃用或删除这些 kfunc 的上游讨论(如果发生此类讨论)。
2.5 注册 kfunc¶
一旦 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 指定无强制转换别名¶
验证器始终会强制要求 BPF 程序传递给 kfunc 的指针的 BTF 类型,与 kfunc 定义中指定的指针类型相匹配。但是,即使它们的 BTF_ID 不同,验证器也允许根据 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 *
的 kfunc(例如 bpf_ct_change_timeout()
)是不安全的。
为了满足这些要求,如果两个类型的名称完全相同,其中一个类型附加了 ___init
后缀,则验证器将强制执行严格的 PTR_TO_BTF_ID 类型匹配。
3. kfunc 生命周期期望¶
kfunc 提供内核 <-> 内核 API,因此不受与内核 <-> 用户 UAPI 相关的任何严格的稳定性限制。这意味着可以将它们视为类似于 EXPORT_SYMBOL_GPL,因此当认为必要时,它们可以由其定义的子系统的维护人员修改或删除。
与对内核的任何其他更改一样,维护人员不会在没有合理理由的情况下更改或删除 kfunc。他们是否选择更改 kfunc 最终取决于多种因素,例如 kfunc 的使用范围、kfunc 在内核中存在的时间、是否存在替代的 kfunc、相关子系统稳定性的规范是什么,当然还有继续支持该 kfunc 的技术成本。
这有几个含义
被广泛使用或在内核中存在很长时间的 kfunc,维护人员更难证明对其进行更改或删除是合理的。换句话说,已知有很多用户并且提供重要价值的 kfunc,为维护人员投入时间和复杂性来支持它们提供了更强的激励。因此,对于在其 BPF 程序中使用 kfunc 的开发人员来说,重要的是沟通和解释如何以及为何使用这些 kfunc,并在上游发生关于这些 kfunc 的讨论时参与其中。
与用 EXPORT_SYMBOL_GPL 标记的常规内核符号不同,调用 kfunc 的 BPF 程序通常不是内核树的一部分。这意味着,当 kfunc 更改时,重构通常无法就地更改调用者,就像例如当内核符号更改时,就地更新上游驱动程序一样。
与常规内核符号不同,这是 BPF 符号的预期行为,并且使用 kfunc 的树外 BPF 程序应被视为与修改和删除这些 kfunc 的讨论和决策相关。 BPF 社区将在必要时积极参与上游讨论,以确保考虑此类用户的观点。
kfunc 永远不会有任何硬性的稳定性保证。 BPF API 不能也不会仅仅出于稳定性原因而硬性阻止内核中的更改。也就是说,kfunc 是旨在解决问题并为用户提供价值的功能。是否更改或删除 kfunc 的决定是一个多因素的技术决策,该决策是根据具体情况做出的,并受到诸如上述提到的数据点的支持。预计在没有警告的情况下删除或更改 kfunc 将不会是常见的情况,或者在没有合理的理由的情况下发生,但如果要使用 kfunc,则必须接受这种可能性。
3.1 kfunc 弃用¶
如上所述,虽然有时维护人员可能会发现必须立即更改或删除 kfunc 以适应其子系统中的某些更改,但通常 kfunc 将能够适应更长和更可衡量的弃用过程。例如,如果出现一个新的 kfunc,它为现有 kfunc 提供了更优越的功能,则可以弃用现有 kfunc 一段时间,以允许用户将其 BPF 程序迁移为使用新的 kfunc。或者,如果某个 kfunc 没有已知的用户,则可以在一段时间的弃用期后决定删除该 kfunc(而不提供替代 API),以便为用户提供一个窗口来通知 kfunc 维护人员,如果事实证明该 kfunc 确实正在使用。
预计常见的情况是,kfunc 将经历一个弃用期,而不是在没有警告的情况下被更改或删除。如 2.4.8 KF_DEPRECATED 标志 中所述,kfunc 框架为 kfunc 开发人员提供了 KF_DEPRECATED 标志,以向用户发出 kfunc 已被弃用的信号。一旦 kfunc 被标记为 KF_DEPRECATED,则遵循以下删除过程
有关已弃用的 kfunc 的任何相关信息都记录在 kfunc 的内核文档中。此文档通常将包括 kfunc 的预期剩余寿命、关于可以替代弃用函数使用的新功能的建议(或解释为什么不存在这样的替代方案)等等。
在首次标记为已弃用后,已弃用的 kfunc 将在内核中保留一段时间。此时间段将根据具体情况选择,通常取决于 kfunc 的使用范围、在内核中存在的时间以及迁移到替代方案的难度。此弃用时间段是“尽力而为”,并且如 上述 所述,有时情况可能决定在完整的预期弃用期过去之前删除 kfunc。
在弃用期之后,kfunc 将被删除。此时,验证器将拒绝调用该 kfunc 的 BPF 程序。
4. 核心 kfunc¶
BPF 子系统提供了许多“核心” kfunc,这些 kfunc 可能适用于各种不同的可能用例和程序。这些 kfunc 在此处记录。
4.1 struct task_struct * kfunc¶
有许多 kfunc 允许将 struct task_struct *
对象用作 kptr
-
__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
正在释放引用的任务。
当您想要获取或释放对作为例如 tracepoint 参数或 struct_ops 回调参数传递的 struct task_struct *
的引用时,这些 kfunc 非常有用。例如
/**
* 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 获取的且未作为 kptr 存储在映射中的 cgroup,必须通过调用
bpf_cgroup_release()
释放。
参数
struct cgroup *cgrp
正在获取引用的 cgroup。
-
__bpf_kfunc void bpf_cgroup_release(struct cgroup *cgrp)¶
释放在 cgroup 上获取的引用。如果此 kfunc 在 RCU 读取区域中调用,则即使其引用计数降至 0,也保证 cgroup 在当前宽限期结束之前不会被释放。
参数
struct cgroup *cgrp
正在释放引用的 cgroup。
这些 kfunc 的使用方式与 bpf_task_acquire()
和 bpf_task_release()
完全相同,因此我们不提供示例。
可用于与 struct cgroup *
对象交互的其他 kfunc 包括 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,必须通过调用
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 提供了一组 kfunc,可用于查询、分配、修改和销毁 struct cpumask * 对象。有关更多详细信息,请参阅 BPF cpumask kfuncs。