3. eBPF 技术基础:程序类型、Map 与 Prog Array 及安全执行模型
eBPF(extended Berkeley Packet Filter)是 Linux 内核中一种革命性的沙箱化运行时环境,允许用户在不修改内核源码或加载内核模块的情况下,安全、高效地注入自定义程序。本章将聚焦于 eBPF 的程序类型、核心数据结构(Map 与 Prog Array)以及内核中的安全执行模型,为后续理解 Kyverno 如何通过 eBPF 实现配额校准奠定技术基础。
3.1 eBPF 程序类型与挂载点
eBPF 程序并非“万能胶水”,每种程序类型都绑定特定的内核事件挂载点(Hook Point),并拥有独立的上下文结构(Context)和返回值语义。下表列出了与系统观测、网络策略及安全审计最相关的几种类型:
| 程序类型 | 挂载点示例 | 典型用途 | 上下文结构 |
|---|---|---|---|
BPF_PROG_TYPE_KPROBE |
内核函数入口/返回 | 动态追踪、延迟分析、参数捕获 | struct pt_regs |
BPF_PROG_TYPE_TRACEPOINT |
静态 tracepoint | 系统调用计数、文件操作审计 | 特定 tracepoint 结构 |
BPF_PROG_TYPE_XDP |
网卡驱动层 | 高速包过滤、DDoS 缓解 | struct xdp_md |
BPF_PROG_TYPE_SCHED_CLS |
TC(Traffic Control) | 容器网络策略、带宽管控 | struct __sk_buff |
BPF_PROG_TYPE_CGROUP_SKB |
cgroup 网络出入口 | 容器级网络访问控制 | struct __sk_buff |
BPF_PROG_TYPE_PERF_EVENT |
硬件/软件性能计数器 | CPU 采样、内存访问模式分析 | struct bpf_perf_event_data |
KPROBE(捕获系统调用参数)与 CGROUP_SKB(统计容器网络流量),两者通过 eBPF Map 共享数据。
3.2 eBPF Map 与 Prog Array
eBPF Map 是内核与用户态、以及不同 eBPF 程序之间交换数据的核心抽象。Map 类型决定了键值对的存储位置、访问方式与并发语义。Prog Array 是一种特殊的 Map,它存储的是其他 eBPF 程序的文件描述符(FD),用于实现程序链式调用或尾调用(Tail Call)。
3.2.1 常用 Map 类型对比
| Map 类型 | 键/值类型 | 并发安全 | 典型场景 |
|---|---|---|---|
BPF_MAP_TYPE_HASH |
任意固定大小 | Per-CPU 锁 | 进程级统计、配置查找 |
BPF_MAP_TYPE_ARRAY |
整数索引 / 固定值 | 无锁(Per-CPU) | 计数器、预分配查找表 |
BPF_MAP_TYPE_PERCPU_HASH |
任意固定大小 | 无锁(Per-CPU 分片) | 高性能聚合统计 |
BPF_MAP_TYPE_RINGBUF |
可变长度记录 | 无锁(生产者-消费者) | 事件流输出到用户态 |
BPF_MAP_TYPE_PROG_ARRAY |
整数索引 / 程序 FD | 原子替换 | 尾调用跳转表 |
3.2.2 Prog Array 与尾调用机制
Prog Array 允许一个 eBPF 程序在执行过程中通过 bpf_tail_call() 辅助函数跳转到另一个 eBPF 程序,且跳转后栈帧被完全替换,从而避免内核栈溢出。这种机制在复杂策略链中至关重要:
- 策略拆分:将配额校验、日志记录、告警触发拆分为独立的小程序,通过 Prog Array 串联。
- 动态更新:用户态可以原子性地替换 Prog Array 中某个索引对应的程序,实现策略热更新而不中断已有连接。
- 性能优势:尾调用仅消耗一次函数调用开销,远低于多次 Kprobe 或 Tracepoint 触发。
XDP 入口程序 → bpf_tail_call(prog_array, index=0) → 流量统计程序 → bpf_tail_call(prog_array, index=1) → 配额判定程序
3.3 eBPF 在内核中的安全执行模型
eBPF 的安全模型是“验证 + 沙箱 + 特权降级”三位一体的设计,确保即使程序存在逻辑缺陷,也不会导致内核崩溃或权限逃逸。
3.3.1 验证器(Verifier)的静态检查
在 eBPF 程序加载到内核之前,验证器会执行以下强制性检查:
- 控制流完整性(CFI):确保程序无循环(除非有界循环)、无死代码、所有跳转目标有效。
- 栈边界检查:禁止越界访问栈空间,栈大小固定为 512 字节。
- 指针类型安全:禁止将标量值当作指针解引用,禁止指针算术越界。
- 辅助函数白名单:程序只能调用与自身类型匹配的、经过授权的内核辅助函数(如
bpf_map_lookup_elem)。 - Map 访问权限:验证器根据程序类型和 Map 创建时的 flag(读/写)进行静态权限检查。
3.3.2 即时编译(JIT)与沙箱执行
通过验证后,eBPF 程序会被 JIT 编译成本地机器码,并置于内核的专用执行上下文中:
- 沙箱隔离:程序无法访问任意内核内存,只能通过辅助函数与 Map 交互。
- 抢占与中断安全:eBPF 程序运行在非抢占上下文中(如软中断、kprobe 上下文),禁止睡眠或调用可能引起调度的函数。
- 执行时间限制:验证器会静态估算程序的最大指令数(默认 4096 条),超限程序将被拒绝加载。
3.3.3 安全执行模型总览
下图展示了从用户态加载到内核安全执行的完整流程:
3.4 配额校准场景中的安全考量
在 Kyverno 策略引擎通过 eBPF 进行配额校准时,安全模型直接决定了系统的可靠性:
- Map 权限最小化:统计类 Map 设置为只读(对用户态),判定类 Map 设置为只写(对 eBPF 程序),避免用户态篡改配额阈值。
- 尾调用链验证:Prog Array 中的每个子程序都必须独立通过验证器检查,防止恶意程序通过尾调用注入。
- 错误处理:eBPF 程序不允许抛出异常,所有错误(如 Map 查找失败)必须通过返回值或辅助函数(如
bpf_printk)静默处理,不得影响内核路径。
printk 或 kmalloc 等非白名单函数,这会导致验证器直接拒绝加载。所有日志输出应通过 bpf_perf_event_output 或 bpf_ringbuf_output 异步发送到用户态。