3.2 Helper是内核给你的有限特权


3.2 Helper 是内核给你的有限特权

本节摘要:eBPF 程序不能直接调用内核任意函数。它只能调用一组按程序类型开放的 Helper:查 Map、读当前任务、安全拷贝内核内存、改包、输出事件。验证器按契约检查参数。把 Helper 当成 libc,是验证失败和安全事故最常见的源头之一。

本节导航

阅读完本节,你应当能够:

  1. 解释 Helper 与普通内核函数导出的区别
  2. 按“读、记、改、送”给常用 Helper 分类
  3. 说明为什么追踪程序里要 probe_read,而不是直接解引用
  4. 判断一个需求该用 Helper 完成,还是该退回用户态

第 2 章说 R1 是上下文,不是完整内核。那 task_struct 里的 comm、包上的端口、当前 cgroup,怎么拿?答案是 Helper:内核写好的、带契约的入口。你不是在内核里“调用函数”,你是在按批准过的按钮。

一、有限特权,不是标准库

用户态 C 里 memcpy 想拷多少拷多少,越界是你的进程的事。内核里同样的事能把别的进程的页毁掉。所以 eBPF 不提供通用拷贝,而提供 bpf_probe_read_kernel 一类:目标大小在验证期或运行期被卡住,失败返回错误而不是 oops。

不同程序类型看到的按钮不同。XDP 能改包、能重定向;普通 kprobe 通常不能。LSM 程序能做允许/拒绝;socket filter 不能杀进程。这不是文档疏忽,是把第 1.2 节“全权模块”拆成多把专用钥匙。

电网调度室的比喻在这里更贴:切负荷按钮存在,拆汽轮机的扳手不存在。你抱怨扳手没有,是因为扳手一旦给你,验证器就无法证明安全。

二、按“读记改送”选型

读。 bpf_get_current_pid_tgidbpf_get_current_commbpf_probe_read_kernelbpf_probe_read_user。追踪类几乎离不开它们。直接 p->mm->exe_file 在模块里常见,在 eBPF 里通常过不了验证:指针链太长,生命周期不明。CO-RE 让字段偏移可重定位,但不等于允许无界解引用。还是要走读 Helper 或被验证器认可的上下文字段。

记。 Map 系列:lookup、update、delete。原子加在计数场景里比“读改写”更不容易丢更新。记住返回值:lookup 失败得到空,你必须分支,验证器靠这个分支才相信后续解引用。

改。 网络侧 bpf_skb_store_bytes、调整包空间、重定向;追踪侧偶发 bpf_override_return(依赖内核配置与权限)。改比读危险,开放范围更窄。能在用户态改策略就不要在内核改控制流。

送。 bpf_perf_event_output 与 ringbuf 提交。送的是给用户态的证据,不是给内核的状态。大记录会打满管道,回到 3.1 的丢失问题。

类别 代表能力 典型程序类型 主要风险
当前进程、安全拷贝 追踪、LSM 读用户内存可能失败;忽略返回值
Map 查找更新 几乎全部 满员、键设计、并发
改包、改返回值 XDP/TC/部分追踪 语义破坏;权限过宽
事件外送 追踪、网络观测 管道满导致静默丢失
时间与随机 时间戳、随机数 观测与采样 用错时钟源导致对不齐

⚠️ 常见坑:忽略 bpf_probe_read_* 的返回值,继续用缓冲区。验证器有时能拦,拦不住时你把垃圾当文件名,安全产品会自己变成噪音源。
💡 关键直觉:Helper 失败是正常控制流,不是异常。内核对象随时可能消失,读失败要当成“这次跳过”,不要当成“内核坏了”。

三、工程上把逻辑推回用户态的时机

钩子里能做不等于应该做。我用三条尺子:

第一,是否必须在该瞬间完成。丢包、拒绝系统调用,必须在瞬间完成。字符串匹配一整份策略树,通常可以在用户态算完,内核只查哈希命中。

第二,验证器是否开始抱怨路径。为了过验证去写一堆 #pragma unroll 和手工边界,维护成本会超过收益。说明这段逻辑不该在字节码里。

第三,Helper 集合是否在暗示你越权。你发现自己在 kprobe 里找“怎么改调度器时间片”,这不是 Helper 缺了,是程序类型选错或需求不该用 eBPF。

瞬间决策 → Helper 在钩子里做完 批量匹配 → 用户态写入 Map,内核 O(1) 查 事后分析 → 送出事件,离开热路径

许可证与能力位也会裁剪 Helper。未签名、无 CAP_BPF 的环境,能用的按钮更少。开发机全能、生产收紧,是第 8 章部署要提前对齐的差异,不是运行时再发现。

问题:为什么不能调用 printk 随便打日志?

有跟踪打印 Helper,但它会打到公共跟踪缓冲,有速率限制,生产热路径一开就能把观测自己打崩。它是调试火柴,不是日志系统。正式外送走环形缓冲区。

问题:Helper 列表会不会随内核增加?以后写的程序会不会过时?

会增加,也会有废弃。CO-RE 主要修结构体布局,不修“这个 Helper 被删了”。要对内核版本矩阵做能力探测:没有的按钮就降级,而不是假设开发机内核等于生产。

四、读任务结构时最容易踩的契约

拿当前进程的 pid 和 comm 看起来像一句话的事,实际是一串契约。你先调用取 pid 的 Helper,得到的是当前上下文里的任务,不一定是“你心里的那个应用线程”——在软中断或某些网络钩子里,当前任务可能是 ksoftirqd。把这个 pid 当业务归属,会把一堆包算到内核线程头上。网络类问题常常要沿套接字找所有者,而不是信当前任务。

