liunx内核的一些研究,主要是对kfunc 或者 helper 的语义,思考他们背后的 safety concern等一些东西

主入口:kernel/bpf/verifier.c 中的 check_kfunc_call()

一、一些基础知识

在进行研究之前,先补一点基础:

我现在研究的重点,是 Linux 内核里 eBPF 相关的一些接口,尤其是:

  • helper
  • kfunc

以及它们背后对应的 verifier 检查逻辑、类型约束和 safety concern。

1. 什么是ebpf,为什么要有verifier

ebpf可以理解成,用户写的一小段可以在内核中去执行的程序,但是内核是系统最核心的部分,很容易出问题,所以,ebpf在执行之前,会先经过一成审查,在进入内核执行,而这个审查就是verifier

verifier的作用就是不是检查业务逻辑,而是检查一些安全问题,是一个静态的审核员:

  • 这段程序会不会越界读写内存
  • 会不会使用空指针
  • 会不会把已经释放的对象继续拿来用
  • 会不会在错误的上下文里睡眠
  • 会不会破坏锁、RCU、引用计数这些内核协议
  • 会不会形成 verifier 无法证明安全的循环,等等

2. 什么是helper,什么是kfunc

helper是什么

——–> eBPF 程序可以调用的一批“老牌内核辅助函数接口”

它们通常有比较固定的调用方式,verifier 也是按老的一套规则去检查的。 比如参数是 map、buffer、长度、ctx 之类,很多规则都比较模板化

kfunc是什么

——–> 暴露给ebpf的普通内核函数,但是带上了btf的类型信息与额外语义标记

可以表达比helper更丰富的类型关系,比如:

  • 参数是不是 struct *
  • 返回值对象会不会带生命周期
  • 这个函数会不会sleep
  • 这个函数是不是iterator的next()
  • verifier会不会隐式的补进去一些函数,等等

所以,verifier会对kfunc的检查更复杂

3. verifier的眼里,程序是一个状态机

他关心的是

  • 寄存器里面是什么
  • 这个值是标量还是指针
  • 这个指针会指向哪里
  • 会不会带null
  • 是不是带引用计数的对象
  • 在不在RCU保护区里面
  • 是不是持有锁
  • 栈上的某个区域是什么样子的

他的核心工作,不是执行程序,而是更新程序状态模型

4. 什么是BTF

可以简单理解一下,就是内核里面的类型信息 比如一个函数参数是:

  • struct task_struct *
  • struct bpf_dynptr *
  • struct bpf_list_head *

如果没有BTF,verifier根本不知道这个指针到底是什么

kfunchelper更灵活,很大程度上就是因为他依赖BTF

  • verifier 可以知道参数是不是 struct *
  • 可以知道返回值是什么类型
  • 可以知道某个字段在结构体里的 offset
  • 可以把返回值建模成 PTR_TO_BTF_ID
  • 可以根据参数名后缀识别特殊语义,比如 __sz、__nullable

5.什么是safety concern

目前研究kfunc,我更想知道的是,为什么verifier要限制它,背后的原因是什么

我理解的concern,大概有:

  1. memory safety

    内存安全,比如说 越界访问、空指针访问、读未初始化内存、把对象当作另一种对象等等

  2. lifetime / ownership

    也就是生命周期的问题,比如说;引用之后未释放、连续释放两次、release之后还继续访问、容器插入之后的所有权

  3. concurrency / RCU / lock

    并发与锁的一些相关问题,比如说;没有没有持锁就操作 list/rbtree、不在 RCU 保护区里却去访问受 RCU 保护的对象、unlock 之后还继续用 borrowed pointer

  4. context safety

    上下文问题,比如说:不能sleep的地方调用sleepable kfunc,不合适的程序类型里调用某类 kfunc、缺少 capability 却调用 destructive kfunc

  5. protocol safety

    协议约束,比如说:dynpttr必须先初始化、iterator必须按照new->next->destroy的流程、irp save / restore必须配对、callback子程序的时候必须满足verifier的限制

所以后面的源码分析,不只是说明这个if在干什么,更关心的是,它在预防什么安全问题,为什么要这样做

我把kernel/bpf/verifier.c 里的 check_kfunc_call()当作主线入口,他的基本逻辑就是;1.取kfunc的元信息;2.检查参数;3.处理上下文约束;4.处理返回值;5.更新verifier的状态

下面是我的研究大纲:

先理清 check_kfunc_call() 大体分成哪几块

再整理一套“约束词表”,比如 KF_ACQUIRE、KF_RELEASE、KF_RET_NULL

再顺着参数检查主链看,“为什么输入必须是这种类型”

最后看返回值和生命周期逻辑,比如 acquire/release、non-owning ref、RCU、iterator 收敛

二、check_kfunc_call()函数研究

首先先看两个关键变量:

meta

struct bpf_kfunc_call_arg_meta meta;

他表示的意思是:“这次kfunc调用的元信息和中间信息的分析结果”

有好多函数会往里面填一些东西,譬如kfunc的flag、kfunc的原型func_proto,某个参数是不是dynptr / iter / list_node,是不是常量

regs

struct bpf_reg_state *regs = cur_regs(env);

当前verifier眼里的寄存器状态

  • R0:kfunc 的返回值
  • R1-R5:调用参数寄存器

第一段:kfunc的元信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

/* skip for now, but return error when we find this in fixup_kfunc_call */
if (!insn->imm)
return 0;

err = fetch_kfunc_arg_meta(env, insn->imm, insn->off, &meta);
if (err == -EACCES && meta.func_name)
verbose(env, "calling kernel function %s is not allowed\n", meta.func_name);
if (err)
return err;

desc_btf = meta.btf;
func_name = meta.func_name;
insn_aux = &env->insn_aux_data[insn_idx];

insn_aux->is_iter_next = is_iter_next_kfunc(&meta);

如果imm是0,就说明它现在还不是一个可正常解析的 kfunc 调用,那么先不处理

然后,err = fetch_kfunc_arg_meta(env, insn->imm, insn->off, &meta); 它会把当前这次 kfunc 调用的关键信息取出来,塞进 meta 里。 比如:这是哪个 kfunc、函数名是什么、它的 BTF 原型是什么、它带了哪些 KF_* 标志、它属于哪个 BTF(vmlinux 还是 module) 先搞清楚,我要检查的kfunc是哪一个

然后,把常用信息拿出来,后面方便用,(其中 insn_aux 后面会存很多“这条 kfunc 调用的附加信息”)

并记住他是不是next()方法,一些函数比如

  • bpf_iter_num_next

    • bpf_iter_task_next

    这类函数不是普通函数。 verifier 后面要额外验证

第二段:特殊 kfunc 的早期分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (!insn->off &&
(insn->imm == special_kfunc_list[KF_bpf_res_spin_lock] ||
insn->imm == special_kfunc_list[KF_bpf_res_spin_lock_irqsave])) {
struct bpf_verifier_state *branch;
struct bpf_reg_state *regs;

branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false);
if (IS_ERR(branch)) {
verbose(env, "failed to push state for failed lock acquisition\n");
return PTR_ERR(branch);
}

regs = branch->frame[branch->curframe]->regs;

/* Clear r0-r5 registers in forked state */
for (i = 0; i < CALLER_SAVED_REGS; i++)
mark_reg_not_init(env, regs, caller_saved[i]);

mark_reg_unknown(env, regs, BPF_REG_0);
err = __mark_reg_s32_range(env, regs, BPF_REG_0, -MAX_ERRNO, -1);
if (err) {
verbose(env, "failed to mark s32 range for retval in forked state for lock\n");
return err;
}
__mark_btf_func_reg_size(env, regs, BPF_REG_0, sizeof(u32));
} else if (!insn->off && insn->imm == special_kfunc_list[KF___bpf_trap]) {
verbose(env, "unexpected __bpf_trap() due to uninitialized variable?\n");
return -EFAULT;
}

这一段是在处理“普通通路不够表达”的特殊 kfunc。

为什么 bpf_res_spin_lock* 要单独分支?

因为这类 kfunc 有一个很特别的点:它可能失败。所以 verifier 不能只假设“调用成功”。它必须显式模拟出一条失败路径。

于是,使用push_stack分支出了一个新路径,模拟锁分配失败,清空寄存器,将返回值 R0 标记为一个未知的错误码(-MAX_ERRNO 到 -1)

第三段:程序级上下文约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
      if (is_kfunc_destructive(&meta) && !capable(CAP_SYS_BOOT)) {
verbose(env, "destructive kfunc calls require CAP_SYS_BOOT capability\n");
return -EACCES;
}

sleepable = is_kfunc_sleepable(&meta);
if (sleepable && !in_sleepable(env)) {
verbose(env, "program must be sleepable to call sleepable kfunc %s\n", func_name);
return -EACCES;
}

/* Track non-sleepable context for kfuncs, same as for helpers. */
if (!in_sleepable_context(env))
insn_aux->non_sleepable = true;

这段代码很有代表性,因为它检查的不是参数,而是调用点环境。

———

### 1. KF_DESTRUCTIVE:破坏性 kfunc 需要更高权限

if (is_kfunc_destructive(&meta) && !capable(CAP_SYS_BOOT)) {
...
return -EACCES;
}
  1. KF_DESTRUCTIVE:破坏性 kfunc 需要更高权限

  2. KF_SLEEPABLE:先检查“这个程序类型允不允许 sleep”

  3. insn_aux->non_sleepable = true:记住当前小上下文

    这句要特别注意。它和上面的 in_sleepable(env) 不是一回事。

    可以这样理解:

    • in_sleepable(env):整个程序大环境允不允许 sleep
    • in_sleepable_context(env):当前这一小段路径是不是仍然处在可 sleep 状态(有种套娃的感觉)

第四段:参数检查与回调检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
err = check_kfunc_args(env, &meta, insn_idx);
if (err < 0)
return err;

if (is_bpf_rbtree_add_kfunc(meta.func_id)) {
err = push_callback_call(env, insn, insn_idx, meta.subprogno,
set_rbtree_add_callback_state);
if (err) {
verbose(env, "kfunc %s#%d failed callback verification\n",
func_name, meta.func_id);
return err;
}
}

if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie]) {
meta.r0_size = sizeof(u64);
meta.r0_rdonly = false;
}

if (is_bpf_wq_set_callback_kfunc(meta.func_id)) {
err = push_callback_call(env, insn, insn_idx, meta.subprogno,
set_timer_callback_state);
if (err) {
verbose(env, "kfunc %s#%d failed callback verification\n",
func_name, meta.func_id);
return err;
}
}

if (is_task_work_add_kfunc(meta.func_id)) {
err = push_callback_call(env, insn, insn_idx, meta.subprogno,
set_task_work_schedule_callback_state);
if (err) {
verbose(env, "kfunc %s#%d failed callback verification\n",
func_name, meta.func_id);
return err;
}
}
  1. check_kfunc_args():整个参数检查主入口

    它负责做的事情不是一个简单的“参数个数检查”,而是:

    • 每个参数到底是什么语义
    • 它是普通标量还是特殊指针
    • 它是不是 __nullable
    • 它是不是 __k
    • 它是不是 dynptr / iter / list_node / rb_root
    • 它是不是 release 参数
    • 它是不是 callback

    check_kfunc_args() 不只是检查,还会往 meta 里写很多后续要用的信息。

    比如:

    • meta.release_regno
    • meta.arg_constant
    • meta.arg_btf
    • meta.subprogno
    • meta.initialized_dynptr
  2. 回调函数安全性验证

    比如说bpf_rbtree_add,类似的还有 bpf_wq_set_callback、bpf_task_work_schedule_*

    1
    2
    3
    4
    5
    if (is_bpf_rbtree_add_kfunc(meta.func_id)) {
    err = push_callback_call(env, insn, insn_idx, meta.subprogno,
    set_rbtree_add_callback_state);
    ...
    }

  3. bpf_session_cookie 先补一下返回值元信息

    在真正进入后面的返回值建模之前

    先给这个特殊 kfunc 补上返回值大小相关的元信息(R0 应该按 8 字节来理解)

1
2
3
4
if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie]) {
meta.r0_size = sizeof(u64);
meta.r0_rdonly = false;
}

第五段:RCU / preempt 状态机更新

verifier 不只是检查函数调用,它还会修改当前抽象执行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
      rcu_lock = is_kfunc_bpf_rcu_read_lock(&meta);
rcu_unlock = is_kfunc_bpf_rcu_read_unlock(&meta);

