2. eBPF程序生命周期:加载、验证、附加、分离、卸载,以及每个阶段对租户隔离的影响
eBPF程序从编写到销毁,经历五个核心阶段:加载、验证、附加、分离、卸载。每个阶段都是内核安全模型的检查点,也是租户隔离策略的生效锚点。理解各阶段内核态与用户态的交互,是设计多租户eBPF控制平面的基础。
核心认知: eBPF生命周期不是简单的“加载→运行→卸载”线性流程。验证阶段是隔离的第一道防线,附加阶段决定隔离边界,分离与卸载阶段则影响资源回收与残留风险。多租户场景下,每个阶段都必须有明确的租户上下文标记。
2.1 生命周期全景:五阶段与内核交互点
下图展示eBPF程序从用户态提交到内核态销毁的完整路径,以及每个阶段涉及的关键内核组件与租户隔离检查点。
2.2 加载阶段:身份注入与配额校验
加载是eBPF程序进入内核的第一道门。用户态通过 bpf() 系统调用传入程序字节码,内核在创建 struct bpf_prog 对象时,必须完成租户身份绑定。
隔离关键点: 加载阶段必须将租户ID(Tenant ID)写入程序元数据。内核通过
bpf_prog->aux->tenant_id 字段(需内核补丁支持)或cgroup/namespace上下文推导租户归属。控制平面应在加载前完成配额检查,避免无效加载消耗内核资源。
| 加载子阶段 | 内核操作 | 租户隔离检查项 | 失败后果 |
|---|---|---|---|
| 字节码复制 | copy_from_user() | 校验用户态地址合法性 | 返回 -EFAULT |
| 程序元数据创建 | 分配 bpf_prog 结构体 | 注入租户ID、检查租户程序总数配额 | 返回 -ENOSPC |
| 许可证检查 | GPL兼容性验证 | (全局策略,不区分租户) | 返回 -EINVAL |
| 运行环境初始化 | 设置prog type、expected attach type | 校验租户是否被允许使用该prog type | 返回 -EPERM |
2.3 验证阶段:隔离的第一道防线
BPF验证器(verifier)是内核最严格的代码审查器。它执行深度优先遍历,模拟每条指令的执行路径,确保程序不会崩溃内核、不会泄露信息、不会越权访问。在多租户环境中,验证器必须额外检查跨租户访问。
隔离风险提示: 标准BPF验证器不感知租户概念。如果内核未打租户隔离补丁,验证器不会阻止程序访问其他租户的map。这是多租户eBPF部署中最常见的安全漏洞。必须通过BPF LSM或自定义验证器扩展来强制执行跨租户访问控制。
2.4 附加阶段:绑定租户上下文到Hook点
验证通过后,程序通过 BPF_PROG_ATTACH 或 BPF_RAW_TRACEPOINT_OPEN 等命令附加到指定事件源。附加操作将程序与内核中的hook点关联,此时必须将租户上下文写入hook点的私有数据区。
| 附加类型 | Hook点示例 | 租户上下文绑定方式 | 隔离粒度 |
|---|---|---|---|
| cgroup BPF | cgroup/{inet_ingress, inet_egress} | cgroup ID 映射到租户ID | 容器/Pod级别 |
| tc BPF | sch_clsact / sch_ingress | 网络命名空间 + 设备索引 | 网络设备级别 |
| XDP | netdev 驱动层 | 接口索引 + 租户标签 | 物理/虚拟网卡级别 |
| tracepoint/kprobe | 系统调用、内核函数 | 当前进程的cgroup/namespace | 进程/容器级别 |
| LSM BPF | security_* hook | BPF LSM 上下文中的租户字段 | 内核对象级别 |
2.5 分离阶段:清理Hook点上的租户残留
分离操作通过 BPF_PROG_DETACH 或关闭程序文件描述符触发。内核将程序从hook点的程序链表中移除,但不会立即释放程序本身(引用计数机制)。
隔离关键点: 分离阶段必须清理hook点上缓存的租户上下文。如果hook点维护了per-tenant的统计计数器或过滤规则,分离时必须同步清除,防止后续附加的其他租户程序读取到残留数据。控制平面应在分离后执行租户资源记账更新。
分离阶段的隔离检查清单:
- Hook点清理: 确认程序已从hook点的程序数组中移除,无并发执行路径
- 租户上下文清除: 删除hook点私有数据中与该程序关联的租户标签
- 引用计数检查: 确保无其他hook点或map仍引用该程序
- 审计日志记录: 记录租户ID、程序ID、分离时间、分离原因
2.6 卸载阶段:资源回收与隔离终结
当程序的引用计数降为零(所有文件描述符关闭、所有hook点分离),内核触发 __bpf_prog_put() 释放 bpf_prog 结构体及其关联资源。这是租户隔离的最后一道保障。
| 资源类型 | 回收动作 | 隔离影响 | 泄漏风险 |
|---|---|---|---|
| 指令镜像 (insnsi) | kfree(prog->insnsi) | 释放租户代码内存 | 低 |
| JIT编译代码 | bpf_jit_free(prog) | 释放可执行内存 | 中(需确认无其他CPU执行) |
| map引用 | bpf_map_put() 递减引用 | 可能触发map卸载 | 高(循环引用导致泄漏) |
| 辅助数据结构 | 释放btf、line_info等 | 释放调试元数据 | 低 |
| 租户审计记录 | 更新租户资源统计 | 扣减租户程序计数 | 中(计数不一致) |
隔离风险提示: map的引用计数是卸载阶段最常见的泄漏点。如果程序A引用了租户B的map,程序A卸载时未正确释放map引用,会导致租户B的map无法卸载,造成跨租户资源泄漏。控制平面必须维护全局的“程序↔map”引用关系图,在卸载时执行完整性检查。
2.7 生命周期各阶段隔离策略总结
下表汇总五个阶段的核心隔离策略、检查时机与推荐实现方式,供控制平面设计参考。
| 生命周期阶段 | 核心隔离策略 | 检查时机 | 推荐实现方式 |
|---|---|---|---|
| 加载 | 租户身份注入 + 配额校验 | bpf() 系统调用入口 | 内核补丁:bpf_prog->aux->tenant_id |
| 验证 | 跨租户访问控制 + 信息泄漏防护 | verifier 指令模拟阶段 | BPF LSM + verifier 扩展 |
| 附加 | 租户上下文绑定 + hook点隔离 | BPF_PROG_ATTACH 命令处理 | cgroup/namespace 映射 + 租户标签 |
| 分离 | hook点清理 + 租户残留清除 | BPF_PROG_DETACH 或 fd关闭 | hook点私有数据清理回调 |
| 卸载 | 资源回收 + 引用完整性检查 | __bpf_prog_put() 执行 | 引用关系图 + 循环引用检测 |
设计建议: 控制平面应在用户态维护一份“租户→程序→map→hook点”的完整拓扑图。每次生命周期变更时,先在图谱中模拟执行隔离检查,通过后再下发到内核。这种“先模拟、后执行”的策略可以避免内核态隔离检查失败导致的程序拒绝服务。