comm 只有短短十六字节量级的截断名。同名进程、截断后的长名字,都会让安全规则误伤。更稳的归属是 cgroup id 或容器运行时写入的标签 Map。Helper 能给你的是内核当时愿意给的,不是业务身份的全部。身份要在用户态对齐运行时,再下发到策略表。

读用户内存更苛刻。指针可能被并发 munmap,读失败是正常的。失败时不要把栈上未写满的缓冲当字符串送出,否则你会得到上次留下的残渣。每次先清缓冲或检查返回长度。验证器关心你不要越界,不关心你把垃圾当文件名。产品正确性是你的义务。

改包 Helper 有另一套契约:你改了长度,就要保证后续校验和与卸载硬件仍一致。有的网卡校验和卸载会和你的改写打架。改完却不更新校验,表现为对端乱序或连接重置,排障会先骂应用。能不在内核改写应用协议,就不要改。XDP 丢垃圾包通常比改 HTTP 头更安全。

把常用 Helper 的失败模式写进团队备忘:哪些会返回空,哪些会截断,哪些只在特定程序类型存在。新人最容易从用户态 libc 习惯出发,把每个调用当必胜。在 eBPF 里,必胜的调用几乎没有,分支才是默认形状。

现场笔记:软中断里的当前任务不是你的应用

网络钩子里用当前任务的 comm 做归属,大量流量被算到 ksoftirqd。业务面板上某个内核线程成了头部客户。改成从套接字找所有者、找不到就记为未知,面板才像人话。Helper 给的是当前上下文,不是业务语义。业务语义要自己用 Map 对齐运行时。

读用户内存失败没检查,文件名事件里出现上一次的路径残渣,安全规则对着残渣告警,误报爆炸。后来每次先清空再读,失败则丢弃事件。验证器不管你把垃圾当证据,产品必须管。失败分支不是啰嗦,是正确性。

改包后没处理校验和,对端重置连接,应用组被骂了两天。最后发现是钩子改了长度。能丢则丢,能在用户态改则用户态改。内核改写应用协议,排障链条会跨过太多组,成本高于那次“巧妙的改写”。巧妙在复盘里通常是骂人的词。

延伸讨论:失败分支是正确性不是啰嗦

Helper 随时可能失败。读用户内存、lookup、改包,都要当普通控制流处理。失败还继续用缓冲,就是用残渣当证据。残渣会让安全产品自己变成噪音源。当前任务不等于业务归属,网络钩子里尤其如此。归属用套接字和 cgroup 对齐运行时,对不齐就记未知。未知比错名诚实。错名会让内核线程成为头部客户,面板失去意义。改包还要管校验和与卸载硬件,管不了就不要改,能丢则丢。把常用失败模式写成备忘:谁返回空,谁截断,谁只在某程序类型存在。新人从 libc 习惯出发会假设必胜。在这里必胜几乎没有。没有失败分支的调用,不进热路径。热路径上的乐观,是误报和重置连接的预付款。预付款总会被催。

对照清单

  1. Helper 是白名单按钮,集合随程序类型变,XDP 能改包不等于 kprobe 能改包。
  2. 读失败是常态,未检查返回值会把残渣当文件名送出,误报随之爆炸。
  3. 网络钩子里当前任务可能是软中断线程,用 comm 做归属会把流量算到内核线程。
  4. comm 会截断,身份用 cgroup 和对齐运行时的标签更稳。
  5. 改包后校验和与硬件卸载可能打架,能丢则丢,能在用户态改则不要在内核改协议。
  6. 跟踪打印有速率限制,生产热路径当日志会把观测自己打崩。
  7. 逻辑能上用户态就上,内核只留瞬间决策和一次查找。
  8. 能力探测:没有的按钮降级,不要假设开发机内核等于生产。
  9. 许可证与能力位会裁剪 Helper,开发机全能生产收紧要提前对齐。
  10. lookup 后必须判空,验证器靠这个分支才相信后续解引用。
  11. 把指针存成整数再取回会丢掉类型,下次再 lookup,不要自己保管裸指针。
  12. 失败分支写进备忘,新人从 libc 习惯来会假设必胜,这里必胜几乎没有。

把 Helper 当标准库,是从用户态搬习惯时最容易犯的错。标准库失败少,失败了进程自己死。这里失败多,失败了内核还要继续为别人服务。所以每次调用都是一次可能的空。空不是尴尬,是设计。设计要求你写分支。分支写烦了,说明这段逻辑不该在钩子里。钩子里只留那些失败了就跳过也没关系、或失败了就必须放弃本次决策的动作。烦是一种信号。信号比再封装一层假必胜 API 诚实。

核心回顾

  • Helper 是白名单按钮:不是 libc,也不是内核全部导出符号。
  • 集合随程序类型变:XDP 能改包,kprobe 未必能。
  • 读要走安全拷贝:长指针链是验证失败重灾区。
  • 失败是常态:读内核/用户内存随时可能失败,必须分支。
  • 逻辑能上用户态就上:内核只留瞬间决策与 O(1) 查找。
  • 能力探测:Helper 可用性是版本与权限的函数,不是源码里写了就能用。

下一章决定火花在哪一拍点燃:追踪、网络、系统三类挂载点,选错瞬间就选错了整场观测。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U