preempt_disable = is_kfunc_bpf_preempt_disable(&meta);
preempt_enable = is_kfunc_bpf_preempt_enable(&meta);

if (rcu_lock) {
env->cur_state->active_rcu_locks++;
} else if (rcu_unlock) {
struct bpf_func_state *state;
struct bpf_reg_state *reg;
u32 clear_mask = (1 << STACK_SPILL) | (1 << STACK_ITER);

if (env->cur_state->active_rcu_locks == 0) {
verbose(env, "unmatched rcu read unlock (kernel function %s)\n", func_name);
return -EINVAL;
}

if (--env->cur_state->active_rcu_locks == 0) {
bpf_for_each_reg_in_vstate_mask(env->cur_state, state, reg, clear_mask, ({
if (reg->type & MEM_RCU) {
reg->type &= ~(MEM_RCU | PTR_MAYBE_NULL);
reg->type |= PTR_UNTRUSTED;
}
}));
}
} else if (preempt_disable) {
env->cur_state->active_preempt_locks++;
} else if (preempt_enable) {
if (env->cur_state->active_preempt_locks == 0) {
verbose(env, "unmatched attempt to enable preemption (kernel function %s)\n",
func_name);
return -EINVAL;
}
env->cur_state->active_preempt_locks--;
}

1. bpf_rcu_read_lock():进入 RCU 保护区

1
2
3
if (rcu_lock) {
env->cur_state->active_rcu_locks++;
}

当前状态里,RCU 锁计数加一

后面很多 KF_RCU_PROTECTED 或 MEM_RCU 指针,都依赖这个状态。

2. bpf_rcu_read_unlock():退出保护区,还要“降级旧指针”

1
2
3
4
5
6
7
8
9
10
11
else if (rcu_unlock) {
...
if (--env->cur_state->active_rcu_locks == 0) {
bpf_for_each_reg_in_vstate_mask(env->cur_state, state, reg, clear_mask, ({
if (reg->type & MEM_RCU) {
reg->type &= ~(MEM_RCU | PTR_MAYBE_NULL);
reg->type |= PTR_UNTRUSTED;
}
}));
}
}

这里有两个点

第一:unlock 必须配对

没加过锁就解锁,直接报错

1
2
3
4
if (env->cur_state->active_rcu_locks == 0) {
...
return -EINVAL;
}

第二:unlock 之后,原来受 RCU 保护的指针不再可信

它不是直接把这些寄存器清空,而是把它们降级成:

  • 不再带 MEM_RCU
  • 不再保留原先那种“受保护可安全使用”的语义
  • 转成 PTR_UNTRUSTED
1
2
3
4
if (reg->type & MEM_RCU) {
reg->type &= ~(MEM_RCU | PTR_MAYBE_NULL);
reg->type |= PTR_UNTRUSTED;
}

3. preempt_disable/enable 也是类似的状态机

1
2
3
4
5
6
7
8
9
else if (preempt_disable) {
env->cur_state->active_preempt_locks++;
} else if (preempt_enable) {
if (env->cur_state->active_preempt_locks == 0) {
...
return -EINVAL;
}
env->cur_state->active_preempt_locks--;
}

与RCU的逻辑基本一致:

  • disable:进入一个新状态
  • enable:退出这个状态
  • 不允许“没 disable 就 enable”

第六段:当前小上下文的额外限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (sleepable && !in_sleepable_context(env)) {
verbose(env, "kernel func %s is sleepable within %s\n",
func_name, non_sleepable_context_description(env));
return -EACCES;
}

if (in_rbtree_lock_required_cb(env) && (rcu_lock || rcu_unlock)) {
verbose(env, "Calling bpf_rcu_read_{lock,unlock} in unnecessary rbtree callback\n");
return -EACCES;
}

if (is_kfunc_rcu_protected(&meta) && !in_rcu_cs(env)) {
verbose(env, "kernel func %s requires RCU critical section protection\n", func_name);
return -EACCES;
}

这个也是在看当前状态对不对

1. 程序总体可 sleep,不等于当前这一刻可 sleep

前面已经检查过一次 in_sleepable(env) 了。这里又检查一次 in_sleepable_context(env)

原因是:

  • 程序总体类型可能允许 sleep
  • 但当前这一条路径上,可能由于锁、回调、异常上下文等原因,已经不允许 sleep

2. 在某些 rbtree callback 里,不允许再手动搞 RCU lock/unlock

某些 rbtree 回调本身已经处在特定上下文协议里,这时再手动去调 bpf_rcu_read_lock/unlock,是 verifier 不接受的

这个主要是为了防止在已经被框定好的回调协议里,再人为打乱上下文状态。

3. KF_RCU_PROTECTED:这类 kfunc 必须在 RCU critical section 里调用

只要 kfunc 带 KF_RCU_PROTECTED,那当前就必须真的在 RCU 保护区里

这类检查背后的 safety concern 很明确:

没有 RCU 保护,就不允许访问那些只在 RCU 条件下有效的对象。

第七段:release 语义和 ownership 转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if (meta.release_regno) {
struct bpf_reg_state *reg = &regs[meta.release_regno];

if (meta.initialized_dynptr.ref_obj_id) {
err = unmark_stack_slots_dynptr(env, reg);
} else {
err = release_reference(env, reg->ref_obj_id);
if (err)
verbose(env, "kfunc %s#%d reference has not been acquired before\n",
func_name, meta.func_id);
}
if (err)
return err;
}

if (is_bpf_list_push_kfunc(meta.func_id) || is_bpf_rbtree_add_kfunc(meta.func_id)) {
release_ref_obj_id = regs[BPF_REG_2].ref_obj_id;
insn_aux->insert_off = regs[BPF_REG_2].var_off.value;
insn_aux->kptr_struct_meta = btf_find_struct_meta(meta.arg_btf, meta.arg_btf_id);

err = ref_convert_owning_non_owning(env, release_ref_obj_id);
if (err) {
verbose(env, "kfunc %s#%d conversion of owning ref to non-owning failed\n",
func_name, meta.func_id);
return err;
}

err = release_reference(env, release_ref_obj_id);
if (err) {
verbose(env, "kfunc %s#%d reference has not been acquired before\n",
func_name, meta.func_id);
return err;
}
}

这一段很明显的说明了,有些 kfunc 调用会直接改变对象生命周期关系。

1. meta.release_regno:参数检查阶段已经告诉我“该释放谁”

check_kfunc_args() 已经提前把“哪个参数是 release 对象”写进了 meta.release_regno

2. dynptr 的 release 走的是另一套逻辑

dynptr 比普通 refcounted 对象特殊。它很多时候和栈槽状态绑定得更紧。

所以这里不是简单调 release_reference(),而是:

  • 取消 dynptr 相关的栈槽标记
  • 让 verifier 知道这块栈上的 dynptr 状态已经失效了

3. 普通 release:注销引用 ID

把这个 ref_obj_id 从 verifier 当前持有引用集合里删掉。

这样后面如果程序还继续用这个对象,verifier 就能发现:

  • 这是 release 后继续使用
  • 属于 UAF 风险

4. list_push / rbtree_add:不是单纯 release,而是 ownership 转移

“对象从 BPF 局部变量拥有,转成容器拥有”

也就是说:

  • 插入前:你手里拿的是 owning ref
  • 插入后:这个对象归 list / rbtree 管
  • 你手里的“拥有关系”要被拿掉
  • 但某些地方还可能保留 non-owning 借用视角

所以这里分成两步:

#### 第一步:ref_convert_owning_non_owning()

把当前寄存器副本从“拥有”降成“非拥有”。

#### 第二步:release_reference()

把原先那个真正的 owning reference 从 verifier 跟踪集合里删掉。

第八段:bpf_throw 的特殊处理

bpf_throw 不是普通 kfunc,它和异常机制绑定得很深。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (meta.func_id == special_kfunc_list[KF_bpf_throw]) {
if (!bpf_jit_supports_exceptions()) {
verbose(env, "JIT does not support calling kfunc %s#%d\n",
func_name, meta.func_id);
return -ENOTSUPP;
}
env->seen_exception = true;

if (!env->exception_callback_subprog) {
err = check_return_code(env, BPF_REG_1, "R1");
if (err < 0)
return err;
}
}

这个代码反映了两个规定:

1.底层 JIT 得支持异常

就算 verifier 理论上允许,底层 JIT 如果不支持异常也不能放行

2.如果没有自定义异常回调,R1 会变成整个程序返回值

没有自定义异常回调时,传给 bpf_throw 的 R1最终会作为整个 BPF 程序的返回值

但是这里需要检查R1是否符合返回码的约束

第九段:调用之后,先清理 Caller-Saved 寄存器

1
2
3
4
5
6
for (i = 0; i < CALLER_SAVED_REGS; i++) {
u32 regno = caller_saved[i];

mark_reg_not_init(env, regs, regno);
regs[regno].subreg_def = DEF_NOT_SUBREG;
}

kfunc 调用之后,R1-R5 都不能再信,全部清除。

第十段:返回值检查的总入口

1
2
3
4
5
6
7
8
9
10
11
12

t = btf_type_skip_modifiers(desc_btf, meta.func_proto->type, NULL);

if (is_kfunc_acquire(&meta) && !btf_type_is_struct_ptr(meta.btf, t)) {
if (meta.btf != btf_vmlinux ||
(!is_bpf_obj_new_kfunc(meta.func_id) &&
!is_bpf_percpu_obj_new_kfunc(meta.func_id) &&
!is_bpf_refcount_acquire_kfunc(meta.func_id))) {
verbose(env, "acquire kernel function does not return PTR_TO_BTF_ID\n");
return -EINVAL;
}
}

1.先把返回类型去掉modifiers

btf_type_skip_modifiers,会去掉const,typedef,以及一些修饰层

2.KF_acquire 一般要求返回struct*

acquire 型 kfunc 一般应该返回一个“有明确 BTF 结构体类型的对象指针”。

因为verifier后面要把这个返回值建模成PTR_TO_BTF_ID,并分配给ref_obj_id

如果不是 struct *,就很难把它当作“受生命周期跟踪的内核对象”来管理。

当然这里有少数特殊例外:

  • bpf_obj_new
  • bpf_percpu_obj_new
  • bpf_refcount_acquire

所以后面又开了例外白名单。

第十一段:标量返回值

1
2
3
4
5
6
7
      if (btf_type_is_scalar(t)) {
mark_reg_unknown(env, regs, BPF_REG_0);
if (meta.btf == btf_vmlinux && (meta.func_id ==
special_kfunc_list[KF_bpf_res_spin_lock] ||
meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave]))
__mark_reg_const_zero(env, &regs[BPF_REG_0]);
mark_btf_func_reg_size(env, BPF_REG_0, t->size);

1. 普通标量返回值

mark_reg_unknown(env, regs, BPF_REG_0);

先把R0当作未知量

2. bpf_res_spin_lock* 成功路径返回 0

我们前面已经fork过一条失败返回负错误码的路径

这里就告诉verifier,成功返回R0==0

3. 记录返回值宽度

mark_btf_func_reg_size(env, BPF_REG_0, t->size);

对 R0 的 subreg / 宽度语义做建模,然后告诉 verifier这个返回值到底按 32 位还是 64 位来理解

第十二段:指针返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
} else if (btf_type_is_ptr(t)) {
ptr_type = btf_type_skip_modifiers(desc_btf, t->type, &ptr_type_id);
err = check_special_kfunc(env, &meta, regs, insn_aux, ptr_type, desc_btf);
if (err) {
if (err < 0)
return err;
} else if (btf_type_is_void(ptr_type)) {
mark_reg_unknown(env, regs, BPF_REG_0);
} else if (!__btf_type_is_struct(ptr_type)) {
...
} else {
...
}

最复杂的一个

check_special_kfunc() 的返回语义是:

  • < 0:出错
  • > 0:已经处理完返回值建模了
  • == 0:不是 special kfunc,继续走通用逻辑

1 void *:按未知标量处理

1
2
} else if (btf_type_is_void(ptr_type)) {
mark_reg_unknown(env, regs, BPF_REG_0);

因为verifier不知道void*会指向什么类型,也不知道大小,所以不敢贸然的把它当作普通对象指针来看待

2 非 struct * 指针:尽量建模成 PTR_TO_MEM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
} else if (!__btf_type_is_struct(ptr_type)) {
if (!meta.r0_size) {
__u32 sz;

if (!IS_ERR(btf_resolve_size(desc_btf, ptr_type, &sz))) {
meta.r0_size = sz;
meta.r0_rdonly = true;
}
}
if (!meta.r0_size) {
ptr_type_name = btf_name_by_offset(desc_btf,
ptr_type->name_off);
verbose(env,
"kernel function %s returns pointer type %s %s is not supported\n",
func_name,
btf_type_str(ptr_type),
ptr_type_name);
return -EINVAL;
}

mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].type = PTR_TO_MEM;
regs[BPF_REG_0].mem_size = meta.r0_size;

