土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】eBPF 安全模型:capabilities、非特权 BPF 与 Spectre 缓解

文章导航

分类入口
kernelebpf
标签入口
#ebpf#security#capabilities#unprivileged-bpf#spectre#bpf_lsm#hardening#linux-kernel

目录

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_PERFMONCAP_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 足以保证安全。这个假设被反复证明是错误的。

关键时间线:

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 的安全模型是四层防御的叠加:

  1. Capability 控制——CAP_BPF 从 CAP_SYS_ADMIN 分离,实现了细粒度权限梯度。这是最有效的一层:阻止未经授权的 BPF 加载。
  2. Verifier 静态分析——从内存安全到类型安全,verifier 是保证 “通过 capability 的 BPF 程序不破坏内核” 的核心机制。但 ~10K 行 C 的正确性无法形式化证明。
  3. JIT hardening——常数盲化和 retpoline 降低 BPF JIT 代码被 Spectre 利用的风险。在生产中按需启用。
  4. BPF LSM——用 BPF 限制 BPF,为可编程安全策略提供 LSM hook。但它是额外约束层,不能创建 “豁免”。

非特权 BPF 从默认启用到永久禁用,反映的是内核社区对 verifier 正确性的现实评估——verifier 太复杂,不能期望它完美无缺。BPF_TOKEN 是这一认知的产物:将信任决策从内核配置转移到用户态守护进程。

本系列中,本文与 第 16 篇(BPF 并发模型) 在 “spinlock 不能与内核锁混用” 上有交集——这也是一个安全边界问题。与 第 18 篇(sched_ext) 相关——sched_ext 的 BPF 程序需要 CAP_SYS_ADMIN

参考资料

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .