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
关键认知:在 Kyverno 配额校准场景中,最常组合使用的是 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 程序加载到内核之前,验证器会执行以下强制性检查:

  1. 控制流完整性(CFI):确保程序无循环(除非有界循环)、无死代码、所有跳转目标有效。
  2. 栈边界检查:禁止越界访问栈空间,栈大小固定为 512 字节。
  3. 指针类型安全:禁止将标量值当作指针解引用,禁止指针算术越界。
  4. 辅助函数白名单:程序只能调用与自身类型匹配的、经过授权的内核辅助函数(如 bpf_map_lookup_elem)。
  5. Map 访问权限:验证器根据程序类型和 Map 创建时的 flag(读/写)进行静态权限检查。

3.3.2 即时编译(JIT)与沙箱执行

通过验证后,eBPF 程序会被 JIT 编译成本地机器码,并置于内核的专用执行上下文中:

  • 沙箱隔离:程序无法访问任意内核内存,只能通过辅助函数与 Map 交互。
  • 抢占与中断安全:eBPF 程序运行在非抢占上下文中(如软中断、kprobe 上下文),禁止睡眠或调用可能引起调度的函数。
  • 执行时间限制:验证器会静态估算程序的最大指令数(默认 4096 条),超限程序将被拒绝加载。

3.3.3 安全执行模型总览

下图展示了从用户态加载到内核安全执行的完整流程:

eBPF 安全执行模型流程图 eBPF 安全执行模型 用户态加载 验证器 (Verifier) JIT 编译 沙箱执行 内核事件触发 Map / 辅助函数 拒绝加载 安全执行要点: • 验证器确保无循环、无越界、指针类型安全,拒绝任何不通过的程序。 • JIT 编译后程序运行在专用沙箱中,只能通过白名单辅助函数与 Map 交互。 • 执行时间与指令数受严格限制(默认 4096 条),防止 DoS 攻击。

3.4 配额校准场景中的安全考量

在 Kyverno 策略引擎通过 eBPF 进行配额校准时,安全模型直接决定了系统的可靠性:

  • Map 权限最小化:统计类 Map 设置为只读(对用户态),判定类 Map 设置为只写(对 eBPF 程序),避免用户态篡改配额阈值。
  • 尾调用链验证:Prog Array 中的每个子程序都必须独立通过验证器检查,防止恶意程序通过尾调用注入。
  • 错误处理:eBPF 程序不允许抛出异常,所有错误(如 Map 查找失败)必须通过返回值或辅助函数(如 bpf_printk)静默处理,不得影响内核路径。
常见陷阱:不要试图在 eBPF 程序中调用 printk 或 kmalloc 等非白名单函数,这会导致验证器直接拒绝加载。所有日志输出应通过 bpf_perf_event_output 或 bpf_ringbuf_output 异步发送到用户态。