if (meta.r0_rdonly)
regs[BPF_REG_0].type |= MEM_RDONLY;

if (meta.ref_obj_id)
regs[BPF_REG_0].ref_obj_id = meta.ref_obj_id;

if (is_kfunc_rcu_protected(&meta))
regs[BPF_REG_0].type |= MEM_RCU;

如果返回的是 int 、char 、或者别的非 struct 指针,verifier还是尽量想建模成一个指向有效内存的指针,但是前提是得知道这块内存有多大

有两种方式拿取大小: #### 第一种:之前参数检查阶段已经写进了 meta.r0_size

某些 kfunc 会通过参数名约定,把返回缓冲区大小传进去。

#### 第二种:直接从 BTF 推导 pointee 大小

1
2
3
4
if (!IS_ERR(btf_resolve_size(desc_btf, ptr_type, &sz))) {
meta.r0_size = sz;
meta.r0_rdonly = true;
}

推出来的默认被认为只读内存

如果两种都不行,那么verifier就拒绝

如果最后成功,R0就被建模成 PTR_TO_MEM,大小是 mem_size

以及一些只读、被引用等,再加上一些标志

3 struct *:建模成 PTR_TO_BTF_ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
} else {
enum bpf_reg_type type = PTR_TO_BTF_ID;

if (meta.func_id == special_kfunc_list[KF_bpf_get_kmem_cache])
type |= PTR_UNTRUSTED;
else if (is_kfunc_rcu_protected(&meta) ||
(is_iter_next_kfunc(&meta) &&
(get_iter_from_state(env->cur_state, &meta)
->type & MEM_RCU))) {
type |= MEM_RCU;
} else {
type |= PTR_TRUSTED;
}

mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].btf = desc_btf;
regs[BPF_REG_0].type = type;
regs[BPF_REG_0].btf_id = ptr_type_id;
}

最标准的kfunc返回对象的建模方式

struct*可以被verifier清楚的知道: - 这是什么类型的对象 - 它属于哪个 BTF - 后续字段访问是否安全

并把R0建模成 PTR_TO_BTF_ID,这里还会细分:

  • PTR_UNTRUSTED:bpf_get_kmem_cache 特判成不可信
  • MEM_RCU:RCU 保护下的返回值
  • PTR_TRUSTED:其他普通情况,默认视为 trusted

第十三段:指针返回值的后处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (is_kfunc_ret_null(&meta)) {
regs[BPF_REG_0].type |= PTR_MAYBE_NULL;
regs[BPF_REG_0].id = ++env->id_gen;
}
mark_btf_func_reg_size(env, BPF_REG_0, sizeof(void *));
if (is_kfunc_acquire(&meta)) {
int id = acquire_reference(env, insn_idx);

if (id < 0)
return id;
if (is_kfunc_ret_null(&meta))
regs[BPF_REG_0].id = id;
regs[BPF_REG_0].ref_obj_id = id;
} else if (is_rbtree_node_type(ptr_type) || is_list_node_type(ptr_type)) {
ref_set_non_owning(env, &regs[BPF_REG_0]);
}
if (reg_may_point_to_spin_lock(&regs[BPF_REG_0]) && !regs[BPF_REG_0].id)
regs[BPF_REG_0].id = ++env->id_gen;

1. KF_RET_NULL:给 R0 打上 PTR_MAYBE_NULL

1
2
3
4
if (is_kfunc_ret_null(&meta)) {
regs[BPF_REG_0].type |= PTR_MAYBE_NULL;
regs[BPF_REG_0].id = ++env->id_gen;
}

如果返回指针不是一定非空,后续调用得先判空

2. 指针返回值的寄存器宽度统一按指针大小处理

1
mark_btf_func_reg_size(env, BPF_REG_0, sizeof(void *));

指针返回值,R0 的宽度就按指针大小建模

3.KF_ACQUIRE:真正生成新的引用 ID

1
2
3
4
5
if (is_kfunc_acquire(&meta)) {
int id = acquire_reference(env, insn_idx);
...
regs[BPF_REG_0].ref_obj_id = id;
}

这个的意思是,这次调用成功后,R0 现在持有一个新的 owning reference

如果是 nullable acquire,会把“判空 ID”和“引用 ID”对齐

4. list/rbtree 节点返回值:不是 owning,是 non-owning

这个返回值虽然是对象指针,但它不是一个“我拥有的引用”,它只是一个“借来的节点指针”

5. 如果可能指向 spin_lock,就补一个唯一 ID

主要是给锁加一个id,区分不同的锁实例

第十四段:void 返回值

1
2
3
4
5
6
7
8
9
10
} else if (btf_type_is_void(t)) {
if (meta.btf == btf_vmlinux) {
if (is_bpf_obj_drop_kfunc(meta.func_id) ||
is_bpf_percpu_obj_drop_kfunc(meta.func_id)) {
insn_aux->kptr_struct_meta =
btf_find_struct_meta(meta.arg_btf,
meta.arg_btf_id);
}
}
}

如果返回类型是 void,一般不需要像指针 / 标量那样去建模 R0

但是bpf_obj_drop / bpf_percpu_obj_drop 这种 release 类 kfunc,需要记录它drop的到底是什么对象结构

第十五段:收尾后处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (is_kfunc_pkt_changing(&meta))
clear_all_pkt_pointers(env);

nargs = btf_type_vlen(meta.func_proto);
args = (const struct btf_param *)(meta.func_proto + 1);
for (i = 0; i < nargs; i++) {
u32 regno = i + 1;
t = btf_type_skip_modifiers(desc_btf, args[i].type, NULL);
if (btf_type_is_ptr(t))
mark_btf_func_reg_size(env, regno, sizeof(void *));
else
mark_btf_func_reg_size(env, regno, t->size);
}

if (is_iter_next_kfunc(&meta)) {
err = process_iter_next_call(env, insn_idx, &meta);
if (err)
return err;
}

if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie])
env->prog->call_session_cookie = true;

return 0;

1. 改包 kfunc 之后,让旧 packet pointer 全部失效

1
2
if (is_kfunc_pkt_changing(&meta))
clear_all_pkt_pointers(env);

如果这个 kfunc 会改变 packet 数据布局,那么之前拿到的包指针就不能继续信了

最典型的就是 bpf_xdp_pull_data 这种。

2. 再次按 BTF 原型修正参数寄存器的宽度信息

1
2
3
4
5
6
7
8
9
10
nargs = btf_type_vlen(meta.func_proto);
args = (const struct btf_param *)(meta.func_proto + 1);
for (i = 0; i < nargs; i++) {
u32 regno = i + 1;
t = btf_type_skip_modifiers(desc_btf, args[i].type, NULL);
if (btf_type_is_ptr(t))
mark_btf_func_reg_size(env, regno, sizeof(void *));
else
mark_btf_func_reg_size(env, regno, t->size);
}

R1-R5 虽然刚刚已经被当作 caller-saved 清掉了,但 verifier 仍然希望保留“这次调用的参数宽度语义”,这样后续一些子寄存器 / 零扩展 / 类型宽度相关逻辑才能更准确。更像是一种记账

3. 如果这是 iterator 的 next(),还要额外验证“循环最终会不会收敛”

1
2
3
4
5
if (is_iter_next_kfunc(&meta)) {
err = process_iter_next_call(env, insn_idx, &meta);
if (err)
return err;
}

主要是为了验证, 会不会形成 verifier 证明不了安全性的死循环。这里调用 process_iter_next_call(),本质上是在更新 iterator 状态机。

4. bpf_session_cookie:在 prog 上打一个特征标记

1
2
3
4
if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie])
env->prog->call_session_cookie = true;

return 0;

verifier 做的事情不只是“检查这一次调用”,有时候它还会把“这个程序用过某种特殊能力”记到 prog 上。譬如这里的 call_session_cookie = true

后面 trampoline / JIT 看到这个标记时,就知道:这个 BPF 程序需要 session cookie 相关的额外处理。这也体现了verifier也在维护整个程序的附加语义信息。

具体来说,它会依次做这些事:

  1. 识别这次调用是谁
  2. 检查当前程序和上下文是否允许调用
  3. 检查参数是否合法
  4. 必要时推进 callback / iter / dynptr / RCU / lock 等协议状态机
  5. 处理 release 和 ownership 转移
  6. 建模返回值 R0
  7. 做调用后的状态失效和程序级标记收尾

kfunc 的安全性同时涉及:类型、生命周期、引用计数、锁、RCU、sleepability、callback、iterator、packet pointer 失效、JIT / trampoline 后端能力等多项

三、建立一些“约束词表”,从 include/linux/btf.h:18 的 KF_* 开始

这一步先定义大类:acquire/release、nullable、sleepable、RCU、RCU_PROTECTED、iter、destructive、implicit args、arena。这是我后面所有标注的一级标签。

为什么要先做这个?

因为后面不管分析哪个 kfunc,最后都绕不开这些问题:

  • 它会不会返回一个新引用?
  • 它会不会返回 NULL?
  • 它能不能 sleep?
  • 它是不是要求 RCU 保护?
  • 它是不是 iterator 的 next()?
  • 它有没有 verifier 帮忙补隐式参数?
  • 它是不是 arena 相关的特殊内存语义?

如果不先把这些一级标签想清楚,后面看任何具体 kfunc 都会越看越散。

所以这一节的目标不是分析某一个 kfunc,而是先建立一套“研究语言”。

3.1 KF_*是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* These need to be macros, as the expressions are used in assembler input */
#define KF_ACQUIRE (1 << 0) /* kfunc is an acquire function */
#define KF_RELEASE (1 << 1) /* kfunc is a release function */
#define KF_RET_NULL (1 << 2) /* kfunc returns a pointer that may be NULL */
#define KF_SLEEPABLE (1 << 5) /* kfunc may sleep */
#define KF_DESTRUCTIVE (1 << 6) /* kfunc performs destructive actions */
#define KF_RCU (1 << 7) /* kfunc takes either rcu or trusted pointer arguments */
/* only one of KF_ITER_{NEW,NEXT,DESTROY} could be specified per kfunc */
#define KF_ITER_NEW (1 << 8) /* kfunc implements BPF iter constructor */
#define KF_ITER_NEXT (1 << 9) /* kfunc implements BPF iter next method */
#define KF_ITER_DESTROY (1 << 10) /* kfunc implements BPF iter destructor */
#define KF_RCU_PROTECTED (1 << 11) /* kfunc should be protected by rcu cs when they are invoked */
#define KF_FASTCALL (1 << 12) /* kfunc supports bpf_fastcall protocol */
#define KF_ARENA_RET (1 << 13) /* kfunc returns an arena pointer */
#define KF_ARENA_ARG1 (1 << 14) /* kfunc takes an arena pointer as its first argument */
#define KF_ARENA_ARG2 (1 << 15) /* kfunc takes an arena pointer as its second argument */
#define KF_IMPLICIT_ARGS (1 << 16) /* kfunc has implicit arguments supplied by the verifier */

KF_* 就是“贴在 kfunc 身上的语义标签”。它是一种verifier与kfunc之间约定好的一组附加语义

比如说:

1
BTF_ID_FLAGS(func, bpf_obj_new, KF_ACQUIRE | KF_RET_NULL | KF_IMPLICIT_ARGS)

意思就是bpf_obj_new 同时带有这三种语义。

也可以看出,一个 kfunc 往往不是只有一个标签,而是多个标签叠加在一起。

把KF_*称作一级标签的原因,主要还是因为他是大致方向

后面更细的参数约束,后面还有具体看

参数名字后缀,比如 __sz、__nullable;参数具体 BTF 类型;check_kfunc_args();get_kfunc_ptr_arg_type();process_kf_arg_*()

3.2 第一类标签:acquire / release

### 1. KF_ACQUIRE

1
#define KF_ACQUIRE    (1 << 0) /* kfunc is an acquire function */

kfunc 调用成功之后,你“拿到了一个新的引用/所有权”

比如:

  1. bpf_obj_new():新分配一个对象给你

  2. bpf_task_acquire():给你一个新的 task 引用

  3. bpf_list_pop_front():把节点从 list 里摘出来,交给你

