2020 年 CVE-2020-8835:verifier 在 OR
指令的边界检查中存在缺陷,攻击者构造的 BPF
程序可以越界读写内核内存。CVSS 评分 7.8(HIGH),影响从 5.4
到 5.6 的所有内核版本。这绝不是第一个、也不是最后一个
verifier 漏洞——在 NVD 中以 bpf +
Linux 为关键词检索(截至 2026-06),BPF
子系统相关 CVE 累计数十起,涵盖 verifier 绕过、JIT
缺陷和权限提升等多种类型。
BPF 程序在内核态执行。一旦 verifier 被绕过——攻击者手上有内核级的任意读写能力。这正是 eBPF 安全模型必须依赖多层防御的原因:verifier 是第一层,capability 控制是第二层,JIT hardening 是第三层,BPF LSM 是第四层。本文逐层拆解这四层防御机制的内核实现。
一、多层防御模型
eBPF 的安全防线不是一条线,而是一组可以独立生效的层:
flowchart TD
U["用户态进程"] --> CAP["第一层:capability 控制<br/>CAP_BPF / CAP_SYS_ADMIN / CAP_NET_ADMIN"]
CAP -->|"通过"| VE["第二层:verifier 静态分析<br/>内存安全 / 类型安全 / 终止性"]
VE -->|"通过"| JIT["第三层:JIT hardening<br/>常数盲化 / retpoline / speculation_barrier"]
JIT -->|"通过"| LSM["第四层:BPF LSM<br/>动态安全策略(可选)"]
LSM --> EXEC["内核态执行"]
CAP -->|"拒绝"| REJECT1["EPERM"]
VE -->|"拒绝"| REJECT2["EACCES"]
每一层都是最后一道防线。capability
控制是最有效的——如果攻击者连加载 BPF
程序的权限都没有,后面的层就不需要发挥作用。但一旦权限配置不当(如容器被授予
CAP_BPF),verifier
的安全性就变成关键防线。
二、Capability 模型:权限梯度
2.1 CAP_BPF 的引入(5.8+)
Linux 5.8 之前,加载 BPF 程序需要
CAP_SYS_ADMIN——这是内核的 “万能 root”
capability,覆盖了数百种特权操作。5.8 引入了细粒度
control:
/* Linux 6.6: kernel/bpf/syscall.c */
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
...
/* 检查 BPF 加载权限 */
if (!bpf_capable())
return -EPERM;
...
}
/* Linux 6.6: include/linux/bpf.h */
static inline bool bpf_capable(void)
{
return capable(CAP_BPF) || capable(CAP_SYS_ADMIN);
}CAP_BPF 允许以下操作: - 加载 BPF
程序(包括创建 map) - 创建大多数 map 类型 - 对已创建的 map
进行读写(通过文件描述符) - pin/unpin BPF 对象
CAP_BPF 不
允许的操作(需要额外的 capability): - 加载 tracing
程序(5.8+ 起可用 CAP_BPF +
CAP_PERFMON;更早版本或部分程序类型仍需
CAP_SYS_ADMIN) -
访问内核内存(bpf_probe_read_kernel() 需要
CAP_SYS_ADMIN) - 挂载 XDP/TC BPF
到网络设备(需要 CAP_NET_ADMIN) - 创建大型
map(需要 CAP_SYS_RESOURCE)
2.1.1 CAP_PERFMON 与 tracing(5.8+)
Linux 5.8 将 CAP_PERFMON 从
CAP_SYS_ADMIN 中拆分出来,允许持有
CAP_BPF + CAP_PERFMON 的进程加载
perf_event 驱动的 tracing 程序(kprobe、tracepoint、raw
tracepoint 等),而无需完整的
CAP_SYS_ADMIN:
| 内核版本 | tracing 权限要求 | 说明 |
|---|---|---|
| < 5.8 | CAP_SYS_ADMIN |
tracing 与 perf 事件均需要完整管理员权限 |
| 5.8+ | CAP_BPF + CAP_PERFMON |
可加载 tracing 程序;fentry/fexit 等
BPF_PROG_TYPE_TRACING 程序亦在此列 |
| 任意版本 | CAP_SYS_ADMIN |
始终可覆盖上述限制 |
2.2 完整的权限梯度
| Capability 组合 | 允许的操作 | 典型用户 |
|---|---|---|
| 无特殊权限 | 无法加载 BPF | 普通用户 |
CAP_BPF |
socket filter, cgroup BPF, 简单 map | 受限容器进程 |
CAP_BPF + CAP_NET_ADMIN |
+ XDP, TC BPF, sockmap | 网络管理进程 |
CAP_BPF + CAP_SYS_ADMIN |
+ tracing(亦可)、内核内存访问 | 系统管理员 |
CAP_SYS_ADMIN |
全功能(含 CAP_BPF 效果) |
root |
CAP_BPF +
CAP_PERFMON(5.8+) |
+ tracing(kprobe/tracepoint/fentry 等) | 性能工程师 |
2.3 细粒度检查的内核实现
不同操作对应不同的 capability 检查函数:
/* Linux 6.6: kernel/bpf/syscall.c */
static bool bpf_prog_load_check_attach(int prog_type, ...)
{
switch (prog_type) {
case BPF_PROG_TYPE_TRACING:
case BPF_PROG_TYPE_LSM:
case BPF_PROG_TYPE_STRUCT_OPS:
/* tracing 挂载路径另走 perfmon_capable() 检查(5.8+ 可用 CAP_PERFMON)*/
if (!bpf_capable())
return false;
break;
case BPF_PROG_TYPE_XDP:
case BPF_PROG_TYPE_SCHED_CLS:
case BPF_PROG_TYPE_SCHED_ACT:
/* 网络类程序 */
break;
...
}
...
}在大规模容器部署中(如 Kubernetes),container runtime
可以为特定容器授予 CAP_BPF 而不授予
CAP_SYS_ADMIN,从而允许这些容器加载 BPF
程序(用于 Cilium
等服务网格)的同时限制其对内核的完整控制。
三、非特权 BPF:历史沿革与现状
3.1 从默认启用到默认禁用
eBPF 在 3.18 合入时,任何进程都可以加载 BPF 程序——只要通过 verifier 的安全检查。当时的假设是 verifier 足以保证安全。这个假设被反复证明是错误的。
关键时间线:
- 2014 (3.18):eBPF 合入,无特权限制
- 2015 (4.4):引入
kernel.unprivileged_bpf_disabledsysctl - 2017–2020:CVE-2017-16995(verifier 指针算术错误)、CVE-2018-18445(verifier 的 ALU32 边界跟踪错误)、CVE-2020-8835(OR 操作边界检查错误)——持续发现 verifier 绕过
- 2021 (5.16):默认禁用非特权
BPF(
kernel.unprivileged_bpf_disabled=1) - 2023 (6.6):改为
kernel.unprivileged_bpf_disabled=2——永久禁用,不能重新启用
3.2 unprivileged_bpf_disabled 的三种模式
/* Linux 6.6: kernel/bpf/syscall.c */
int sysctl_unprivileged_bpf_disabled __read_mostly;
static int __init bpf_global_ma_init(void) { ... }
/* 检查逻辑 */
if (sysctl_unprivileged_bpf_disabled && !bpf_capable())
return -EPERM;| 值 | 含义 | 可重新启用 |
|---|---|---|
| 0 | 允许非特权 BPF(不推荐) | N/A |
| 1 | 禁止非特权 BPF(默认) | 可以改回 0 |
| 2 | 永久禁止(生产推荐) | 不能——只能通过重启修改内核命令行 |
3.3 BPF_TOKEN:权限委派的新机制(6.8+)
为了在禁用非特权 BPF 的同时保留细粒度控制,Linux 6.8
引入了 BPF_TOKEN——一个文件描述符,代表
“信任委派”。特权进程创建 token
并传递给受限进程,受限进程使用 token 加载受限制的 BPF
程序:
/* Linux 6.8: BPF_TOKEN 使用示例(通过 bpf() syscall) */
/* 特权进程创建 token */
int token_fd = bpf(BPF_TOKEN_CREATE, &attr, sizeof(attr));
/* 受限进程使用 token 加载 BPF */
attr.prog_token_fd = token_fd;
int prog_fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
/* token 可以被设置为只允许特定程序类型和 map 类型 */BPF_TOKEN 的优势:将 “是否允许 BPF” 的决策从内核配置转移到用户态守护进程,由后者基于自己的安全策略做判断。
四、Spectre v2 缓解:bpf_jit_harden
4.1 问题:JIT 代码可以被 Spectre 利用
BPF 程序经过 JIT 编译为本地指令后,受到 CPU 推测执行(speculative execution)漏洞的影响。攻击者可以用精心构造的 BPF 程序训练分支预测器,然后通过侧信道泄漏内核内存内容。
bpf_jit_harden 提供两级缓解:
4.2 模式 1:常数盲化(constant blinding)
/* Linux 6.6: kernel/bpf/core.c */
/* 常数盲化:将 BPF_LD_IMM64 指令中的 64-bit 常数
* 替换为一系列操作——XOR 一个随机盲值,使用时再 XOR 回来
*/
static void bpf_jit_blind_constants(struct bpf_prog *prog)
{
...
/* 为每个 64-bit 立即数生成盲化操作序列 */
for (i = 0; i < insn_cnt; i++) {
if (insn->code == (BPF_LD | BPF_IMM | BPF_DW)) {
/* 生成 xor 操作,将常数与随机值混合 */
/* 后续 use 指令前再 xor 回去 */
}
}
}效果:JIT 生成的代码中不包含原始常量值(如内核地址),降低了通过 Spectre v2 泄漏敏感信息的风险。
4.3 模式 2:retpoline 强制
模式 2 在模式 1 的基础上,对所有通过 JIT 编译的间接跳转/call 指令强制使用 retpoline(return trampoline):
/* Linux 6.6: arch/x86/net/bpf_jit_comp.c */
/* bpf_jit_harden=2 时,JIT 将 BPF_CALL 和 BPF_EXIT
* 翻译为 retpoline 序列而不是直接 jmp/call
*/retpoline 序列(简化):
/* 标准 call *%r11 被替换为: */
call .Lretpoline_target
.Lretpoline_capture:
pause
lfence /* 停止推测执行 */
jmp .Lretpoline_capture
.Lretpoline_target:
mov %r11, (%rsp)
ret /* 通过 return 跳转 */4.4 开销评估
| harden 模式 | 额外开销 | 安全收益 |
|---|---|---|
| 0 (关闭) | 无 | 无(默认,适合大多数场景) |
| 1 (常数盲化) | 有(依程序而定,内核未给出固定百分比) | 防止常量泄漏(地址、密钥等) |
| 2 (全开) | 高于模式 1(含 retpoline 成本) | 防止间接跳转推测执行 |
上述开销为定性描述——具体百分比取决于 BPF
程序中常量与间接跳转的密度,本站未做基准测试。生产建议:JIT
始终开启(net.core.bpf_jit_enable=1),但
bpf_jit_harden 只在有 Spectre
风险的场景下升级。大多数部署在 harden=0
下运行,因为 verifier + capability
控制已经提供了足够的保护。
五、Spectre v4 (SSB) 缓解:speculation_barrier
5.1 Spectre v4 的攻击模型
Spectre v4(Speculative Store Bypass, SSB)允许攻击者使 CPU 推测性地忽略存储-加载依赖关系,从而在安全检查之前就加载敏感数据。
/* 危险模式:安全边界检查后访问敏感数据 */
if (user_has_permission()) { // 安全检查
data = sensitive_kernel_data; // CPU 可能在检查完成前推测性加载
process(data);
}5.2 speculation_barrier 的实现
Linux 5.17+ 引入的 bpf_speculation_barrier()
helper 在检点之间的关键边界插入 lfence
屏障:
/* Linux 6.6: kernel/bpf/core.c */
BPF_CALL_0(bpf_speculation_barrier)
{
barrier_nospec(); /* 插入 lfence(x86)或 csdb(ARM64)*/
return 0;
}在 BPF 程序中的使用位置:在安全条件检查之后、访问敏感数据之前:
/* 在 LSM BPF 程序中使用 speculation_barrier */
SEC("lsm/file_open")
int BPF_PROG(my_file_open, struct file *file)
{
struct task_struct *task = bpf_get_current_task_btf();
/* 安全边界检查 */
if (task->cred->uid.val != 0) /* 非 root */
return -EPERM;
/* speculation barrier 防止推测性加载跨越安全检查 */
bpf_speculation_barrier();
/* 访问敏感数据——已受保护 */
/* ... */
return 0;
}5.3 架构映射
| 架构 | speculation_barrier() 的指令 | 效果 |
|---|---|---|
| x86-64 | lfence |
停止推测,直到之前的所有指令完成 |
| ARM64 | csdb |
消费推测数据屏障 |
| RISC-V | fence r,r |
通用内存屏障 |
| PowerPC | ori 0,0,0 |
推测停止 |
与 bpf_jit_harden
不同,speculation_barrier
是按需使用的——只有在真正存在 Spectre v4
风险的代码路径上才需要插入,开销仅对使用了它的程序生效(单次
lfence ~5–10ns)。
六、BPF LSM:用 BPF 限制 BPF
6.1 机制:LSM hook + BPF
BPF LSM(Linux 5.7+)允许 BPF 程序挂载到 LSM(Linux Security Module)钩子上,实现自定义的安全策略。这与其他 LSM(SELinux、AppArmor)共享相同的 hook 基础设施:
/* Linux 6.6: security/security.c */
/* LSM hook 的典型调用方式 */
int security_file_open(struct file *file)
{
...
/* 调用所有已注册的 LSM(包括 BPF LSM)*/
ret = call_int_hook(file_open, file);
...
}6.2 BPF LSM 程序的语义
BPF LSM 程序的返回值语义:
| 返回值 | 含义 |
|---|---|
| 0 | 允许操作(通过检查) |
负数(-EPERM 等) |
拒绝操作(MAC override) |
关键语义:BPF LSM 不能覆盖其他 LSM 的允许决定——它是 “额外约束” 而不是 “豁免”。如果一个 LSM 返回拒绝,BPF LSM 的允许不能否决它。
/* BPF LSM 程序的典型模式 */
SEC("lsm/file_open")
int BPF_PROG(restrict_file_open, struct file *file)
{
/* 禁止特定进程打开敏感文件 */
u32 pid = bpf_get_current_pid_tgid() >> 32;
/* 查询 map 确定该 pid 是否被限制 */
u8 *restricted = bpf_map_lookup_elem(&policy_map, &pid);
if (restricted && *restricted) {
/* 获取文件名做进一步判断 */
struct path *f_path = &file->f_path;
/* ... */
return -EPERM;
}
return 0;
}6.3 BPF LSM 的可用性检查
# 检查内核是否支持 BPF LSM
bpftool feature probe | grep lsm
# 更直接的方法:检查 BPF LSM 是否已注册
cat /sys/kernel/security/lsm | grep bpf如果输出中包含 bpf,说明 BPF LSM
已启用。大多数发行版通过内核命令行 lsm=...,bpf
启用(如
lsm=lockdown,capability,yama,bpf),无需重新编译内核;仅当发行版内核构建时未包含
CONFIG_BPF_LSM 才需要换内核或自行编译。
6.4 BPF LSM 的限制
与 SELinux/AppArmor 这类完整的 LSM 相比,BPF LSM 有根本性差异:
| 维度 | 完整 LSM (SELinux) | BPF LSM |
|---|---|---|
| 策略存储 | 文件系统标签 | BPF maps(可编程) |
| 决策模型 | 预定义的允许/拒绝规则 | 程序化、动态决策 |
| 可覆盖允许决定 | 是(自己的规则) | 否(只能加约束) |
| 上下文感知 | 标签驱动 | 程序可访问内核数据结构 |
| verifier 约束 | 无需 | 需要(限制了可访问性) |
七、安全配置最佳实践
7.1 生产环境推荐配置
# /etc/sysctl.d/99-bpf-security.conf
# 禁用非特权 BPF
kernel.unprivileged_bpf_disabled=2
# JIT 始终开启(性能要求)
net.core.bpf_jit_enable=1
# JIT hardening 按环境选择:
# - 多租户 / 共享机器:1 或 2
# - 专用服务器 / 低风险:0
net.core.bpf_jit_harden=0
# 记录 JIT 镜像到 kallsyms(便于调试和审计)
net.core.bpf_jit_kallsyms=1
# 限制 BPF map 的最大内存(根据场景)
# 在 cgroup v2 中通过 memory.max 控制7.2 容器中的 BPF 安全
在 Kubernetes 环境中,BPF 安全的关键配置:
# Pod SecurityContext(Kubernetes)
securityContext:
capabilities:
add:
- BPF # 允许加载 BPF 程序
# 不添加 SYS_ADMIN——限制 tracing 和其他高权限操作
drop:
- ALL # 遵循最小权限原则7.3 审计 BPF 程序
# 列出所有已加载的 BPF 程序及其挂载点
bpftool prog list
# 检查每个程序的所有者(UID)
bpftool prog show id <ID> --json | jq '.created_by_uid'
# 使用 linux audit 监控 BPF syscall
auditctl -a always,exit -S bpf -F key=bpf_audit八、总结
eBPF 的安全模型是四层防御的叠加:
- Capability 控制——CAP_BPF 从 CAP_SYS_ADMIN 分离,实现了细粒度权限梯度。这是最有效的一层:阻止未经授权的 BPF 加载。
- Verifier 静态分析——从内存安全到类型安全,verifier 是保证 “通过 capability 的 BPF 程序不破坏内核” 的核心机制。但 ~10K 行 C 的正确性无法形式化证明。
- JIT hardening——常数盲化和 retpoline 降低 BPF JIT 代码被 Spectre 利用的风险。在生产中按需启用。
- BPF LSM——用 BPF 限制 BPF,为可编程安全策略提供 LSM hook。但它是额外约束层,不能创建 “豁免”。
非特权 BPF 从默认启用到永久禁用,反映的是内核社区对 verifier 正确性的现实评估——verifier 太复杂,不能期望它完美无缺。BPF_TOKEN 是这一认知的产物:将信任决策从内核配置转移到用户态守护进程。
本系列中,本文与 第 16
篇(BPF 并发模型) 在 “spinlock 不能与内核锁混用”
上有交集——这也是一个安全边界问题。与 第 18
篇(sched_ext) 相关——sched_ext 的 BPF 程序需要
CAP_SYS_ADMIN。
参考资料
- Linux 内核源码
kernel/bpf/syscall.c:capability 检查与BPF_TOKEN实现 - Linux 内核源码
kernel/bpf/core.c:bpf_jit_blind_constants()和常数盲化 - Linux 内核源码
arch/x86/net/bpf_jit_comp.c:retpoline JIT 编译 - Linux 内核源码
security/security.c:LSM hook 框架 - CVE-2020-8835、CVE-2021-3490、CVE-2022-23222:verifier 漏洞案例
- Linux 内核文档
Documentation/bpf/bpf_lsm.rst:BPF LSM 文档 - IETF BPF ISA 草案:指令集安全模型讨论
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】JIT 编译器后端:x86-64 与 ARM64 的 BPF→Native 翻译管线
从 bpf_jit_compile() 入口出发,拆解 BPF 字节码到 x86-64/ARM64 本地指令的翻译过程——寄存器映射策略、ALU 指令的 one-to-one/many-to-one 翻译、尾调用与 call 的本地实现、JIT 镜像的 kallsyms 集成,以及 JIT 与 interpreter 的性能边界。
【eBPF 内核实现深度拆解】Map 内核实现(上):hash / array / per-CPU 的数据结构与并发模型
从 bpf_map_ops 虚函数表出发,逐层拆解 BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_ARRAY、per-CPU 变体的内核实现——htab 的 bucket 链表与 prealloc、bpf_array 的零拷贝共享、per-CPU 分配器的无锁语义。