loader 退出了,程序却还挂着;验证器通过了,计数仍可能丢失。eBPF 的对象寿命、内存安全证明和业务正确性 是三件事。
本篇讨论扩展 BPF。seccomp 使用经典 BPF 过滤格式及专门系统调用语义,不能混作普通 eBPF 程序类型。
查阅目标 阅读位置
加载、挂接与 verifier 第 1~2 节
运行成本、map 与事件缓冲 第 3~5 节
兼容性与权限 第 6~7 节
1. 生命周期里有多个独立对象
BPF 对象:编译、加载、挂接与持续引用
编译为 BPF ELF
代码、map 描述、BTF 与重定位信息
1
用户态 loader 准备对象
按目标 BTF 做 CO-RE 重定位,建立 maps
2
内核加载并验证程序
检查类型、边界、资源及目标程序类型约束
3
挂接到支持的执行点
事件触发后使用 maps 或输出事件
4
移除挂接并释放引用
FD、link、pin 等共同决定对象寿命
5
load 成功不代表已经 attach;关闭一个 FD 也未必立即销毁对象。
对象或动作 在生命周期中的作用
程序 经解析、重定位和内核加载,持有所需 map 等引用
map 独立管理状态,寿命不简单等于 loader 进程
attach / BPF link 将程序接到事件源,按接口管理关联
pin 将对象引用保存在 bpffs,供其他进程取得
libbpf 协调 ELF 解析、重定位和内核接口调用;不能把加载成功当作已经挂接。
关闭 loader 进程不一定撤销所有程序:被 pin 或由其他对象引用的资源仍可能存在;不同 attach API 的寿命规则也不同。设计工具时应明确谁持有 link、是否 pin、退出时如何撤销,不能只依赖“进程结束应该会自动清理”。见 libbpf 总览 。
2. Verifier 检查的是可证明的安全条件
验证器沿控制流传播抽象状态,跟踪寄存器与栈槽中的标量范围、指针类型、已初始化区域和需要释放的资源。它通过状态合并、剪枝等机制处理路径,不是把每种具体输入逐一执行一遍。
检查 常见拒绝原因
类型和来源 把普通整数当内核指针,读取未初始化寄存器
内存边界 没有证明报文范围,越过 map value 或栈边界
引用与资源 取得受跟踪对象后没有在所有路径释放
helper/kfunc 契约 程序类型不允许调用,参数类型或上下文不满足
控制流与复杂度 无法证明终止,状态探索或调用/栈限制超出预算
有界循环、bpf_loop 或迭代器等机制扩大了可表达范围,但并非任意循环都接受。程序指令数量与 verifier 探索复杂度是不同限制;不应把“一百万”同时当作源代码长度、运行次数和所有程序统一上限。
BPF 栈有严格限制,常见每个子程序栈帧上限为 512 字节;调用链、tail call 组合还会增加约束。tail call 的链式限制又不是普通 BPF-to-BPF 调用深度。应以目标内核的验证日志为准,而不是记住一张跨版本通用常数表。
非空证明与 key 范围不是一件事
下面是 map 查找后的局部模式,假设 counts 的 value 类型是 __u64,key 是已初始化的 __u32:
__u32 key = 7 ;
__u64 * value = bpf_map_lookup_elem ( & counts, & key) ;
if ( ! value)
return 0 ;
__sync_fetch_and_add ( value, 1 ) ;
查找 helper 根据 map 类型和 key 内容决定结果;验证器要确认 key 内存可以读取,并在解引用 value 前确认它不是 NULL。不能把 if (x < 10) 当成任意 map 查找必定成功的证明,尤其 x 可能有符号而下界尚未检查。
普通 (*value)++ 对共享 value 的并发更新可能丢计数,例子使用原子加;原子操作仍要满足目标指令和访问约束。验证器接受不证明业务统计正确,也不保证内核或 verifier 的实现永无漏洞。见 verifier 文档 。
把一次运行的 key 是否存在,与加载时的所有路径证明分开控制。即使这个样本恰好安全,未排除的 NULL 或越界路径仍会阻止加载。
正在呈现知识画面 重新加载
这次查找成功,不等于所有路径都安全 验证器要证明所有可达路径的类型与边界。一个恰好非空且未越界的运行样本,不能排除另一条 NULL 解引用路径。
{"id":"verifier-path-proof","title":"这次查找成功,不等于所有路径都安全","summary":"验证器要证明所有可达路径的类型与边界。一个恰好非空且未越界的运行样本,不能排除另一条 NULL 解引用路径。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"guard\" checked><span data-i18n=\"guard\"></span></label><div class=\"control\"><label for=\"offset\" data-i18n=\"offset\"></label><output for=\"offset\" id=\"offset-value\"></output><input id=\"offset\" type=\"range\" min=\"-4\" max=\"20\" value=\"0\" step=\"4\"></div><label class=\"check\"><input type=\"checkbox\" id=\"found\" checked><span data-i18n=\"found\"></span></label><div class=\"two\"><div class=\"card\"><header data-i18n=\"nullPath\"></header><div id=\"null-path\" class=\"readout\"></div></div><div class=\"card\"><header data-i18n=\"valuePath\"></header><div id=\"value-path\" class=\"readout\"></div></div></div><div class=\"track-row\"><span data-i18n=\"map\"></span><div id=\"map\" class=\"track\"></div></div><div class=\"formula\" id=\"bounds\"></div><div class=\"card\"><header data-i18n=\"verdict\"></header><div id=\"verdict\" class=\"readout\"></div></div><div class=\"card sample\"><header data-i18n=\"sample\"></header><div id=\"sample\" class=\"readout\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.readout{min-height:6.4em}.sample{margin-top:12px}.formula{min-height:4.8em}#map{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}#map .cell{min-height:40px;font-size:10px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function draw(){const guard=$('#guard').checked,o=+$('#offset').value,found=$('#found').checked,safe=o>=0&&o+4<=16;set('offset-value',o+' B');set('null-path',t(guard?'returned':'unsafe'));set('value-path',t(safe?'safe':'unsafe'));cells('map',[-4,0,4,8,12,16,20].map(n=>({text:String(n),cls:o===n?'gold':n>=0&&n<16?'on':'empty'})));set('bounds','0 ≤ '+o+' ∧ '+(o+4)+' ≤ 16\\naccess ['+o+','+(o+4)+')');set('verdict',t(guard&&safe?'accepted':'rejected'));set('sample',t(found?safe?'safe':'unsafe':guard?'returned':'unsafe'));set('explanation',t('note'));}$('#guard').onchange=$('#found').onchange=$('#offset').oninput=draw;draw();","audio":false,"strings":{"accepted":"这两项检查通过","found":"这一次运行:key 存在","guard":"解引用前有 if (!p) return","heading":"这次查找成功,不等于所有路径都安全","intro":"切换 NULL 检查并改变四字节访问的偏移,再选择一次运行示例是否找到 key。观察运行示例与加载判定为何可以不同。","map":"value 的四个 4 B 单元;两侧为范围之外","note":"保持偏移 0、去掉 NULL 检查,再让本次 key 存在。样本访问安全,但加载仍拒绝;验证的是可能路径,不是只重放这一次输入。","nullPath":"路径一:p = NULL","offset":"四字节访问起始偏移","rejected":"拒绝:存在未证明的路径","reset":"重置","returned":"安全返回,不解引用","safe":"此路径访问范围成立","sample":"这次运行的结果","scope":"nullable map lookup 返回 16 B value,4 B 对齐访问,key 已初始化,其他验证条件均满足。这里只展示 NULL 和访问上下界两类规则,不运行真实 verifier,也不证明业务逻辑或并发计数正确。","scopeLabel":"模型范围与约定","unsafe":"可能非法访问","valuePath":"路径二:p 指向 value","verdict":"加载时的两项证明"}}
3. JIT 降低解释开销,不消除所有成本
支持并启用 JIT 时,BPF 指令编译成目标机器码;其他可用执行路径取决于架构和构建配置。不能为所有 BPF 指令指定固定“对应 1 到 3 条 x86 指令”:调用、原子操作、检查和 JIT 加固都会影响生成代码。
运行成本还包括 hook、map 查找、锁、事件复制及用户态消费。验证发生在加载阶段,但源程序用于证明边界的判断可能仍保留在运行代码中,因此不能说“所有边界检查运行时零开销”。
4. Maps 是具有不同语义的状态对象
Map 适合存什么 使用边界
ARRAY 固定索引配置或计数 索引范围固定,元素不能像 hash 一样删除
HASH 稀疏键值状态 容量和更新失败需要处理
PERCPU_ARRAY / HASH 分 CPU 计数 用户态需汇总各 CPU 槽,汇总不是原子快照
LRU_HASH 容量受限的近似缓存 淘汰会丢失状态,不适合无损保存所有在途请求
RINGBUF 向消费者输出事件 空间不足时预留/输出可失败
PROG_ARRAY tail call 目标 不是普通函数指针数组
per-CPU 减少 CPU 间共享写入竞争,但不代表整个 map 实现“绝对无锁”,也不自动解决用户态读取时的一致性。hash 的结构更新与 value 内多个字段的原子一致性同样需要区分。
BPF 全局变量也能保留状态,libbpf 常把相关数据段组织为 map,因此“程序完全无状态”只是过度简化。更多语义见 BPF maps 。
5. Ring buffer 与 perf buffer 如何选择
perf event array 常配合每 CPU perf 缓冲区输出 BPF 事件;BPF ringbuf 提供多生产者、单消费者的共享环形缓冲区,也可以用多个 ringbuf 分片。二者都支持变长记录,不能把 perf buffer 说成仅支持固定记录长度。
较早预留未完成,后面的事件也可能等待
ringbuf 的 reserve/submit 路径允许直接填充预留记录;较早预留的记录若尚未完成,会影响消费者继续读取后续记录。顺序对应同一个 ringbuf 内的预留序列,不等于跨 CPU 事件的绝对发生时间顺序,更不等于多个 ringbuf 之间有全局顺序。
两种方案都可能丢事件。ringbuf 预留失败要计数,perf buffer 的 lost 信息要处理;timestamp、CPU、事件标识和丢失计数应成为数据协议的一部分。是否共享内存、是否有复制也依具体输出 API 和消费路径不同,不能简单说 perf 固定两次复制、ringbuf 永远无复制。见 BPF ring buffer 。
6. BTF 与 CO-RE 的兼容边界
BTF 描述类型等信息;CO-RE 在加载阶段利用目标 BTF 处理字段偏移、大小、存在性等重定位。BPF_CORE_READ(task, pid) 可以使字段移动后的访问仍正确,但不把整个内核内部 ABI 变成稳定接口。
兼容条件 CO-RE 是否能单独解决
字段偏移变化 可通过目标 BTF 重定位处理
字段缺失 需要适当存在性分支或兼容路径
helper、kfunc、map 或 attach 不支持 不能靠字段重定位补齐
字段语义改变、函数消失、权限不足 仍需程序设计和部署处理
程序最终还必须通过目标版本 verifier。
/sys/kernel/btf/vmlinux 常提供运行内核 BTF,模块 BTF 又有自己的可用条件。CO-RE 不必在每台目标机安装完整编译器,但构建与发布仍需规划最低能力、兼容路径及验证矩阵。
7. 权限和观察工具
加载 tracing 相关程序通常还涉及 CAP_PERFMON,网络管理类操作可涉及 CAP_NET_ADMIN;CAP_BPF 并不是所有 attach 的通行证。非特权 BPF 受 sysctl、程序类型及其他限制。LSM、lockdown 等还可能继续约束操作。
安装 bpftool 后,以下命令用于只读检查对象;能看到什么取决于权限:
bpftool prog show
bpftool map show
bpftool link show
bpftool feature probe 会通过尝试相关操作探测能力,不应把它当作纯粹读取一份静态版本表。JIT 指令展示也依权限和内核配置,不保证每个程序都有可导出的机器码。
继续阅读:程序类型 、追踪与调试 。