也就是说,这个对象归我管了,但是这种语义,在verifier中,通常意味着返回值要被建模成一种“可跟踪生命周期的对象指针”,并且后面通常必须能找到对应的 release,verifier 要给这个返回值分配 ref_obj_id

所以,他的核心safe concern一般是: - 引用泄漏 - release 之后继续使用 - 忘记 release - 生命周期建模错误

2. KF_RELEASE

1
#define KF_RELEASE    (1 << 1) /* kfunc is a release function */

这个kfuc就会消耗的手里有的那个引用

比如: 1. bpf_obj_drop():把对象 drop 掉

  1. bpf_task_release():把 task 引用释放掉

  2. bpf_key_put():减少 key 引用

这个时候,verifier就会关心,1.传进来的参数里,到底哪个才是“要被释放的对象”;2.这个对象是不是真的之前 acquire 过;3.会不会出现 double free;4.会不会出现“释放了一个自己根本不拥有的对象”

所以,他的核心safe concern一般是:

  • double free
  • 释放未持有对象
  • release 后继续使用
  • 生命周期不平衡

acquire/release本质上是一套配对语义:

​ acquire:创建或得到一个拥有关系

​ release:销毁一个拥有关系

所以后面做逐个 kfunc 标注时,可以先问两个问题:

  1. 这个函数会不会产生 owning ref?
  2. 这个函数会不会消耗 owning ref?

这样,生命周期大体就出来了。

3.3 第二类标签:nullable

1.KF_RET_NULL

1
#define KF_RET_NULL   (1 << 2) /* kfunc returns a pointer that may be NULL */

这个 kfunc 的返回值不是“肯定有对象”,而是“有可能是空”。

比如:

  • bpf_obj_new():分配失败时可能返回 NULL
  • bpf_list_front():list 空的时候会返回 NULL
  • bpf_iter_num_next():迭代结束时会返回 NULL

但是,KF_RET_NULL只告诉你,返回值可能为空,但是不告诉你为什么为空,需要结合具体的API才可以分析

在verifier中,这个标签通常意味着:

​ R0 要带 PTR_MAYBE_NULL

​ 后续程序如果要安全使用它,通常需要先判空

​ nullable acquire 场景下,还要正确处理“有引用”和“没引用”的分支

所以,他的核心safe concern一般是:

  • NULL deref
  • 判空路径与非空路径的状态混淆

3.4 第三类标签:sleepable

1.KF_SLEEPABLE

1
#define KF_SLEEPABLE    (1 << 5) /* kfunc may sleep */

一些典型例子:

1
2
3
BTF_ID_FLAGS(func, bpf_lookup_user_key, KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_verify_pkcs7_signature, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_crypto_ctx_create, KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE)

这个kfunc可能会睡眠,不是任何一个地方都能调用的

注意,能不能sleep不是参数问题,而是调用上下文问题

所以,KF_SLEEPABLE的安全关注点,通常是一些: - 在错误上下文里阻塞 - 破坏当前执行语义 - 导致一些 verifier 不允许出现的上下文切换 等等

更偏向于context safety

3.5 第四类标签:RCU

1. KF_RCU

1
#define KF_RCU          (1 << 7) /* kfunc takes either rcu or trusted pointer arguments */

例如:bpf_task_acquire,bpf_cgroup_acquire, bpf_crypto_encrypt

带有这个语义标签的kfunc对传入的指针来源很敏感,一般要求:trusted pointer或者 RCU 语义下可接受的 pointer

所以这类典型的 safety concern 是:

  • 访问已经悬空的对象
  • 在不可信对象上做操作
  • 并发下对象已经死了,但你还拿它继续用

2. KF_RCU_PROTECTED

1
#define KF_RCU_PROTECTED (1 << 11) /* kfunc should be protected by rcu cs when they are invoked */

它和 KF_RCU 的区别:

  • KF_RCU:更像“参数要是可信 / RCU 语义对象”
  • KF_RCU_PROTECTED:更像“调用点本身必须处在 RCU critical section 里”

也就是说,KF_RCU_PROTECTED 对当前执行状态提出了更强约束。

那么这个safety consern 更直接:

  • 确少RCU读侧保护
  • 对只在RCU条件下有效对象做访问

KF_RCU:更偏参数来源和对象活性

KF_RCU_PROTECTED:更偏调用点上下文约束

3.6 第五类标签:iter

1
2
3
#define KF_ITER_NEW     (1 << 8) /* kfunc implements BPF iter constructor */
#define KF_ITER_NEXT (1 << 9) /* kfunc implements BPF iter next method */
#define KF_ITER_DESTROY (1 << 10) /* kfunc implements BPF iter destructor */

这三个标签的意思就是,这个 kfunc 属于一个“iterator 协议”的某个阶段

  • NEW:创建 / 初始化 iterator 状态
  • NEXT:推进 iterator
  • DESTROY:销毁 iterator 状态

所以,关注点就变了,主要是:1.iterator的状态是不是初始化了 2.是不是栈上的正确对象 3.next()最终会不会收敛到NULL 4.destroy()的调用

所以这一类对应的 safety concern 更偏 protocol safety:

  • 状态机走错
  • 循环无法证明收敛
  • 资源没销毁
  • 销毁后还继续使用

3.7 第六类标签:destructive

KF_DESTRUCTIVE

1
#define KF_DESTRUCTIVE  (1 << 6) /* kfunc performs destructive actions */

典型例子就是BTF_ID_FLAGS(func, crash_kexec, KF_DESTRUCTIVE)

意思是这个kfunc不是普通辅助函数,它会做”破坏性动作”

由于影响较大,所以第一个检查的是资格,一些经典约束比如 capability 检查、程序权限边界

对应的saafety concern 是: - 非授权破坏性操作 - 直接影响系统稳定性的动作被滥用

3.8 第七类标签:implicit args

1
#define KF_IMPLICIT_ARGS (1 << 16) /* kfunc has implicit arguments supplied by the verifier */

这个 kfunc 的真实实现原型里,有些参数不是 BPF 程序员自己传的,而是 verifier 帮你补进去 / 改写的。

最典型的例子就是:

bpf_obj_new() 里的 struct btf_struct_meta *meta bpf_list_push_front() 里的 meta 和 off bpf_wq_set_callback() 里的某些 verifier 辅助信息

“BPF 暴露出来的调用原型”和“内核真实处理的原型”是不一定一样的

3.9 第八类标签:arena

1
2
3
#define KF_ARENA_RET    (1 << 13) /* kfunc returns an arena pointer */
#define KF_ARENA_ARG1 (1 << 14) /* kfunc takes an arena pointer as its first argument */
#define KF_ARENA_ARG2 (1 << 15) /* kfunc takes an arena pointer as its second argument */

一些例子:

1
2
3
BTF_ID_FLAGS(func, bpf_arena_alloc_pages, KF_ARENA_RET | KF_ARENA_ARG2)
BTF_ID_FLAGS(func, bpf_arena_free_pages, KF_ARENA_ARG2)
BTF_ID_FLAGS(func, bpf_arena_reserve_pages, KF_ARENA_ARG2)

这个 kfunc 涉及一种特殊内存域:arena。

这类标签对应的是一种特殊内存域语义,而不是传统的“普通 BTF 对象指针”。

需要判断,哪个参数是 arena map / arena pointer、返回值是不是 arena pointer、后续这个指针能做什么,往往和 arena 规则绑定

对应的 safety concern 主要是:

  • 指针 provenance 错误
  • arena 内存边界 / 归属语义错误
  • 把 arena pointer 当成普通 pointer 使用
一级标签 对应 KF_* 我目前的理解
acquire KF_ACQUIRE 成功后得到一个新的 owning ref
release KF_RELEASE 调用会消耗 / 释放已有引用
nullable KF_RET_NULL 返回指针可能为 NULL
sleepable KF_SLEEPABLE 调用可能 sleep,需要正确上下文
RCU KF_RCU 参数对象需要 trusted / RCU 语义
RCU_PROTECTED KF_RCU_PROTECTED 调用点必须处于 RCU critical section
iter-new KF_ITER_NEW iterator 构造阶段
iter-next KF_ITER_NEXT iterator 推进阶段
iter-destroy KF_ITER_DESTROY iterator 销毁阶段
destructive KF_DESTRUCTIVE 破坏性操作,需要更高权限
implicit-args KF_IMPLICIT_ARGS verifier 会补 / 改一些隐式参数
arena-ret KF_ARENA_RET 返回 arena pointer
arena-arg KF_ARENA_ARG1/ARG2 某个参数是 arena pointer

四、参数检查主链

参数检查这一部分真正的核心就是:

check_kfunc_args()

get_kfunc_ptr_arg_type()

is_kfunc_arg_*

process_kf_arg_ptr_to_btf_id()

process_kf_arg_ptr_to_list_head/root/node()

process_iter_arg()

process_dynptr_func()、process_irq_flag()等协议型处理函数

这一部分解决,为什么某个kfunc的输入参数,必须是某种特定的类型

也是防止一些安全问题:越界访问、NULL指针、类型混淆、UAF,double free、锁或者RCU条件的错误访问,以及一些协议对象的错误使用

4.1 参数检查主链总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_arg_meta *meta,
int insn_idx)
{
args = (const struct btf_param *)(meta->func_proto + 1);
nargs = btf_type_vlen(meta->func_proto);

for (i = 0; i < nargs; i++) {
struct bpf_reg_state *regs = cur_regs(env), *reg = &regs[i + 1];
const struct btf_type *t, *ref_t, *resolve_ret;
enum bpf_arg_type arg_type = ARG_DONTCARE;
u32 regno = i + 1, ref_id, type_size;
bool is_ret_buf_sz = false;
int kf_arg_type;

if (is_kfunc_arg_prog_aux(btf, &args[i]))
...
if (is_kfunc_arg_ignore(btf, &args[i]) || is_kfunc_arg_implicit(meta, i))
continue;

t = btf_type_skip_modifiers(btf, args[i].type, NULL);

if (btf_type_is_scalar(t)) {
...
continue;
}

if (!btf_type_is_ptr(t)) {
...
return -EINVAL;
}

...
kf_arg_type = get_kfunc_ptr_arg_type(...);
...
ret = check_func_arg_reg_off(env, reg, regno, arg_type);
...
switch (kf_arg_type) {
case KF_ARG_PTR_TO_CTX:
...
case KF_ARG_PTR_TO_BTF_ID:
...
case KF_ARG_PTR_TO_MEM:
...
case KF_ARG_PTR_TO_DYNPTR:
...
case KF_ARG_PTR_TO_ITER:
...
}
}

if (is_kfunc_release(meta) && !meta->release_regno) {
...
return -EINVAL;
}

return 0;
}

先从BTF原型里面获取参数列表,然后逐个参数遍历,跳过 __ign 和 implicit args,再先判断是 scalar 还是 pointer,对pointer参数做一个语义分类:kf_arg_type,最后再进入不同参数类型的专门处理逻辑

总结来说就是识别然后分类参数,然后进行对应的检查

4.2 参数分类:is_kfunc_arg_* 和 get_kfunc_ptr_arg_type()

4.2.1 is_kfunc_arg_*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static bool is_kfunc_arg_mem_size(const struct btf *btf,
const struct btf_param *arg,
const struct bpf_reg_state *reg)
{
...
return btf_param_match_suffix(btf, arg, "__sz");
}

static bool is_kfunc_arg_const_mem_size(const struct btf *btf,
const struct btf_param *arg,
const struct bpf_reg_state *reg)
{
...
return btf_param_match_suffix(btf, arg, "__szk");
}

static bool is_kfunc_arg_constant(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__k");
}

static bool is_kfunc_arg_nullable(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__nullable");
}

static bool is_kfunc_arg_const_str(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__str");
}

static bool is_kfunc_arg_irq_flag(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__irq_flag");
}

通过参数名后缀,给参数附加语义。

kfunc 的“参数语义”,有一部分是编码在 BTF 参数名里的。

  • __k:必须是已知常量
  • __sz:这是内存长度参数
  • __szk:这是长度参数,而且也必须是常量
  • __nullable:允许传 NULL
  • __str:这是常量字符串
  • __irq_flag:这是 irq flag 协议对象

4.2.2 特殊结构体类型识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static bool is_kfunc_arg_dynptr(const struct btf *btf, const struct btf_param *arg)
{
return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_DYNPTR_ID);
}

static bool is_kfunc_arg_list_head(const struct btf *btf, const struct btf_param *arg)
{
return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_LIST_HEAD_ID);
}

