KfuncVerifier
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根本不知道这个指针到底是什么
kfunc比helper更灵活,很大程度上就是因为他依赖BTF
verifier可以知道参数是不是struct *- 可以知道返回值是什么类型
- 可以知道某个字段在结构体里的
offset - 可以把返回值建模成
PTR_TO_BTF_ID - 可以根据参数名后缀识别特殊语义,比如
__sz、__nullable
5.什么是safety concern
目前研究kfunc,我更想知道的是,为什么verifier要限制它,背后的原因是什么
我理解的concern,大概有:
memory safety
内存安全,比如说 越界访问、空指针访问、读未初始化内存、把对象当作另一种对象等等
lifetime / ownership
也就是生命周期的问题,比如说;引用之后未释放、连续释放两次、release之后还继续访问、容器插入之后的所有权
concurrency / RCU / lock
并发与锁的一些相关问题,比如说;没有没有持锁就操作 list/rbtree、不在 RCU 保护区里却去访问受 RCU 保护的对象、unlock 之后还继续用 borrowed pointer
context safety
上下文问题,比如说:不能sleep的地方调用sleepable kfunc,不合适的程序类型里调用某类 kfunc、缺少 capability 却调用 destructive kfunc
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 |
|
如果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 | if (!insn->off && |
这一段是在处理“普通通路不够表达”的特殊 kfunc。
为什么 bpf_res_spin_lock* 要单独分支?
因为这类 kfunc 有一个很特别的点:它可能失败。所以 verifier 不能只假设“调用成功”。它必须显式模拟出一条失败路径。
于是,使用push_stack分支出了一个新路径,模拟锁分配失败,清空寄存器,将返回值 R0 标记为一个未知的错误码(-MAX_ERRNO 到 -1)
第三段:程序级上下文约束
1 | if (is_kfunc_destructive(&meta) && !capable(CAP_SYS_BOOT)) { |
KF_DESTRUCTIVE:破坏性 kfunc 需要更高权限
KF_SLEEPABLE:先检查“这个程序类型允不允许 sleep”
insn_aux->non_sleepable = true:记住当前小上下文
这句要特别注意。它和上面的 in_sleepable(env) 不是一回事。
可以这样理解:
- in_sleepable(env):整个程序大环境允不允许 sleep
- in_sleepable_context(env):当前这一小段路径是不是仍然处在可 sleep 状态(有种套娃的感觉)
第四段:参数检查与回调检查
1 | err = check_kfunc_args(env, &meta, insn_idx); |
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
回调函数安全性验证
比如说bpf_rbtree_add,类似的还有 bpf_wq_set_callback、bpf_task_work_schedule_*
1
2
3
4
5if (is_bpf_rbtree_add_kfunc(meta.func_id)) {
err = push_callback_call(env, insn, insn_idx, meta.subprogno,
set_rbtree_add_callback_state);
...
}bpf_session_cookie 先补一下返回值元信息
在真正进入后面的返回值建模之前
先给这个特殊 kfunc 补上返回值大小相关的元信息(R0 应该按 8 字节来理解)
1 | if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie]) { |
第五段:RCU / preempt 状态机更新
verifier 不只是检查函数调用,它还会修改当前抽象执行状态
1 | rcu_lock = is_kfunc_bpf_rcu_read_lock(&meta); |
1. bpf_rcu_read_lock():进入 RCU 保护区
1 | if (rcu_lock) { |
当前状态里,RCU 锁计数加一
后面很多 KF_RCU_PROTECTED 或 MEM_RCU 指针,都依赖这个状态。
2. bpf_rcu_read_unlock():退出保护区,还要“降级旧指针”
1 | else if (rcu_unlock) { |
这里有两个点
第一:unlock 必须配对
没加过锁就解锁,直接报错
1 | if (env->cur_state->active_rcu_locks == 0) { |
第二:unlock 之后,原来受 RCU 保护的指针不再可信
它不是直接把这些寄存器清空,而是把它们降级成:
- 不再带 MEM_RCU
- 不再保留原先那种“受保护可安全使用”的语义
- 转成 PTR_UNTRUSTED
1 | if (reg->type & MEM_RCU) { |
3. preempt_disable/enable 也是类似的状态机
1 | else if (preempt_disable) { |
与RCU的逻辑基本一致:
- disable:进入一个新状态
- enable:退出这个状态
- 不允许“没 disable 就 enable”
第六段:当前小上下文的额外限制
1 | if (sleepable && !in_sleepable_context(env)) { |
这个也是在看当前状态对不对
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 | if (meta.release_regno) { |
这一段很明显的说明了,有些 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 | if (meta.func_id == special_kfunc_list[KF_bpf_throw]) { |
这个代码反映了两个规定:
1.底层 JIT 得支持异常
就算 verifier 理论上允许,底层 JIT 如果不支持异常也不能放行
2.如果没有自定义异常回调,R1 会变成整个程序返回值
没有自定义异常回调时,传给 bpf_throw 的 R1最终会作为整个 BPF 程序的返回值
但是这里需要检查R1是否符合返回码的约束
第九段:调用之后,先清理 Caller-Saved 寄存器
1 | for (i = 0; i < CALLER_SAVED_REGS; i++) { |
kfunc 调用之后,R1-R5 都不能再信,全部清除。
第十段:返回值检查的总入口
1 |
|
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 | if (btf_type_is_scalar(t)) { |
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 | } else if (btf_type_is_ptr(t)) { |
最复杂的一个
check_special_kfunc() 的返回语义是:
- < 0:出错
- > 0:已经处理完返回值建模了
- == 0:不是 special kfunc,继续走通用逻辑
1 void *:按未知标量处理
1 | } else if (btf_type_is_void(ptr_type)) { |
因为verifier不知道void*会指向什么类型,也不知道大小,所以不敢贸然的把它当作普通对象指针来看待
2 非 struct * 指针:尽量建模成 PTR_TO_MEM
1 | } else if (!__btf_type_is_struct(ptr_type)) { |
如果返回的是 int 、char 、或者别的非 struct 指针,verifier还是尽量想建模成一个指向有效内存的指针,但是前提是得知道这块内存有多大
有两种方式拿取大小: #### 第一种:之前参数检查阶段已经写进了 meta.r0_size
某些 kfunc 会通过参数名约定,把返回缓冲区大小传进去。
#### 第二种:直接从 BTF 推导 pointee 大小
1 | if (!IS_ERR(btf_resolve_size(desc_btf, ptr_type, &sz))) { |
推出来的默认被认为只读内存
如果两种都不行,那么verifier就拒绝
如果最后成功,R0就被建模成 PTR_TO_MEM,大小是 mem_size
以及一些只读、被引用等,再加上一些标志
3 struct *:建模成 PTR_TO_BTF_ID
1 | } else { |
最标准的kfunc返回对象的建模方式
struct*可以被verifier清楚的知道: - 这是什么类型的对象 - 它属于哪个 BTF - 后续字段访问是否安全
并把R0建模成 PTR_TO_BTF_ID,这里还会细分:
- PTR_UNTRUSTED:bpf_get_kmem_cache 特判成不可信
- MEM_RCU:RCU 保护下的返回值
- PTR_TRUSTED:其他普通情况,默认视为 trusted
第十三段:指针返回值的后处理
1 | if (is_kfunc_ret_null(&meta)) { |
1. KF_RET_NULL:给 R0 打上 PTR_MAYBE_NULL
1 | if (is_kfunc_ret_null(&meta)) { |
如果返回指针不是一定非空,后续调用得先判空
2. 指针返回值的寄存器宽度统一按指针大小处理
1 | mark_btf_func_reg_size(env, BPF_REG_0, sizeof(void *)); |
指针返回值,R0 的宽度就按指针大小建模
3.KF_ACQUIRE:真正生成新的引用 ID
1 | if (is_kfunc_acquire(&meta)) { |
这个的意思是,这次调用成功后,R0 现在持有一个新的 owning reference
如果是 nullable acquire,会把“判空 ID”和“引用 ID”对齐
4. list/rbtree 节点返回值:不是 owning,是 non-owning
这个返回值虽然是对象指针,但它不是一个“我拥有的引用”,它只是一个“借来的节点指针”
5. 如果可能指向 spin_lock,就补一个唯一 ID
主要是给锁加一个id,区分不同的锁实例
第十四段:void 返回值
1 | } else if (btf_type_is_void(t)) { |
如果返回类型是 void,一般不需要像指针 / 标量那样去建模 R0
但是bpf_obj_drop / bpf_percpu_obj_drop 这种 release 类 kfunc,需要记录它drop的到底是什么对象结构
第十五段:收尾后处理
1 | if (is_kfunc_pkt_changing(&meta)) |
1. 改包 kfunc 之后,让旧 packet pointer 全部失效
1 | if (is_kfunc_pkt_changing(&meta)) |
如果这个 kfunc 会改变 packet 数据布局,那么之前拿到的包指针就不能继续信了
最典型的就是 bpf_xdp_pull_data 这种。
2. 再次按 BTF 原型修正参数寄存器的宽度信息
1 | nargs = btf_type_vlen(meta.func_proto); |
R1-R5 虽然刚刚已经被当作 caller-saved 清掉了,但 verifier 仍然希望保留“这次调用的参数宽度语义”,这样后续一些子寄存器 / 零扩展 / 类型宽度相关逻辑才能更准确。更像是一种记账
3. 如果这是 iterator 的 next(),还要额外验证“循环最终会不会收敛”
1 | if (is_iter_next_kfunc(&meta)) { |
主要是为了验证, 会不会形成 verifier 证明不了安全性的死循环。这里调用 process_iter_next_call(),本质上是在更新 iterator 状态机。
4. bpf_session_cookie:在 prog 上打一个特征标记
1 | if (meta.func_id == special_kfunc_list[KF_bpf_session_cookie]) |
verifier 做的事情不只是“检查这一次调用”,有时候它还会把“这个程序用过某种特殊能力”记到 prog 上。譬如这里的 call_session_cookie = true
后面 trampoline / JIT 看到这个标记时,就知道:这个 BPF 程序需要 session cookie 相关的额外处理。这也体现了verifier也在维护整个程序的附加语义信息。
具体来说,它会依次做这些事:
- 识别这次调用是谁
- 检查当前程序和上下文是否允许调用
- 检查参数是否合法
- 必要时推进 callback / iter / dynptr / RCU / lock 等协议状态机
- 处理 release 和 ownership 转移
- 建模返回值 R0
- 做调用后的状态失效和程序级标记收尾
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 | /* These need to be macros, as the expressions are used in assembler input */ |
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 |
kfunc 调用成功之后,你“拿到了一个新的引用/所有权”
比如:
bpf_obj_new():新分配一个对象给你
bpf_task_acquire():给你一个新的 task 引用
bpf_list_pop_front():把节点从 list 里摘出来,交给你
也就是说,这个对象归我管了,但是这种语义,在verifier中,通常意味着返回值要被建模成一种“可跟踪生命周期的对象指针”,并且后面通常必须能找到对应的 release,verifier 要给这个返回值分配 ref_obj_id
所以,他的核心safe concern一般是: - 引用泄漏 - release 之后继续使用 - 忘记 release - 生命周期建模错误
2. KF_RELEASE
1 |
这个kfuc就会消耗的手里有的那个引用
比如: 1. bpf_obj_drop():把对象 drop 掉
bpf_task_release():把 task 引用释放掉
bpf_key_put():减少 key 引用
这个时候,verifier就会关心,1.传进来的参数里,到底哪个才是“要被释放的对象”;2.这个对象是不是真的之前 acquire 过;3.会不会出现 double free;4.会不会出现“释放了一个自己根本不拥有的对象”
所以,他的核心safe concern一般是:
- double free
- 释放未持有对象
- release 后继续使用
- 生命周期不平衡
acquire/release本质上是一套配对语义:
acquire:创建或得到一个拥有关系
release:销毁一个拥有关系
所以后面做逐个 kfunc 标注时,可以先问两个问题:
- 这个函数会不会产生 owning ref?
- 这个函数会不会消耗 owning ref?
这样,生命周期大体就出来了。
3.3 第二类标签:nullable
1.KF_RET_NULL
1 |
这个 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 |
一些典型例子:
1 | BTF_ID_FLAGS(func, bpf_lookup_user_key, KF_ACQUIRE | KF_RET_NULL | KF_SLEEPABLE) |
这个kfunc可能会睡眠,不是任何一个地方都能调用的
注意,能不能sleep不是参数问题,而是调用上下文问题
所以,KF_SLEEPABLE的安全关注点,通常是一些: - 在错误上下文里阻塞 - 破坏当前执行语义 - 导致一些 verifier 不允许出现的上下文切换 等等
更偏向于context safety
3.5 第四类标签:RCU
1. KF_RCU
1 |
例如:bpf_task_acquire,bpf_cgroup_acquire, bpf_crypto_encrypt
带有这个语义标签的kfunc对传入的指针来源很敏感,一般要求:trusted pointer或者 RCU 语义下可接受的 pointer
所以这类典型的 safety concern 是:
- 访问已经悬空的对象
- 在不可信对象上做操作
- 并发下对象已经死了,但你还拿它继续用
2. KF_RCU_PROTECTED
1 |
它和 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 |
这三个标签的意思就是,这个 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 |
典型例子就是BTF_ID_FLAGS(func, crash_kexec, KF_DESTRUCTIVE)
意思是这个kfunc不是普通辅助函数,它会做”破坏性动作”
由于影响较大,所以第一个检查的是资格,一些经典约束比如 capability 检查、程序权限边界
对应的saafety concern 是: - 非授权破坏性操作 - 直接影响系统稳定性的动作被滥用
3.8 第七类标签:implicit args
1 |
这个 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 |
一些例子:
1 | BTF_ID_FLAGS(func, bpf_arena_alloc_pages, KF_ARENA_RET | 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 | static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_arg_meta *meta, |
先从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 | static bool is_kfunc_arg_mem_size(const struct btf *btf, |
通过参数名后缀,给参数附加语义。
kfunc 的“参数语义”,有一部分是编码在 BTF 参数名里的。
- __k:必须是已知常量
- __sz:这是内存长度参数
- __szk:这是长度参数,而且也必须是常量
- __nullable:允许传 NULL
- __str:这是常量字符串
- __irq_flag:这是 irq flag 协议对象
4.2.2 特殊结构体类型识别
1 | static bool is_kfunc_arg_dynptr(const struct btf *btf, const struct btf_param *arg) |
如果参数不是普通指针,而是指向某些 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 | static enum kfunc_ptr_arg_type |
把指针参数分成verifier内部能理解的若干类
1 | - PTR_TO_CTX |
4.3 BTF 对象参数检查:process_kf_arg_ptr_to_btf_id()
这一部分处理的是 BTF 结构体对象指针
1 | case KF_ARG_PTR_TO_ALLOC_BTF_ID: |
如果kfunc参数是一个BTF对象指针,verifier先看指针来源可不可信,普通情况:它必须是 trusted / referenced pointer;如果这个 kfunc 带 KF_RCU:那至少也得是 RCU pointer
4.3.1 真正的类型匹配逻辑process_kf_arg_ptr_to_btf_id()
1 | static int process_kf_arg_ptr_to_btf_id(struct bpf_verifier_env *env, |
1 | /* Enforce strict type matching for calls to kfuncs that are acquiring |
对 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 | static int |
有几个点:
第一:为什么一定要 constant offset
1 | if (!tnum_is_const(reg->var_off)) { |
这个寄存器必须稳定地指向结构体里某个固定 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 | static int |
主要是验证,这个 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 | case KF_ARG_PTR_TO_LIST_HEAD: |
add/push 这种“插入容器”的操作,通常要求你手里有 owning ref
remove/left/right 这种操作,则允许 non-owning 或 refcounted 节点
某些场景还禁止在 rbtree callback 里调用
也就是说,我们要知道,什么时候你必须拥有这个节点,什么时候借用也可以,节点进入容器之后所有权语义怎么变
4.5 dynptr / iter / callback / irq 这类协议型参数
核心问题是,有没有遵守协议,协议有没有走对
### 4.5.1 dynptr:初始化协议 + 类型协议
1 | case KF_ARG_PTR_TO_DYNPTR: |
对参数的检查,主要关心的是: - dynptr 是不是只读 - 是不是未初始化 - 它是哪种 dynptr 类型 - 有没有底层 ref_obj_id - 某些操作会不会消耗它
比如:from_* 类函数:构造 dynptr;discard:释放相关状态;clone:需要把父 dynptr 的类型和 ref 关系传过去
所以 dynptr 的参数检查,本质上在防:未初始化使用
4.5.2 iter:必须是栈上对象,而且 new/next/destroy 协议要对
先看 is_kfunc_arg_iter():
1 | static bool is_kfunc_arg_iter(struct bpf_kfunc_call_arg_meta *meta, int arg_idx, |
对 iterator kfunc 本身,第一参数默认就是 iter state
对普通 kfunc,如果参数名带 __iter,也会被当成 iter 参数
再看 process_iter_arg():
1 | static int process_iter_arg(struct bpf_verifier_env *env, int regno, int insn_idx, |
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 | case KF_ARG_PTR_TO_CALLBACK: |
callback 参数不是普通整数,也不是普通指针,它必须是 verifier 认识的 PTR_TO_FUNC,并且 verifier 要把对应的 subprogno(子程序编号) 记下来
后续在check_kfunc_call() 里,push_callback_call() 就会继续验证这个 callback 子程序。
4.5.4 irq flag:save/restore 必须成对,状态必须在栈上
1 | static int process_irq_flag(struct bpf_verifier_env *env, int regno, |
和 iter 很像,也是协议型检查:
- save 时,flag 必须是未初始化的
- restore 时,flag 必须已经初始化
- 必须是栈上的对象
- save 之后要打标记
- restore 之后要去掉标记
所以它可以防止的是:restore 一个没保存过的 flag;重复 restore;乱用栈对象;irq 协议不配对等
4.5.5 res_spin_lock:不是普通参数,而是锁协议入口
1 | case KF_ARG_PTR_TO_RES_SPIN_LOCK: |
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_release、bpf_cgroup_acquire |
部分可自动 |
arg.graph.root.locked |
__process_kf_arg_ptr_to_graph_root() |
arg | 参数是 list/rbtree root/head | offset 必须是常量,field 必须存在,保护锁必须已持有 | 容器损坏 / 并发问题 | bpf_list_push_front、bpf_rbtree_add |
部分可自动 |
arg.graph.node.compat |
__process_kf_arg_ptr_to_graph_node() |
arg | 参数是 list/rbtree node | node 类型、offset、所属 value type 必须与 root 匹配 | 错节点插入错误容器 | bpf_rbtree_add、bpf_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_skb、bpf_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_add、bpf_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 | if (meta.release_regno) { |
所谓“返回值和生命周期逻辑”,其实是 3 层东西叠在一起:
- 先处理这次调用带来的生命周期副作用 比如 release,或者 list/rbtree 插入导致 ownership transfer。
- 再建模 R0 也就是把返回值翻译成 verifier 能理解的寄存器状态。
- 最后做后处理 比如 iter next 分叉、packet pointer 失效。
5.2 check_special_kfunc()
一些特殊的kfunc返回值,通用的scalar/ptr/void不够用,必须先优先处理一下
### 5.2.1 bpf_obj_new / bpf_percpu_obj_new
1 | if (is_bpf_obj_new_kfunc(meta->func_id) || is_bpf_percpu_obj_new_kfunc(meta->func_id)) { |
先验证程序是否真的带BTF,对应的类型是否是struct,另外验证 type id 参数是否是合法常量
如果成功过了验证之后,便把R0建模成PTR_TO_BTF_ID | MEM_ALLOC,然后把对象大小、struct meta 填进 insn_aux里面
这里面就是在防御 allocation object的可建模性
5.2.2 bpf_refcount_acquire
1 | } else if (is_bpf_refcount_acquire_kfunc(meta->func_id)) { |
可以看到,返回对象的类型 btf以及btf_id,是沿用的arg也就是参数阶段搜集到的meta->arg_btf / meta->arg_btf_id
这说明,返回值的建模有时候依靠前面参数的分析结果
5.2.3 graph node 返回值
1 | } else if (is_list_node_type(ptr_type)) { |
如果kfunc返回list或者rbtee,返回值不仅仅是一个struct,会进一步的标记为这是某个 graph root协议里面的node
这也说明了,返回值不仅仅是类型,还有容器协议语义
5.2.4 bpf_rdonly_cast / dynptr slice
看两个例子:
1 | } else if (meta->func_id == special_kfunc_list[KF_bpf_rdonly_cast]) { |
对于一些特殊的kfunc,他的返回语义非常依赖于arg带来的额外协议信息
同时,还会给R0建模成只读/UNTRUSTED/dynptr_id等标签
综上,check_special是通用返回值建模之前的一个补丁,主要是为了补全所有的返回情况
5.3 /* Check return type */
见第三部分
简单来讲,有四类返回值
5.3.1 返回标量
1 | if (btf_type_is_scalar(t)) { |
对于scalar返回值,R0 = unknown scalar,然后再记录寄存器宽度
个别的kfunc成功路径会被压缩成常量0
对于标量来说,只需要数值建模
5.3.2 返回 void *,按照标量处理
1 | } else if (btf_type_is_void(ptr_type)) { |
出于安全保护,verifier不会把 void *当成可解引用的指针,所以保守处理成一个标量
5.3.3 返回非struct * ,建模成 PTR_TO_MEM (指向内存)
建模成可访问内存
1 | } else if (!__btf_type_is_struct(ptr_type)) { |
对于 int * 或者 char * 等非struct 的指针,verifier更愿意把他们建模成一块有效内存,但是前提是得知道这块内存有多大
如果能从参数阶段拿到meta.r0_size 那就用,不行的话就从btf进行推导,如果还是不行的话就拒绝
主要是为了防止:无边界的内存访问、对象release缺失之后还能访问依附内存、RCU保护条件缺失等等
5.3.4 返回 struct *:建模成 PTR_TO_BTF_ID (指向对应的BTF)
1 | } else { |
标准的kfunc返回对象的建模方式,因为这样verifier明确的知道他是哪一个btf,n哪种结构体,后续访问以及生命周期跟踪怎么做,另外type还有一成附加语义
5.3.5 nullable / acquire / borrowed ref 的后处理
1 | if (is_kfunc_ret_null(&meta)) { |
返回值建模完之后并不是设置完type就结束,还要继续确定,会不会是NULL,会不会是acquire,会不会是 borrowed node指针等等
5.4 acquire_reference() / release_reference()
生命周期建模
5.4.1 acquire_reference()
定义:
1 | static int acquire_reference(struct bpf_verifier_env *env, int insn_idx) |
调用点:
1 | if (is_kfunc_acquire(&meta)) { |
这段逻辑的本质是:我先给这次新拿到的对象分配一个新的引用id,然后把它记录到当前verifier state 的 refs[]里面,最后把这个ID绑定到 R0.ref_obj_id里面
那么从验证器的角度来看的话,R0现在就不是一个普通的对象指针了,而是:我现在手里面新持有一个 owning ref,可以用来追踪
5.4.2 release_reference()
1 | static int release_reference(struct bpf_verifier_env *env, int ref_obj_id) |
这个函数做了两件事:
第一:他把当前状态的acquired_refs里面删掉这个 ref_id
第二:把所有持有这个ref_id的寄存器副本全部作废
防止在同一个对象的多个副本中,有一个被release了,另外的还在偷偷用。这个规定就是让其在整个状态里面传播失效
5.4.3 在 check_kfunc_call() 里,release 是怎么触发的
1 | if (meta.release_regno) { |
在第四部分里面,我们以及确定了,哪一个参数是 release对象,然后在这一部分就执行生命周期状态的转移
另外,对于dyptr,这个是特殊的case,普通的对象就走release_reference
这样调用之后,就会让某个原本alive的ref在verifier眼里失效
## 5.5 non-owning / ownership transfer
对于acquire以及release,这是一个二元的关系,但是这一部分要讲的,不只是二元关系,而是对于某些对象来说,他们的属性不是拥有,而是借用
5.5.1 ref_set_non_owning()
1 | static int ref_set_non_owning(struct bpf_verifier_env *env, struct bpf_reg_state *reg) |
这个函数就是set non owning 的逻辑:
建立在active lock的前提下,把寄存器类型加上NON_OWN_REF标签,某些情况下还顺带加上MEM_RCU标签
这个borrowed ref的语义就是,你可以读取里面的内容,但是他不是你能够拥有的,他的有效性依赖于锁的保护
5.5.2 返回 borrowed node 指针
1 | } else if (is_rbtree_node_type(ptr_type) || is_list_node_type(ptr_type)) { |
这个的处理就是,如果kfunc返回的是list/rbtree节点,并且不是acquire的返回值,那么就按照默认的non owning ref来处理
5.5.3 容器插入时的 ownership transfer
list/rbtree节点的插入:
1 | if (is_bpf_list_push_kfunc(meta.func_id) || is_bpf_rbtree_add_kfunc(meta.func_id)) { |
owning 与 non owning的转换函数:
1 | static int ref_convert_owning_non_owning(struct bpf_verifier_env *env, u32 ref_obj_id) |
可以看到:
节点在插入之前,是BPF局部手里的owning ref 节点插入之后,通过转换函数,这个对象归 list/rbtree 管 另外,BPF手里面的owning 关系要被 release掉,同时一些现有视图可能降为non-owning
5.5.4 non-owning ref 什么时候失效
看一下锁释放路径里面的调用:
1 | if (release_lock_state(cur, type, reg->id, ptr)) { |
invalidate_non_owning_refs函数本身:
1 | static void invalidate_non_owning_refs(struct bpf_verifier_env *env) |
一旦锁被释放,borrowed ref ,这些non owning 的引用必须全部作废。
也就是说,锁没了,借来的node指针就不能用了
5.6 iterator / packet pointer 失效
下面是两类调用后的状态失效:
5.6.1 process_iter_next_call()
1 | static int process_iter_next_call(struct bpf_verifier_env *env, int insn_idx, |
简单来说,就是把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 | /* Packet data might have moved, any old PTR_TO_PACKET[_META,_END] |
这里很简单,就是说如果有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 |