static bool is_kfunc_arg_list_node(const struct btf *btf, const struct btf_param *arg)
{
return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_LIST_NODE_ID);
}

static bool is_kfunc_arg_rbtree_root(const struct btf *btf, const struct btf_param *arg)
{
return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_RB_ROOT_ID);
}

static bool is_kfunc_arg_rbtree_node(const struct btf *btf, const struct btf_param *arg)
{
return __is_kfunc_ptr_arg_type(btf, arg, KF_ARG_RB_NODE_ID);
}

如果参数不是普通指针,而是指向某些 verifier 认识的“协议对象”,那 verifier 会把它们单独分类

struct bpf_dynptr *

struct bpf_list_head *

struct bpf_list_node *

struct bpf_rb_root *

struct bpf_rb_node *

struct bpf_timer *

struct bpf_wq *

### 4.2.3 分类中心:get_kfunc_ptr_arg_type()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static enum kfunc_ptr_arg_type
get_kfunc_ptr_arg_type(struct bpf_verifier_env *env,
struct bpf_kfunc_call_arg_meta *meta,
const struct btf_type *t, const struct btf_type *ref_t,
const char *ref_tname, const struct btf_param *args,
int argno, int nargs)
{
...

if (btf_is_prog_ctx_type(...))
return KF_ARG_PTR_TO_CTX;

if (is_kfunc_arg_nullable(meta->btf, &args[argno]) && register_is_null(reg) &&
!arg_mem_size)
return KF_ARG_PTR_TO_NULL;

if (is_kfunc_arg_alloc_obj(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_ALLOC_BTF_ID;

if (is_kfunc_arg_refcounted_kptr(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_REFCOUNTED_KPTR;

if (is_kfunc_arg_dynptr(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_DYNPTR;

if (is_kfunc_arg_iter(meta, argno, &args[argno]))
return KF_ARG_PTR_TO_ITER;

if (is_kfunc_arg_list_head(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_LIST_HEAD;

if (is_kfunc_arg_list_node(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_LIST_NODE;

if (is_kfunc_arg_rbtree_root(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_RB_ROOT;

if (is_kfunc_arg_rbtree_node(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_RB_NODE;

if (is_kfunc_arg_const_str(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_CONST_STR;

if (is_kfunc_arg_map(meta->btf, &args[argno]))
return KF_ARG_PTR_TO_MAP;

...

if ((base_type(reg->type) == PTR_TO_BTF_ID || reg2btf_ids[base_type(reg->type)])) {
...
return KF_ARG_PTR_TO_BTF_ID;
}

if (is_kfunc_arg_callback(env, meta->btf, &args[argno]))
return KF_ARG_PTR_TO_CALLBACK;

...
return arg_mem_size ? KF_ARG_PTR_TO_MEM_SIZE : KF_ARG_PTR_TO_MEM;
}

把指针参数分成verifier内部能理解的若干类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- PTR_TO_CTX
- PTR_TO_NULL
- PTR_TO_ALLOC_BTF_ID
- PTR_TO_REFCOUNTED_KPTR
- PTR_TO_DYNPTR
- PTR_TO_ITER
- PTR_TO_LIST_HEAD
- PTR_TO_LIST_NODE
- PTR_TO_RB_ROOT
- PTR_TO_RB_NODE
- PTR_TO_CONST_STR
- PTR_TO_MAP
- PTR_TO_WORKQUEUE
- PTR_TO_TIMER
- PTR_TO_TASK_WORK
- PTR_TO_IRQ_FLAG
- PTR_TO_RES_SPIN_LOCK
- PTR_TO_BTF_ID
- PTR_TO_CALLBACK
- PTR_TO_MEM
- PTR_TO_MEM_SIZE

4.3 BTF 对象参数检查:process_kf_arg_ptr_to_btf_id()

这一部分处理的是 BTF 结构体对象指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case KF_ARG_PTR_TO_ALLOC_BTF_ID:
case KF_ARG_PTR_TO_BTF_ID:
if (!is_trusted_reg(reg)) {
if (!is_kfunc_rcu(meta)) {
verbose(env, "R%d must be referenced or trusted\n", regno);
return -EINVAL;
}
if (!is_rcu_reg(reg)) {
verbose(env, "R%d must be a rcu pointer\n", regno);
return -EINVAL;
}
}
...
break;

如果kfunc参数是一个BTF对象指针,verifier先看指针来源可不可信,普通情况:它必须是 trusted / referenced pointer;如果这个 kfunc 带 KF_RCU:那至少也得是 RCU pointer

4.3.1 真正的类型匹配逻辑process_kf_arg_ptr_to_btf_id()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int process_kf_arg_ptr_to_btf_id(struct bpf_verifier_env *env,
struct bpf_reg_state *reg,
const struct btf_type *ref_t,
const char *ref_tname, u32 ref_id,
struct bpf_kfunc_call_arg_meta *meta,
int argno)
{
...

if ((is_kfunc_release(meta) && reg->ref_obj_id) ||
btf_type_ids_nocast_alias(&env->log, reg_btf, reg_ref_id, meta->btf, ref_id))
strict_type_match = true;

...

struct_same = btf_struct_ids_match(&env->log, reg_btf, reg_ref_id, reg->var_off.value,
meta->btf, ref_id, strict_type_match);

taking_projection = btf_is_projection_of(ref_tname, reg_ref_tname);
if (!taking_projection && !struct_same) {
verbose(env, "kernel function %s args#%d expected pointer to %s %s but R%d has a pointer to %s %s\n",
meta->func_name, argno, btf_type_str(ref_t), ref_tname, argno + 1,
btf_type_str(reg_ref_t), reg_ref_tname);
return -EINVAL;
}
return 0;
}

1
2
3
4
5
6
/* Enforce strict type matching for calls to kfuncs that are acquiring
* or releasing a reference, or are no-cast aliases. We do _not_
* enforce strict matching for kfuncs by default,
* as we want to enable BPF programs to pass types that are bitwise
* equivalent ...
*/

对 acquire/release 这类敏感操作,要严格匹配

对一般 kfunc,默认可以稍微放宽

允许一些“位级兼容”的 struct 类型通过

允许 projection type 的语义

我记得这一块可以总结成两层concern:

第一层是直接的:

  • 类型混淆
  • 把错误对象传给 release/acquire
  • 对错误对象做字段访问

第二层是 verifier 建模层面的:

  • verifier 必须知道这到底是哪类对象
  • 否则后面的 ref_obj_id、生命周期、字段访问都没法算

4.4 list/rbtree 参数检查

主要检查下面几块: - 是不是在操作正确的容器字段 - 这个字段 offset 对不对 - 你是不是在正确的锁保护下 - 节点和根是不是同一类对象 - 当前这个节点是 owning 还是 non-owning

4.4.1 graph root:list_head / rb_root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
static int
__process_kf_arg_ptr_to_graph_root(struct bpf_verifier_env *env,
struct bpf_reg_state *reg, u32 regno,
struct bpf_kfunc_call_arg_meta *meta,
enum btf_field_type head_field_type,
struct btf_field **head_field)
{
...

if (!tnum_is_const(reg->var_off)) {
verbose(env,
"R%d doesn't have constant offset. %s has to be at the constant offset\n",
regno, head_type_name);
return -EINVAL;
}

rec = reg_btf_record(reg);
head_off = reg->var_off.value;
field = btf_record_find(rec, head_off, head_field_type);
if (!field) {
verbose(env, "%s not found at offset=%u\n", head_type_name, head_off);
return -EINVAL;
}

/* All functions require bpf_list_head to be protected using a bpf_spin_lock */
if (check_reg_allocation_locked(env, reg)) {
verbose(env, "bpf_spin_lock at off=%d must be held for %s\n",
rec->spin_lock_off, head_type_name);
return -EINVAL;
}

...
*head_field = field;
return 0;
}

有几个点:

第一:为什么一定要 constant offset

1
2
3
4
if (!tnum_is_const(reg->var_off)) {
...
return -EINVAL;
}

这个寄存器必须稳定地指向结构体里某个固定 offset 的 graph field,如果offest不是常量,那么他是不是bpf_list_head,是不是bpf_rb_root,会不会落到别的字段上等等这些都不确定

主要是预防 错字节访问,数据结构破坏,verifier无法精确建模等

第二:为什么必须持有 bpf_spin_lock

对 list/rbtree 这种容器来说,问题不只是“这个字段是不是 head/root”,还包括

你现在有没有资格安全地操作它。

所以,如果没有正确的持锁的话,可能出现并发修改、结果损坏、节点状态竞态等问题

4.4.2 graph node:list_node / rb_node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int
__process_kf_arg_ptr_to_graph_node(struct bpf_verifier_env *env,
struct bpf_reg_state *reg, u32 regno,
struct bpf_kfunc_call_arg_meta *meta,
enum btf_field_type head_field_type,
enum btf_field_type node_field_type,
struct btf_field **node_field)
{
...

if (!tnum_is_const(reg->var_off)) {
verbose(env,
"R%d doesn't have constant offset. %s has to be at the constant offset\n",
regno, node_type_name);
return -EINVAL;
}

node_off = reg->var_off.value;
field = reg_find_field_offset(reg, node_off, node_field_type);
if (!field) {
verbose(env, "%s not found at offset=%u\n", node_type_name, node_off);
return -EINVAL;
}

field = *node_field;

et = btf_type_by_id(field->graph_root.btf, field->graph_root.value_btf_id);
t = btf_type_by_id(reg->btf, reg->btf_id);
if (!btf_struct_ids_match(&env->log, reg->btf, reg->btf_id, 0, field->graph_root.btf,
field->graph_root.value_btf_id, true)) {
verbose(env, "operation on %s expects arg#1 %s at offset=%d "
"in struct %s, but arg is at offset=%d in struct %s\n",
...
);
return -EINVAL;
}

...
if (node_off != field->graph_root.node_offset) {
verbose(env, "arg#1 offset=%d, but expected %s at offset=%d in struct %s\n",
...
);
return -EINVAL;
}

return 0;
}

主要是验证,这个 node,是不是属于这个 root/head 所在容器协议的那个 node

具体要求如下: - node 本身也必须位于常量 offset - 该 offset 上必须真有 list_node / rb_node - 当前 node 所属结构体类型,必须和 root 记录的 value type 匹配 - offset 也必须和 root 期待的 node offset 一致

防止 容器与节点的错误匹配,节点错误插入,offest错误导致数据结构损坏

4.4.3 list/rbtree的ownership 约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
case KF_ARG_PTR_TO_LIST_HEAD:
if (reg->type != PTR_TO_MAP_VALUE &&
reg->type != (PTR_TO_BTF_ID | MEM_ALLOC)) {
verbose(env, "arg#%d expected pointer to map value or allocated object\n", i);
return -EINVAL;
}
...
ret = process_kf_arg_ptr_to_list_head(env, reg, regno, meta);
break;

case KF_ARG_PTR_TO_LIST_NODE:
if (reg->type != (PTR_TO_BTF_ID | MEM_ALLOC)) {
verbose(env, "arg#%d expected pointer to allocated object\n", i);
return -EINVAL;
}
if (!reg->ref_obj_id) {
verbose(env, "allocated object must be referenced\n");
return -EINVAL;
}
ret = process_kf_arg_ptr_to_list_node(env, reg, regno, meta);
break;

以及 rbtree:

case KF_ARG_PTR_TO_RB_NODE:
if (is_bpf_rbtree_add_kfunc(meta->func_id)) {
if (reg->type != (PTR_TO_BTF_ID | MEM_ALLOC)) {
...
return -EINVAL;
}
if (!reg->ref_obj_id) {
...
return -EINVAL;
}
} else {
if (!type_is_non_owning_ref(reg->type) && !reg->ref_obj_id) {
verbose(env, "%s can only take non-owning or refcounted bpf_rb_node pointer\n", func_name);
return -EINVAL;
}
if (in_rbtree_lock_required_cb(env)) {
verbose(env, "%s not allowed in rbtree cb\n", func_name);
return -EINVAL;
}
}
...

add/push 这种“插入容器”的操作,通常要求你手里有 owning ref

remove/left/right 这种操作,则允许 non-owning 或 refcounted 节点

某些场景还禁止在 rbtree callback 里调用

也就是说,我们要知道,什么时候你必须拥有这个节点,什么时候借用也可以,节点进入容器之后所有权语义怎么变

4.5 dynptr / iter / callback / irq 这类协议型参数

核心问题是,有没有遵守协议,协议有没有走对

### 4.5.1 dynptr:初始化协议 + 类型协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
case KF_ARG_PTR_TO_DYNPTR:
{
enum bpf_arg_type dynptr_arg_type = ARG_PTR_TO_DYNPTR;
int clone_ref_obj_id = 0;

if (reg->type == CONST_PTR_TO_DYNPTR)
dynptr_arg_type |= MEM_RDONLY;

if (is_kfunc_arg_uninit(btf, &args[i]))
dynptr_arg_type |= MEM_UNINIT;

if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_skb]) {
dynptr_arg_type |= DYNPTR_TYPE_SKB;
} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_xdp]) {
dynptr_arg_type |= DYNPTR_TYPE_XDP;
} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_from_file]) {
dynptr_arg_type |= DYNPTR_TYPE_FILE;
} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_file_discard]) {
dynptr_arg_type |= DYNPTR_TYPE_FILE;
meta->release_regno = regno;
}

ret = process_dynptr_func(env, regno, insn_idx, dynptr_arg_type, clone_ref_obj_id);
...
if (!(dynptr_arg_type & MEM_UNINIT)) {
meta->initialized_dynptr.id = dynptr_id(env, reg);
meta->initialized_dynptr.type = dynptr_get_type(env, reg);
meta->initialized_dynptr.ref_obj_id = dynptr_ref_obj_id(env, reg);
}
break;
}

对参数的检查,主要关心的是: - dynptr 是不是只读 - 是不是未初始化 - 它是哪种 dynptr 类型 - 有没有底层 ref_obj_id - 某些操作会不会消耗它

比如:from_* 类函数:构造 dynptr;discard:释放相关状态;clone:需要把父 dynptr 的类型和 ref 关系传过去

所以 dynptr 的参数检查,本质上在防:未初始化使用

4.5.2 iter:必须是栈上对象,而且 new/next/destroy 协议要对

先看 is_kfunc_arg_iter():

1
2
3
4
5
6
7
8
static bool is_kfunc_arg_iter(struct bpf_kfunc_call_arg_meta *meta, int arg_idx,
const struct btf_param *arg)
{
if (is_iter_kfunc(meta))
return arg_idx == 0;

return btf_param_match_suffix(meta->btf, arg, "__iter");
}

对 iterator kfunc 本身,第一参数默认就是 iter state

对普通 kfunc,如果参数名带 __iter,也会被当成 iter 参数

再看 process_iter_arg():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static int process_iter_arg(struct bpf_verifier_env *env, int regno, int insn_idx,
struct bpf_kfunc_call_arg_meta *meta)
{
struct bpf_reg_state *reg = reg_state(env, regno);
...

if (reg->type != PTR_TO_STACK) {
verbose(env, "arg#%d expected pointer to an iterator on stack\n", regno - 1);
return -EINVAL;
}

btf_id = btf_check_iter_arg(meta->btf, meta->func_proto, regno - 1);
if (btf_id < 0) {
verbose(env, "expected valid iter pointer as arg #%d\n", regno - 1);
return -EINVAL;
}

if (is_iter_new_kfunc(meta)) {
if (!is_iter_reg_valid_uninit(env, reg, nr_slots)) {
verbose(env, "expected uninitialized iter_%s as arg #%d\n",
iter_type_str(meta->btf, btf_id), regno - 1);
return -EINVAL;
}
...
err = mark_stack_slots_iter(env, meta, reg, insn_idx, meta->btf, btf_id, nr_slots);
} else {
err = is_iter_reg_valid_init(env, reg, meta->btf, btf_id, nr_slots);
...
err = mark_iter_read(env, reg, spi, nr_slots);
...
meta->iter.spi = spi;
meta->iter.frameno = reg->frameno;
meta->ref_obj_id = iter_ref_obj_id(env, reg, spi);

if (is_iter_destroy_kfunc(meta)) {
err = unmark_stack_slots_iter(env, reg, nr_slots);
...

iter的协议,总结一下: - iter 必须放在栈上 - new 需要 uninitialized iter state - next/destroy 需要 initialized iter state - 某些 iter 使用时还可能要求 RCU CS - destroy 会清掉 iter 状态 - next 还会把 meta->iter 记下来,供后面 process_iter_next_call() 使用

可以防止:use-before-init,double-destroy, destroy 后继续使用,iterator 状态机走错,以及后续循环收敛分析失真等等

### 4.5.3 callback:参数必须真的是函数指针

1
2
3
4
5
6
7
case KF_ARG_PTR_TO_CALLBACK:
if (reg->type != PTR_TO_FUNC) {
verbose(env, "arg%d expected pointer to func\n", i);
return -EINVAL;
}
meta->subprogno = reg->subprogno;
break;

callback 参数不是普通整数,也不是普通指针,它必须是 verifier 认识的 PTR_TO_FUNC,并且 verifier 要把对应的 subprogno(子程序编号) 记下来

后续在check_kfunc_call() 里,push_callback_call() 就会继续验证这个 callback 子程序。

4.5.4 irq flag:save/restore 必须成对,状态必须在栈上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int process_irq_flag(struct bpf_verifier_env *env, int regno,
struct bpf_kfunc_call_arg_meta *meta)
{
...

if (irq_save) {
if (!is_irq_flag_reg_valid_uninit(env, reg)) {
verbose(env, "expected uninitialized irq flag as arg#%d\n", regno - 1);
return -EINVAL;
}

err = check_mem_access(env, env->insn_idx, regno, 0, BPF_DW, BPF_WRITE, -1, false, false);
...
err = mark_stack_slot_irq_flag(env, meta, reg, env->insn_idx, kfunc_class);
...
} else {
err = is_irq_flag_reg_valid_init(env, reg);
if (err) {
verbose(env, "expected an initialized irq flag as arg#%d\n", regno - 1);
return err;
}

err = mark_irq_flag_read(env, reg);
...
err = unmark_stack_slot_irq_flag(env, reg, kfunc_class);
...
}
return 0;
}

和 iter 很像,也是协议型检查:

  • save 时,flag 必须是未初始化的
  • restore 时,flag 必须已经初始化
  • 必须是栈上的对象
  • save 之后要打标记
  • restore 之后要去掉标记

所以它可以防止的是:restore 一个没保存过的 flag;重复 restore;乱用栈对象;irq 协议不配对等

4.5.5 res_spin_lock:不是普通参数,而是锁协议入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case KF_ARG_PTR_TO_RES_SPIN_LOCK:
{
int flags = PROCESS_RES_LOCK;

if (reg->type != PTR_TO_MAP_VALUE && reg->type != (PTR_TO_BTF_ID | MEM_ALLOC)) {
verbose(env, "arg#%d doesn't point to map value or allocated object\n", i);
return -EINVAL;
}

if (meta->func_id == special_kfunc_list[KF_bpf_res_spin_lock] ||
meta->func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave])
flags |= PROCESS_SPIN_LOCK;
if (meta->func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave] ||
meta->func_id == special_kfunc_list[KF_bpf_res_spin_unlock_irqrestore])
flags |= PROCESS_LOCK_IRQ;

ret = process_spin_lock(env, regno, flags);
...
}

res_spin_lock 这类参数不是“普通结构体指针”,它会直接驱动 verifier 的锁状态机。对应的检查会进一步进入 process_spin_lock()

所以这里背后的 concern 已经不只是参数类型,而是:锁协议、锁上下文、irq 与锁的组合约束

约束 ID (constraint_id) 来源函数 (source_function) 作用域 前置条件 (precondition) 后置条件 / 状态写入 (postcondition) 安全隐患 (safety_concern) 代表性 kfunc 自动提取
arg.scalar.required check_kfunc_args() arg BTF 参数是 scalar 对应寄存器必须是 SCALAR_VALUE 类型错配 大量标量参数 kfunc
arg.const.required check_kfunc_args() + is_kfunc_arg_constant() arg 参数名带 __k reg->var_off 必须为常量,写入 meta->arg_constant 非常量导致 verifier 无法证明安全 bpf_obj_new 这类 type id 参数
arg.ret_buf_size check_kfunc_args() arg 参数名是 rdonly_buf_size / rdwr_buf_size 写入 meta->r0_size、可能写 meta->r0_rdonly 返回缓冲区大小未知导致后续返回值无法建模 返回 PTR_TO_MEM 的 kfunc 部分可自动
arg.nullability check_kfunc_args() + is_kfunc_arg_nullable() arg 参数可能为 NULL 若未标 __nullable 则拒绝可空指针 NULL deref 各类可空对象参数
arg.trusted.or.rcu check_kfunc_args() arg 参数分类为 PTR_TO_BTF_ID / ALLOC_BTF_ID 必须是 trusted;若函数带 KF_RCU 则至少要是 RCU 指针 悬空对象 / 不可信来源 / 并发活性问题 bpf_task_acquire 部分可自动
arg.btf.match process_kf_arg_ptr_to_btf_id() arg 参数是 BTF 对象指针 类型必须严格匹配或结构兼容匹配 类型混淆 bpf_task_releasebpf_cgroup_acquire 部分可自动
arg.graph.root.locked __process_kf_arg_ptr_to_graph_root() arg 参数是 list/rbtree root/head offset 必须是常量,field 必须存在,保护锁必须已持有 容器损坏 / 并发问题 bpf_list_push_frontbpf_rbtree_add 部分可自动
arg.graph.node.compat __process_kf_arg_ptr_to_graph_node() arg 参数是 list/rbtree node node 类型、offset、所属 value type 必须与 root 匹配 错节点插入错误容器 bpf_rbtree_addbpf_rbtree_remove 部分可自动
arg.dynptr.protocol check_kfunc_args() + process_dynptr_func() arg 参数是 dynptr 检查 init/uninit、type、clone、discard 协议,并写入 meta->initialized_dynptr use-before-init / 状态错乱 bpf_dynptr_from_skbbpf_dynptr_clone 部分可自动
arg.iter.protocol process_iter_arg() arg 参数是 iter 必须在栈上,new 需 uninit,next/destroy 需 init,写入 meta->iter 协议误用 / 循环分析失真 bpf_iter_num_new / next / destroy 部分可自动
arg.callback.funcptr check_kfunc_args() arg 参数分类为 callback 必须是 PTR_TO_FUNC,写入 meta->subprogno 错误 callback / 子程序未验证 bpf_rbtree_addbpf_wq_set_callback
arg.irq_flag.protocol process_irq_flag() arg 参数名带 __irq_flag save 需 uninit,restore 需 init,更新栈槽 irq state save/restore 不配对 bpf_local_irq_save / restore 部分可自动

五、“返回值和生命周期”相关逻辑

这部分对应的是 UAF/leak/double-free、RCU/锁保护、iterator 收敛、packet pointer 失效 这些 safety concern。

第四部分解决的是: “调用之前,这个 kfunc 的输入够不够合法?”

第五部分解决的是:“调用之后,verifier 应该把返回值建模成什么,以及这次调用会怎样改变对象生命周期和程序状态。”

所以这一部分的重点,不再是“参数是不是某种类型”,而是:

  • R0 应该被当成 scalar、PTR_TO_MEM,还是 PTR_TO_BTF_ID
  • 这个返回值会不会是 NULL
  • 这次调用会不会新建一个引用
  • 这次调用会不会消耗一个引用
  • 返回的是 owning ref 还是 non-owning ref
  • 某些调用之后,旧 iter / 旧 packet pointer 是否应该失效

5.1 返回值建模总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
if (meta.release_regno) {
...
err = release_reference(env, reg->ref_obj_id);
...
}

if (is_bpf_list_push_kfunc(meta.func_id) || is_bpf_rbtree_add_kfunc(meta.func_id)) {
release_ref_obj_id = regs[BPF_REG_2].ref_obj_id;
err = ref_convert_owning_non_owning(env, release_ref_obj_id);
...
err = release_reference(env, release_ref_obj_id);
...
}

/* Check return type */
t = btf_type_skip_modifiers(desc_btf, meta.func_proto->type, NULL);

if (btf_type_is_scalar(t)) {
...
} else if (btf_type_is_ptr(t)) {
ptr_type = btf_type_skip_modifiers(desc_btf, t->type, &ptr_type_id);
err = check_special_kfunc(...);
...
if (is_kfunc_ret_null(&meta)) {
regs[BPF_REG_0].type |= PTR_MAYBE_NULL;
regs[BPF_REG_0].id = ++env->id_gen;
}
if (is_kfunc_acquire(&meta)) {
int id = acquire_reference(env, insn_idx);
...
regs[BPF_REG_0].ref_obj_id = id;
} else if (is_rbtree_node_type(ptr_type) || is_list_node_type(ptr_type)) {
ref_set_non_owning(env, &regs[BPF_REG_0]);
}
} else if (btf_type_is_void(t)) {
...
}

if (is_kfunc_pkt_changing(&meta))
clear_all_pkt_pointers(env);

if (is_iter_next_kfunc(&meta)) {
err = process_iter_next_call(env, insn_idx, &meta);
...
}

所谓“返回值和生命周期逻辑”,其实是 3 层东西叠在一起:

  1. 先处理这次调用带来的生命周期副作用 比如 release,或者 list/rbtree 插入导致 ownership transfer。
  2. 再建模 R0 也就是把返回值翻译成 verifier 能理解的寄存器状态。
  3. 最后做后处理 比如 iter next 分叉、packet pointer 失效。

5.2 check_special_kfunc()

一些特殊的kfunc返回值,通用的scalar/ptr/void不够用,必须先优先处理一下

### 5.2.1 bpf_obj_new / bpf_percpu_obj_new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if (is_bpf_obj_new_kfunc(meta->func_id) || is_bpf_percpu_obj_new_kfunc(meta->func_id)) {
...
ret_btf = env->prog->aux->btf;
ret_btf_id = meta->arg_constant.value;

if (!ret_btf) {
verbose(env, "bpf_obj_new/bpf_percpu_obj_new requires prog BTF\n");
return -EINVAL;
}

ret_t = btf_type_by_id(ret_btf, ret_btf_id);
if (!ret_t || !__btf_type_is_struct(ret_t)) {
verbose(env, "bpf_obj_new/bpf_percpu_obj_new type ID argument must be of a struct\n");
return -EINVAL;
}

...
mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].type = PTR_TO_BTF_ID | MEM_ALLOC;
regs[BPF_REG_0].btf = ret_btf;
regs[BPF_REG_0].btf_id = ret_btf_id;
if (is_bpf_percpu_obj_new_kfunc(meta->func_id))
regs[BPF_REG_0].type |= MEM_PERCPU;

insn_aux->obj_new_size = ret_t->size;
insn_aux->kptr_struct_meta = struct_meta;
}

先验证程序是否真的带BTF,对应的类型是否是struct,另外验证 type id 参数是否是合法常量

如果成功过了验证之后,便把R0建模成PTR_TO_BTF_ID | MEM_ALLOC,然后把对象大小、struct meta 填进 insn_aux里面

这里面就是在防御 allocation object的可建模性

5.2.2 bpf_refcount_acquire

1
2
3
4
5
6
7
8
9
} else if (is_bpf_refcount_acquire_kfunc(meta->func_id)) {
mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].type = PTR_TO_BTF_ID | MEM_ALLOC;
regs[BPF_REG_0].btf = meta->arg_btf;
regs[BPF_REG_0].btf_id = meta->arg_btf_id;

insn_aux->kptr_struct_meta =
btf_find_struct_meta(meta->arg_btf, meta->arg_btf_id);
}

可以看到,返回对象的类型 btf以及btf_id,是沿用的arg也就是参数阶段搜集到的meta->arg_btf / meta->arg_btf_id

这说明,返回值的建模有时候依靠前面参数的分析结果

5.2.3 graph node 返回值

1
2
3
4
5
6
7
8
9
} else if (is_list_node_type(ptr_type)) {
struct btf_field *field = meta->arg_list_head.field;

mark_reg_graph_node(regs, BPF_REG_0, &field->graph_root);
} else if (is_rbtree_node_type(ptr_type)) {
struct btf_field *field = meta->arg_rbtree_root.field;

mark_reg_graph_node(regs, BPF_REG_0, &field->graph_root);
}

如果kfunc返回list或者rbtee,返回值不仅仅是一个struct,会进一步的标记为这是某个 graph root协议里面的node

这也说明了,返回值不仅仅是类型,还有容器协议语义

5.2.4 bpf_rdonly_cast / dynptr slice

看两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
} else if (meta->func_id == special_kfunc_list[KF_bpf_rdonly_cast]) {
...
if (btf_type_is_struct(ret_t)) {
regs[BPF_REG_0].type = PTR_TO_BTF_ID | PTR_UNTRUSTED;
...
} else if (btf_type_is_void(ret_t)) {
regs[BPF_REG_0].type = PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED;
regs[BPF_REG_0].mem_size = 0;
}
}

} else if (meta->func_id == special_kfunc_list[KF_bpf_dynptr_slice] ||
meta->func_id == special_kfunc_list[KF_bpf_dynptr_slice_rdwr]) {
...
regs[BPF_REG_0].mem_size = meta->arg_constant.value;
regs[BPF_REG_0].type = PTR_TO_MEM | type_flag;
...
regs[BPF_REG_0].dynptr_id = meta->initialized_dynptr.id;
}

对于一些特殊的kfunc,他的返回语义非常依赖于arg带来的额外协议信息

同时,还会给R0建模成只读/UNTRUSTED/dynptr_id等标签

综上,check_special是通用返回值建模之前的一个补丁,主要是为了补全所有的返回情况

5.3 /* Check return type */

见第三部分

简单来讲,有四类返回值

5.3.1 返回标量

1
2
3
4
5
6
7
if (btf_type_is_scalar(t)) {
mark_reg_unknown(env, regs, BPF_REG_0);
if (meta.btf == btf_vmlinux && (meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock] ||
meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave]))
__mark_reg_const_zero(env, &regs[BPF_REG_0]);
mark_btf_func_reg_size(env, BPF_REG_0, t->size);
}

对于scalar返回值,R0 = unknown scalar,然后再记录寄存器宽度

个别的kfunc成功路径会被压缩成常量0

对于标量来说,只需要数值建模

5.3.2 返回 void *,按照标量处理

1
2
3
4
} else if (btf_type_is_void(ptr_type)) {
/* kfunc returning 'void *' is equivalent to returning scalar */
mark_reg_unknown(env, regs, BPF_REG_0);
}

出于安全保护,verifier不会把 void *当成可解引用的指针,所以保守处理成一个标量

5.3.3 返回非struct * ,建模成 PTR_TO_MEM (指向内存)

建模成可访问内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
} else if (!__btf_type_is_struct(ptr_type)) {
if (!meta.r0_size) {
__u32 sz;

if (!IS_ERR(btf_resolve_size(desc_btf, ptr_type, &sz))) {
meta.r0_size = sz;
meta.r0_rdonly = true;
}
}
if (!meta.r0_size) {
...
return -EINVAL;
}

mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].type = PTR_TO_MEM;
regs[BPF_REG_0].mem_size = meta.r0_size;

if (meta.r0_rdonly)
regs[BPF_REG_0].type |= MEM_RDONLY;

if (meta.ref_obj_id)
regs[BPF_REG_0].ref_obj_id = meta.ref_obj_id;

if (is_kfunc_rcu_protected(&meta))
regs[BPF_REG_0].type |= MEM_RCU;
}

对于 int * 或者 char * 等非struct 的指针,verifier更愿意把他们建模成一块有效内存,但是前提是得知道这块内存有多大

如果能从参数阶段拿到meta.r0_size 那就用,不行的话就从btf进行推导,如果还是不行的话就拒绝

主要是为了防止:无边界的内存访问、对象release缺失之后还能访问依附内存、RCU保护条件缺失等等

5.3.4 返回 struct *:建模成 PTR_TO_BTF_ID (指向对应的BTF)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
} else {
enum bpf_reg_type type = PTR_TO_BTF_ID;

if (meta.func_id == special_kfunc_list[KF_bpf_get_kmem_cache])
type |= PTR_UNTRUSTED;
else if (is_kfunc_rcu_protected(&meta) ||
(is_iter_next_kfunc(&meta) &&
(get_iter_from_state(env->cur_state, &meta)->type & MEM_RCU))) {
type |= MEM_RCU;
} else {
type |= PTR_TRUSTED;
}

mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].btf = desc_btf;
regs[BPF_REG_0].type = type;
regs[BPF_REG_0].btf_id = ptr_type_id;
}

标准的kfunc返回对象的建模方式,因为这样verifier明确的知道他是哪一个btf,n哪种结构体,后续访问以及生命周期跟踪怎么做,另外type还有一成附加语义

5.3.5 nullable / acquire / borrowed ref 的后处理

1
2
3
4
5
6
7
8
9
10
11
12
13
if (is_kfunc_ret_null(&meta)) {
regs[BPF_REG_0].type |= PTR_MAYBE_NULL;
regs[BPF_REG_0].id = ++env->id_gen;
}

if (is_kfunc_acquire(&meta)) {
int id = acquire_reference(env, insn_idx);
...
regs[BPF_REG_0].ref_obj_id = id;
} else if (is_rbtree_node_type(ptr_type) || is_list_node_type(ptr_type)) {
ref_set_non_owning(env, &regs[BPF_REG_0]);
}

返回值建模完之后并不是设置完type就结束,还要继续确定,会不会是NULL,会不会是acquire,会不会是 borrowed node指针等等

5.4 acquire_reference() / release_reference()

生命周期建模

5.4.1 acquire_reference()

定义:

1
2
3
4
5
6
7
8
9
10
11
static int acquire_reference(struct bpf_verifier_env *env, int insn_idx)
{
struct bpf_reference_state *s;

s = acquire_reference_state(env, insn_idx);
if (!s)
return -ENOMEM;
s->type = REF_TYPE_PTR;
s->id = ++env->id_gen;
return s->id;
}

调用点:

1
2
3
4
5
6
7
8
9
if (is_kfunc_acquire(&meta)) {
int id = acquire_reference(env, insn_idx);

if (id < 0)
return id;
if (is_kfunc_ret_null(&meta))
regs[BPF_REG_0].id = id;
regs[BPF_REG_0].ref_obj_id = id;
}

这段逻辑的本质是:我先给这次新拿到的对象分配一个新的引用id,然后把它记录到当前verifier state 的 refs[]里面,最后把这个ID绑定到 R0.ref_obj_id里面

那么从验证器的角度来看的话,R0现在就不是一个普通的对象指针了,而是:我现在手里面新持有一个 owning ref,可以用来追踪

5.4.2 release_reference()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int release_reference(struct bpf_verifier_env *env, int ref_obj_id)
{
struct bpf_verifier_state *vstate = env->cur_state;
struct bpf_func_state *state;
struct bpf_reg_state *reg;
int err;

err = release_reference_nomark(vstate, ref_obj_id);
if (err)
return err;

bpf_for_each_reg_in_vstate(vstate, state, reg, ({
if (reg->ref_obj_id == ref_obj_id)
mark_reg_invalid(env, reg);
}));

return 0;
}

这个函数做了两件事:

第一:他把当前状态的acquired_refs里面删掉这个 ref_id

第二:把所有持有这个ref_id的寄存器副本全部作废

防止在同一个对象的多个副本中,有一个被release了,另外的还在偷偷用。这个规定就是让其在整个状态里面传播失效

5.4.3 在 check_kfunc_call() 里,release 是怎么触发的

1
2
3
4
5
6
7
8
9
10
11
12
if (meta.release_regno) {
struct bpf_reg_state *reg = &regs[meta.release_regno];

if (meta.initialized_dynptr.ref_obj_id) {
err = unmark_stack_slots_dynptr(env, reg);
} else {
err = release_reference(env, reg->ref_obj_id);
...
}
if (err)
return err;
}

在第四部分里面,我们以及确定了,哪一个参数是 release对象,然后在这一部分就执行生命周期状态的转移

另外,对于dyptr,这个是特殊的case,普通的对象就走release_reference

这样调用之后,就会让某个原本alive的ref在verifier眼里失效

## 5.5 non-owning / ownership transfer

对于acquire以及release,这是一个二元的关系,但是这一部分要讲的,不只是二元关系,而是对于某些对象来说,他们的属性不是拥有,而是借用

5.5.1 ref_set_non_owning()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int ref_set_non_owning(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
{
struct btf_record *rec = reg_btf_record(reg);

if (!env->cur_state->active_locks) {
verifier_bug(env, "%s w/o active lock", __func__);
return -EFAULT;
}

if (type_flag(reg->type) & NON_OWN_REF) {
verifier_bug(env, "NON_OWN_REF already set");
return -EFAULT;
}

reg->type |= NON_OWN_REF;
if (rec->refcount_off >= 0)
reg->type |= MEM_RCU;

return 0;
}

这个函数就是set non owning 的逻辑:

建立在active lock的前提下,把寄存器类型加上NON_OWN_REF标签,某些情况下还顺带加上MEM_RCU标签

这个borrowed ref的语义就是,你可以读取里面的内容,但是他不是你能够拥有的,他的有效性依赖于锁的保护

5.5.2 返回 borrowed node 指针

1
2
3
} else if (is_rbtree_node_type(ptr_type) || is_list_node_type(ptr_type)) {
ref_set_non_owning(env, &regs[BPF_REG_0]);
}

这个的处理就是,如果kfunc返回的是list/rbtree节点,并且不是acquire的返回值,那么就按照默认的non owning ref来处理

5.5.3 容器插入时的 ownership transfer

list/rbtree节点的插入:

1
2
3
4
5
6
7
8
if (is_bpf_list_push_kfunc(meta.func_id) || is_bpf_rbtree_add_kfunc(meta.func_id)) {
release_ref_obj_id = regs[BPF_REG_2].ref_obj_id;
...
err = ref_convert_owning_non_owning(env, release_ref_obj_id);
...
err = release_reference(env, release_ref_obj_id);
...
}

owning 与 non owning的转换函数:

1
2
3
4
5
6
7
8
9
10
11
static int ref_convert_owning_non_owning(struct bpf_verifier_env *env, u32 ref_obj_id)
{
...
bpf_for_each_reg_in_vstate(env->cur_state, unused, reg, ({
if (reg->ref_obj_id == ref_obj_id) {
reg->ref_obj_id = 0;
ref_set_non_owning(env, reg);
}
}));
return 0;
}

可以看到:

节点在插入之前,是BPF局部手里的owning ref 节点插入之后,通过转换函数,这个对象归 list/rbtree 管 另外,BPF手里面的owning 关系要被 release掉,同时一些现有视图可能降为non-owning

5.5.4 non-owning ref 什么时候失效

看一下锁释放路径里面的调用:

1
2
3
4
5
6
if (release_lock_state(cur, type, reg->id, ptr)) {
...
return -EINVAL;
}

invalidate_non_owning_refs(env);

invalidate_non_owning_refs函数本身:

1
2
3
4
5
6
7
8
9
10
static void invalidate_non_owning_refs(struct bpf_verifier_env *env)
{
struct bpf_func_state *unused;
struct bpf_reg_state *reg;

bpf_for_each_reg_in_vstate(env->cur_state, unused, reg, ({
if (type_is_non_owning_ref(reg->type))
mark_reg_invalid(env, reg);
}));
}

一旦锁被释放,borrowed ref ,这些non owning 的引用必须全部作废。

也就是说,锁没了,借来的node指针就不能用了

5.6 iterator / packet pointer 失效

下面是两类调用后的状态失效:

5.6.1 process_iter_next_call()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static int process_iter_next_call(struct bpf_verifier_env *env, int insn_idx,
struct bpf_kfunc_call_arg_meta *meta)
{
...
cur_iter = get_iter_from_state(cur_st, meta);

if (cur_iter->iter.state == BPF_ITER_STATE_ACTIVE) {
prev_st = find_prev_entry(env, cur_st->parent, insn_idx);

/* branch out active iter state */
queued_st = push_stack(env, insn_idx + 1, insn_idx, false);
if (IS_ERR(queued_st))
return PTR_ERR(queued_st);

queued_iter = get_iter_from_state(queued_st, meta);
queued_iter->iter.state = BPF_ITER_STATE_ACTIVE;
queued_iter->iter.depth++;
if (prev_st)
widen_imprecise_scalars(env, prev_st, queued_st);

queued_fr = queued_st->frame[queued_st->curframe];
mark_ptr_not_null_reg(&queued_fr->regs[BPF_REG_0]);
}

/* mark current iter state as drained and assume returned NULL */
cur_iter->iter.state = BPF_ITER_STATE_DRAINED;
__mark_reg_const_zero(env, &cur_fr->regs[BPF_REG_0]);

return 0;
}

简单来说,就是把iter_next()变成“非空继续分支”+“最终为空收敛分支”的状态机

如果当前 iterator 是 active ,那么verifier会手动分叉出一个”继续迭代”的路径,这个路径的R0会被标记为 non- NULL

如果当前 iterator 不是 active , 那么当前路径被标记为 drained(流尽),当前路径上的R0也被直接标记为0

process_iter_next_call()这个函数,不仅仅是在做一个普通的状态检查,而是在做iterator协议的状态转移以及NULL / 非 NULL 的分支分化

5.6.2 clear_all_pkt_pointers()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  /* Packet data might have moved, any old PTR_TO_PACKET[_META,_END]
* are now invalid, so turn them into unknown SCALAR_VALUE.
*
* This also applies to dynptr slices belonging to skb and xdp dynptrs,
* since these slices point to packet data.
*/
static void clear_all_pkt_pointers(struct bpf_verifier_env *env)
{
struct bpf_func_state *state;
struct bpf_reg_state *reg;

bpf_for_each_reg_in_vstate(env->cur_state, state, reg, ({
if (reg_is_pkt_pointer_any(reg) || reg_is_dynptr_slice_pkt(reg))
mark_reg_invalid(env, reg);
}));
}

// 调用点:
if (is_kfunc_pkt_changing(&meta))
clear_all_pkt_pointers(env);

这里很简单,就是说如果有kfunc的操作会改变packet的数据布局,那么 verifier 采取的策略就是:全部作废。之前算出来的所有 packet pointer 都不可信了,连通 packet data 的dynptr slice 也一样作废

这个主要是防止一些问题,比如:改包后继续沿用旧的 data/data_end; stale packet pointer导致的越界访问;旧 packet slice 继续访问错误内存等

下面是codex总结的四、第五部分合并成一张“源码证据 -> 约束抽象 -> spec 模板 hint”的总表:

要把东西分为三层来看: 一是源码证据;二是约束抽象(自己起的规范化名字);三是spec模板hint

类别 源码证据 约束抽象 Spec 模板 Hint Concern (安全隐患) 代表 Kfunc
参数 btf_type_is_scalar(t)reg->type != SCALAR_VALUE 则拒绝 arg.scalar.required requires arg[i].kind == scalar 类型错配 大量标量参数
参数 参数名后缀 __k,且 !tnum_is_const(reg->var_off) 则拒绝 arg.const.required requires arg[i].is_const Verifier 无法稳定建模 bpf_obj_new
参数 rdonly_buf_size / rdwr_buf_size 写入 meta->r0_size arg.retbuf.size ensures ret.mem_size = arg[i] 返回缓冲区大小未知 buffer-return kfunc
参数 指针可能为 NULL 但未标 __nullable 则拒绝 arg.nullability.gated requires arg[i] == NULL -> param.nullable NULL deref (空指针解引用) 各类可空对象参数
参数 PTR_TO_BTF_ID/ALLOC_BTF_ID 参数要求 trusted;带 KF_RCU 时至少要求 RCU ptr arg.trusted.or.rcu requires arg[i].trusted or arg[i].rcu_live 悬空对象 / 来源不可信 bpf_task_acquire
参数 process_kf_arg_ptr_to_btf_id() 做 strict/compatible match arg.btf.match requires type_compatible(arg[i], expected_btf) 类型混淆 bpf_task_release
参数 graph root 要求 constant offset + field 存在 + lock held arg.graph.root.valid_locked requires arg[i].field == graph_root && arg[i].offset_const && lock_held(arg[i]) 容器损坏 / 竞态 bpf_list_push_front
参数 graph node 要求 value type/offset 与 root 匹配 arg.graph.node.compat requires node_matches_root(arg[node], arg[root]) 错节点插入错误容器 bpf_rbtree_add
参数 dynptr 参数区分 MEM_UNINIT、只读、type、clone、discard arg.dynptr.protocol requires dynptr_state_ok(op, arg[i]) use-before-init / 状态错乱 bpf_dynptr_clone
参数 iter 参数必须是栈上对象;new 要 uninit,next/destroy 要 init arg.iter.protocol requires iter_state_ok(op, arg[i]) 协议误用 / 循环分析失真 bpf_iter_num_new/next/destroy
参数 callback 参数必须是 PTR_TO_FUNC,写入 meta->subprogno arg.callback.funcptr requires arg[i].kind == funcptr 错误回调 / 子程序未验证 bpf_wq_set_callback
参数 irq flag save 要 uninit,restore 要 init arg.irq_flag.protocol requires irq_flag_state_ok(op, arg[i]) save/restore 不配对 bpf_local_irq_save/restore
参数 KF_ARG_PTR_TO_MEM_SIZE 先校验 (ptr,len) 配对可访问性 arg.mem_size.pair_safe requires memory_range_valid(arg[i], arg[i+1]) 越界访问 buffer+len 型 kfunc
返回 is_kfunc_acquire(&meta) && !btf_type_is_struct_ptr(...) 则拒绝,少数特例除外 ret.acquire.requires_struct_ptr requires acquire_kfunc -> ret.kind in {struct_ptr, special_case} 生命周期无法建模 bpf_task_acquire
返回 check_special_kfunc() 命中特殊分支,直接设置 R0 ret.special.override ensures ret = special_semantics(kfunc, args) 通用模型不够表达 bpf_obj_new
返回 返回类型是 scalar,则 R0 = unknown scalar ret.scalar.unknown ensures ret.kind == scalar_unknown 普通数值返回 大量标量返回
返回 返回 void * 时按未知标量处理 ret.ptr.void.as_scalar ensures ret.kind == scalar_unknown 无法安全解引用 void * 返回 kfunc
返回 返回非 struct * 且大小可知,则 R0 = PTR_TO_MEM ret.ptr.nonstruct.mem ensures ret.kind == mem_ptr && ret.size_known 无边界访问 buffer-return kfunc
返回 struct * 返回值若 meta.r0_rdonly,则加 MEM_RDONLY ret.mem.readonly ensures ret.readonly 非法写只读缓冲区 bpf_dynptr_slice
返回 返回 struct *,则 R0 = PTR_TO_BTF_ID,并附加 trusted/untrusted/rcu ret.ptr.struct.btfid ensures ret.kind == btf_ptr && ret.btf_id = T 类型与并发语义错误 bpf_cgroup_acquire
返回 KF_RET_NULL 时给 R0 加 PTR_MAYBE_NULL 和判空 id ret.ptr.nullable ensures ret.maybe_null NULL deref bpf_obj_new
生命周期 acquire_reference() 分配新 ref_obj_id 并写回 R0 state.acquire.new_ref ensures creates_ref(ret) leak / UAF / double-free bpf_obj_new
生命周期 release_reference()refs[] 删除 ref,并把所有副本 mark_reg_invalid state.release.consume_ref ensures invalidates(all_aliases(ref)) release 后继续使用 bpf_obj_drop
生命周期 返回 list/rbtree node 且非 acquire 时,ref_set_non_owning() ret.node.borrowed ensures ret.borrowed == true 把 borrowed ref 当 owning ref bpf_list_front
生命周期 list/rbtree insert 前先 ref_convert_owning_non_owning() state.ownership.transfer.to_container ensures transfer_ownership(arg[node], container) ownership transfer 错误 bpf_rbtree_add
生命周期 lock unlock 路径调用 invalidate_non_owning_refs() state.borrowed.invalidate.on_unlock invalidates all borrowed_refs after unlock use-after-unlock list/rbtree 遍历路径
生命周期 meta.ref_obj_id 绑定到 PTR_TO_MEM 返回值 ret.mem.depends_on_owner ensures ret.alias_of(ref_obj) owner release 后继续访问附着内存 某些内存返回型 kfunc
状态失效 process_iter_next_call() 分叉出 non-NULL 分支,当前分支变 drained 且 R0=0 state.iter.next.split ensures branch1(ret != NULL, iter=ACTIVE), branch2(ret == NULL, iter=DRAINED) 循环不收敛 / iterator 协议失真 bpf_iter_num_next
状态失效 clear_all_pkt_pointers() 使所有 packet ptr / pkt dynptr slice 失效 state.pkt.invalidate.on_change invalidates all pkt_related_ptrs stale packet pointer / OOB bpf_xdp_pull_data
返回辅助 reg_may_point_to_spin_lock(&R0) && !id 时补 id ret.lock.identity ensures ret.lock_id = fresh_id 不同锁实例混淆 bpf_res_spin_lock*
返回辅助 void 返回但 bpf_obj_drop 这类会写 insn_aux->kptr_struct_meta ret.void.side_metadata ensures aux.kptr_meta = arg_type_meta drop 后续检查缺少元信息 bpf_obj_drop