一、BPF LSM 不是另一个 LSM:内核机制与执行语义
在讨论 BPF LSM 的能力之前,必须先澄清一个常见的误解:BPF LSM 并不是一个独立的"安全模块",像 SELinux 或 AppArmor 那样有自己完整的策略语言和对象标签体系。它是一个LSM 框架的扩展机制,允许开发者将自定义的 eBPF 程序挂载到已有的 LSM hook 点上,与现有的 major/minor LSM 协同工作。
1.1 LSM 框架的调用链
LSM 框架在内核关键路径上预埋了 200 余个 hook 函数。以文件打开为例,简化后的调用链如下:
vfs_open()
-> do_dentry_open()
-> security_file_open() // LSM hook 入口
-> call_int_hook(file_open, 0, file, ...) // 遍历所有注册的 LSMsecurity_file_open() 不是单一模块的函数,而是 LSM 框架的分发器。它会依次调用所有已注册 LSM 的 file_open 实现。内核通过 cat /sys/kernel/security/lsm 可以查看当前激活的模块列表,例如:
lockdown,capability,landlock,yama,selinux,bpf,ima,evm
注意这里的顺序:capability 总是第一(因为 capability breaking 是内核基础能力),yama、landlock 等 minor LSM 居中,selinux 或 apparmor 作为 major LSM 排在末尾。bpf 作为一个 minor LSM,被插入到这个链中——这意味着 BPF LSM 程序的执行时机早于 major LSM,但晚于 capability 和 lockdown。
这个顺序有重要的工程含义:BPF LSM 可以拦截已经被 capability 检查过的操作,但它无法覆盖 capability 本身的逻辑。如果你的策略目标是限制 CAP_SYS_PTRACE 的使用,BPF LSM 可以在 ptrace 操作的 LSM hook 中补充检查,但不能替代 capability 的权限模型。
1.2 BPF LSM 的挂载与返回值语义
BPF LSM 程序通过 BPF_PROG_TYPE_LSM 类型加载,使用 bpf_link 机制 attach 到具体的 LSM hook。内核源码中的关键数据结构如下(来自 kernel/bpf/bpf_lsm.c):
BTF_SET_START(bpf_lsm_hooks)
BTF_ID(func, bpf_lsm_file_open)
BTF_ID(func, bpf_lsm_inode_unlink)
BTF_ID(func, bpf_lsm_task_fix_setuid)
// ... 200+ hooks
BTF_SET_END(bpf_lsm_hooks)每个 hook 的函数签名由 BTF(BPF Type Format)定义,verifier 在加载时会严格检查 eBPF 程序的参数类型和返回值类型。返回值语义是 BPF LSM 最容易踩坑的地方:
- 返回 0:允许操作继续,LSM 链中的下一个模块将继续检查
- 返回负数(如 -EPERM):拒绝操作,LSM 链立即终止
- 返回正数:在部分 hook 中被调用方误解释为有效指针,导致内核 panic
CVE-2024-47703 就是典型的返回值语义问题。在 file_alloc_security hook 中,文件系统子系统使用 IS_ERR() 宏检查返回值,该宏只识别负值错误码。如果 BPF LSM 程序返回正数,文件系统会将其当作一个有效的 struct file * 指针使用,导致空指针解引用或越界访问。这个漏洞的修复方案不是在文件系统中增加检查,而是在 BPF verifier 层增加返回值约束:LSM 程序不允许返回正数。
这个案例说明了一个工程边界:BPF LSM 程序的返回值不是完全自由的,它受到调用方上下文和内核子系统实现细节的约束。即使 verifier 通过了,也不意味着程序在所有 hook 上都是安全的。
1.3 与 major LSM 的协同关系
BPF LSM 与 SELinux/AppArmor 的关系可以用"补充"而非"替代"来概括:
| 维度 | SELinux/AppArmor | BPF LSM |
|---|---|---|
| 策略模型 | 基于标签/路径的完整 MAC 体系 | 基于 eBPF 程序的点状拦截 |
| 策略更新 | 需重新加载策略文件或重启 | 运行时热加载/卸载 |
| 对象覆盖 | 全系统所有文件/进程/端口 | 仅挂载的 hook 点 |
| 审计能力 | 内置 AVC 审计日志 | 需自行通过 perf event/ring buffer 输出 |
| 学习曲线 | 策略语言复杂但文档成熟 | C/Rust + eBPF verifier 约束 |
在生产环境中,更常见的模式是"双层防御":SELinux 提供基线强制访问控制,BPF LSM 在关键路径上提供动态、细粒度的补充策略。例如,KubeArmor 的架构就是优先使用 BPF-LSM 实现运行时策略,但在需要正则表达式匹配或 Capabilities 限制的场景回退到 AppArmor。
BPF LSM 在内核 5.7 被引入时,社区的主流反应是:"终于可以用 eBPF 写安全策略了,不用再碰 SELinux 的策略语言。" 三年过去,这个判断只对了一半。BPF LSM 确实让安全策略变得可编程,但它并没有、也不打算取代 SELinux 或 AppArmor 的强制访问控制体系。真正发生变化的是安全策略的交付方式:从编译时或启动时加载的静态策略文件,变成了运行时可热加载、可观测、可版本化的 eBPF 程序。
这个转变的工程意义远大于语法层面的便利。它意味着安全团队可以把策略当作代码来管理——用 CI/CD 流水线发布策略、用 Prometheus 监控策略命中、用 A/B 测试验证策略效果。但这也意味着,安全策略的可靠性边界从"策略引擎是否正确解析了配置文件"扩展到了"eBPF verifier 是否允许这段程序加载""JIT 编译后的指令是否引入了新的攻击面""LSM hook 的返回值语义是否与调用方兼容"。
本文从三个维度展开:第一,BPF LSM 在内核中的真实执行机制与 hook 语义;第二,从 Kernel 5.7 到 6.18 的关键演进与工程约束;第三,生产环境中使用 BPF LSM 的能力边界、已知风险与落地建议。目标不是写一份 BPF LSM 的入门教程,而是回答一个工程问题:当你决定在生产环境用 BPF LSM 替代或补充传统 LSM 时,你究竟在依赖什么,以及这种依赖的失效模式是什么。
二、从 Kernel 5.7 到 6.18:能力演进与工程约束
BPF LSM 从 5.7 引入至今,经历了三个阶段的演进:基础能力落地(5.7-5.15)、安全加固与生态扩展(5.16-6.10)、以及机架级安全与签名验证(6.11-6.18)。每个阶段都伴随着新的能力,也引入了新的约束。
2.1 基础能力期:5.7 的引入与 5.15 的成熟
Kernel 5.7 首次引入 BPF_PROG_TYPE_LSM,支持将 eBPF 程序 attach 到 LSM hook。这个版本的关键约束包括:
- 仅支持 x86_64:ARM64 的 BPF LSM 支持在后续版本中逐步完善
- 需要
CONFIG_BPF_LSM=y:大多数发行版默认未启用,需修改 GRUB 启动参数lsm=...,bpf - BTF 依赖:
CONFIG_DEBUG_INFO_BTF=y是 CO-RE(Compile Once-Run Everywhere)的前提 - GPL 许可证要求:所有 LSM 程序必须声明 GPL 兼容许可证,verifier 会拒绝非 GPL 程序
5.15 是一个重要的稳定节点。SUSE 文档显示,SLE 15 SP3 的 Live Patch 50 在这个内核版本上验证了跨架构和虚拟机环境的一致性。对于 BPF LSM 来说,5.15 意味着 verifier 对 LSM 程序的检查已经足够成熟,可以支撑生产级的工作负载。
2.2 安全加固期:CVE-2024-47703 与 verifier 增强
2024 年的 CVE-2024-47703 是一个标志性事件。它暴露了 BPF LSM 与内核子系统之间的接口契约问题,修复方案也反映了社区对 BPF LSM 安全模型的重新思考:
漏洞根因:file_alloc_security hook 的调用方(文件系统)只处理负值错误码,而 BPF LSM 程序可以返回任意整数。
修复策略:在 verifier 层增加 bpf_lsm_verify_prog 检查,拒绝可能返回正值的程序。具体代码如下(来自 openEuler 的 backport patch):
int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog, const struct bpf_prog *prog) {
u32 btf_id = prog->aux->attach_btf_id;
const char *func_name = prog->aux->attach_func_name;
if (!prog->gpl_compatible) {
bpf_log(vlog, "LSM programs must have a GPL compatible license\n");
return -EINVAL;
}
if (btf_id_set_contains(&bpf_lsm_disabled_hooks, btf_id)) {
bpf_log(vlog, "attach_btf_id %u points to disabled hook %s\n",
btf_id, func_name);
return -EINVAL;
}
if (!btf_id_set_contains(&bpf_lsm_hooks, btf_id)) {
bpf_log(vlog, "attach_btf_id %u points to wrong type name %s\n",
btf_id, func_name);
return -EINVAL;
}
// ... 返回值约束检查
}除了返回值检查,社区还引入了disabled hooks 列表。部分 LSM hook 因为与内核子系统的交互过于复杂,暂时禁止 BPF LSM 程序 attach。例如 bpf_lsm_vm_enough_memory、bpf_lsm_inode_need_killpriv 等 hook 就在禁用列表中。这个列表不是静态的,随着内核版本的迭代,部分 hook 可能会被重新启用。
2.3 机架级安全:6.18 的 signed BPF 与 multi-LSM
Linux Kernel 6.18(2025 年 11 月发布)是 BPF LSM 的一个重要里程碑。根据 Linux Journal 的报道,6.18 引入了两项关键安全特性:
Cryptographically signed BPF programs:eBPF 字节码在加载时可以被验证签名。这意味着攻击者即使获得了内核的 CAP_BPF 权限,也无法加载未签名的恶意 eBPF 程序。这个特性对 BPF LSM 尤为重要,因为 LSM 程序运行在内核安全决策路径上,一旦被篡改,后果比网络过滤或性能监控更严重。
Refined multi-LSM support:6.18 改进了多个 LSM 同时运行时的兼容性。在之前的版本中,同时启用 SELinux 和 BPF LSM 可能会导致策略冲突或性能退化。6.18 的改进使得"SELinux + BPF LSM"的双层模式更加稳定。
然而,6.18 也有一个重要的移除:Bcachefs 支持被从主线内核中移除。这对使用 Bcachefs 的系统意味着需要依赖外部模块或 DKMS 构建,而外部模块与 BPF LSM 的交互可能存在未测试的边界。
2.4 当前内核版本的工程约束总结
| 内核版本 | BPF LSM 状态 | 关键约束 |
|---|---|---|
| 5.7-5.14 | 实验性 | x86_64 only, verifier 不成熟, 部分 hook 不可用 |
| 5.15-6.10 | 生产可用 | ARM64 支持完善, CVE-2024-47703 修复, disabled hooks 列表 |
| 6.11-6.18 | 增强安全 | Signed BPF, multi-LSM 兼容性改进, Bcachefs 移除 |
工程建议:生产环境至少使用 5.15+,如果安全合规要求签名验证,需升级到 6.18 并配置 eBPF 签名证书链。
三、生产落地的能力边界:什么能做,什么不能
BPF LSM 的工程价值在于"运行时可编程",但这个可编程性是有明确边界的。理解这些边界,比理解它能做什么更重要。
3.1 能做的:三类典型场景
场景一:实时补丁与漏洞缓解
BPF LSM 最常见的生产场景是"热修复"——在不重启内核、不加载内核模块的情况下,对已知漏洞的触发路径进行拦截。一个典型的例子是利用 cred_prepare hook 阻止 unshare(CLONE_NEWUSER) 的滥用:
SEC("lsm/cred_prepare")
int BPF_PROG(deny_unshare_cred, struct cred *new, const struct cred *old, gfp_t gfp) {
// 检查当前进程是否正在执行 unshare 系统调用
// 通过 task_struct 的 flags 或系统调用上下文判断
if (current->thread_info.flags & _TIF_SYSCALL_EMU) {
// 进一步检查 unshare_flags 是否包含 CLONE_NEWUSER
// 这里需要读取 pt_regs 中的 orig_ax 或相关寄存器
return -ENOMEM; // 返回错误阻止 cred 准备
}
return 0;
}这个场景的实际性能开销已经被量化:根据代码之家的实测数据,加载上述策略后,单次 unshare 系统调用的 CPU 周期从约 63,294 增加到约 70,138,增幅约 10%(6,844 个周期)。由于 unshare 通常在任务创建时调用,不会在正常执行期间重复触发,这个开销对大多数工作负载是可接受的。
场景二:文件完整性保护
通过 file_open、inode_unlink、inode_rename 等 hook,可以实现比 AIDE/Tripwire 更实时的文件保护:
SEC("lsm/file_open")
int BPF_PROG(protect_file, struct file *file) {
int flags = file->f_flags;
// 检查是否为写模式
if ((flags & O_ACCMODE) == O_WRONLY || (flags & O_ACCMODE) == O_RDWR) {
// 通过 inode 号或 bpf_d_path 检查目标文件
// 如果在保护列表中,返回 -EPERM
return -EPERM;
}
return 0;
}这个模式的核心优势是拦截而非检测。传统的文件完整性工具只能在文件被修改后报警,而 BPF LSM 可以在修改请求到达 VFS 层之前就拒绝它。对于 /etc/passwd、/etc/shadow 等关键文件,这意味着攻击者即使获得了 root 权限,也无法直接修改它们。
场景三:容器运行时安全
KubeArmor 是 BPF LSM 在容器场景的代表性应用。它的架构设计反映了 BPF LSM 在云原生环境中的实际定位:
KubeArmor Controller (Kubernetes CRD)
|
v
KubeArmor DaemonSet (每个节点一个)
|-- BPF-LSM 引擎:优先策略执行
|-- AppArmor 回退:BPF-LSM 不支持的场景
|-- 审计日志:通过 ring buffer 输出到用户态
KubeArmor 选择 BPF-LSM 作为优先引擎的原因很明确:
- 细粒度控制:系统调用级别的拦截,可精确到容器/进程/文件
- 动态更新:Kubernetes Pod 调度变化时,策略可以实时加载/卸载
- 低性能损耗:JIT 编译后的 eBPF 程序将开销控制在 3% 以内
但 KubeArmor 的文档也明确列出了当前版本的两个限制:
- 正则表达式匹配:
MatchPatterns策略目前依赖 AppArmor 的路径模式匹配 - Capabilities 限制:
CAP_NET_ADMIN等 Linux Capabilities 的控制正在开发中
3.2 不能做的:四个明确边界
边界一:不能替代完整的 MAC 体系
BPF LSM 程序是"点状"的——每个程序只挂载到一个 hook 上,处理一个具体的决策点。它不像 SELinux 那样有一个全局的安全标签系统和策略数据库,能够推理出"这个进程(httpd_t)能否访问那个文件(httpd_sys_content_t)"。如果你的安全模型需要基于类型的强制访问控制,BPF LSM 无法独立完成,必须配合 SELinux 或 AppArmor。
边界二:不能覆盖所有 hook
内核源码中的 bpf_lsm_disabled_hooks 列表明确禁用了部分 hook。截至 2024 年的 patch,禁用的 hook 包括:
bpf_lsm_vm_enough_memorybpf_lsm_inode_need_killprivbpf_lsm_inode_getsecuritybpf_lsm_inode_listsecuritybpf_lsm_inode_copy_up_xattrbpf_lsm_getprocattrbpf_lsm_setprocattrbpf_lsm_key_getsecuritybpf_lsm_audit_rule_matchbpf_lsm_ismaclabel
这些 hook 被禁用的原因各不相同:有些涉及内核内存管理的关键路径,有些与现有的安全属性系统(如 xattr、keyring、audit)有复杂的交互。试图绕过这个限制(例如通过内核模块直接修改 bpf_lsm_disabled_hooks)会导致 verifier 拒绝加载,甚至触发内核安全机制。
边界三:不能处理所有返回值语义
CVE-2024-47703 已经证明,不同 hook 的调用方对返回值的解释是不同的。file_alloc_security 的调用方使用 IS_ERR() 检查,而 security_file_open 的调用方使用简单的非零检查。BPF LSM 程序必须了解每个 hook 的具体语义,而这一点在文档中并不总是明确的。工程实践中, safest 的做法是:
- 只允许返回 0(允许)或 -EPERM(拒绝)
- 避免返回其他错误码,除非明确知道调用方会正确处理
- 避免返回任何正值
边界四:不能保证自身的安全性
BPF LSM 程序本身也是攻击目标。攻击者如果获得了 CAP_BPF 权限,可以:
- 卸载现有的 BPF LSM 程序
- 加载恶意的 BPF LSM 程序,绕过安全策略
- 利用 eBPF verifier 的漏洞加载本应被拒绝的程序
Kernel 6.18 的 signed BPF 特性就是为了缓解这个风险,但它引入了新的管理复杂性:证书链的维护、签名密钥的保护、更新策略的制定。如果签名密钥泄露,攻击者可以签署任意 BPF 程序,整个安全模型就会崩溃。
3.3 性能与稳定性的工程权衡
| 维度 | 传统 LSM (SELinux/AppArmor) | BPF LSM |
|---|---|---|
| 策略加载延迟 | 秒级(加载策略文件) | 毫秒级(verifier + JIT) |
| 运行时开销 | 低(内核原生代码) | 极低(JIT 编译后接近原生) |
| 内存占用 | 固定(策略数据库) | 动态(eBPF maps + 程序) |
| 内核升级影响 | 需重新验证策略兼容性 | 需重新验证 verifier 行为 |
| 调试难度 | 成熟(audit2allow 等工具) | 较高(verifier 日志 + bpf_trace_printk) |
工程建议:在已有 SELinux/AppArmor 的环境中,BPF LSM 适合作为"快速响应层"——用于临时拦截新漏洞、实验性策略验证、或针对特定工作负载的精细化控制。不要期望用 BPF LSM 完全替代现有的 MAC 体系,至少在 2026 年的内核版本中,这种替代在工程上是不成熟的。
四、实战构造:一个最小可复现的 BPF LSM 拦截程序
为了验证上述机制,下面给出一个可在 Ubuntu 22.04/24.04(内核 5.15+)上直接编译运行的最小示例。该程序拦截对 /etc/passwd 的写操作,不依赖任何外部框架,使用原生 libbpf 和 CO-RE。
4.1 环境检查
# 检查内核是否支持 BPF LSM
cat /sys/kernel/security/lsm | grep bpf
# 输出应包含 bpf,如:lockdown,capability,yama,selinux,bpf
# 检查 BTF 是否可用
ls /sys/kernel/btf/vmlinux
# 如果存在,说明内核已编译 BTF,CO-RE 可用
# 如果 lsm 输出中没有 bpf,需修改 GRUB 并重启
sudo sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="/&lsm=lockdown,yama,apparmor,bpf /' /etc/default/grub
sudo update-grub
sudo reboot4.2 内核态 eBPF 程序(file_protect.bpf.c)
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define EPERM 1
char LICENSE[] SEC("license") = "GPL";
// 使用 inode 号作为保护目标(/etc/passwd 的 inode 需预先获取)
const volatile u32 target_ino = 0;
SEC("lsm/file_open")
int BPF_PROG(restrict_file_open, struct file *file) {
struct inode *inode;
u32 ino;
int flags;
if (!target_ino)
return 0;
// 获取文件打开标志
flags = BPF_CORE_READ(file, f_flags);
// 只拦截写操作
if ((flags & 03) == 00) // O_RDONLY = 00
return 0;
// 获取 inode 号
inode = BPF_CORE_READ(file, f_inode);
ino = BPF_CORE_READ(inode, i_ino);
if (ino == target_ino) {
bpf_printk("BPF LSM: blocked write to protected inode %u\n", ino);
return -EPERM;
}
return 0;
}4.3 用户态加载器(loader.c)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <bpf/libbpf.h>
#include "file_protect.skel.h"
int main(int argc, char **argv) {
struct file_protect_bpf *skel;
struct stat st;
int err;
if (argc != 2) {
fprintf(stderr, "Usage: %s <file_to_protect>\n", argv[0]);
return 1;
}
if (stat(argv[1], &st) != 0) {
perror("stat");
return 1;
}
skel = file_protect_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
// 将目标 inode 号传递给 eBPF 程序
skel->rodata->target_ino = (u32)st.st_ino;
err = file_protect_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load BPF skeleton: %d\n", err);
goto cleanup;
}
err = file_protect_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
printf("BPF LSM loaded. Protecting inode %lu (%s)\n", st.st_ino, argv[1]);
printf("Try: echo test >> %s\n", argv[1]);
// 保持运行
while (1) {
sleep(1);
}
cleanup:
file_protect_bpf__destroy(skel);
return err;
}4.4 编译与测试
# 依赖:clang, llvm, libbpf-dev, linux-headers-$(uname -r)
# 使用 bpftool 生成 skeleton
cd /src
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 编译 eBPF 程序
clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 \
-c file_protect.bpf.c -o file_protect.bpf.o
# 生成 skeleton
bpftool gen skeleton file_protect.bpf.o > file_protect.skel.h
# 编译用户态程序
clang -g -O2 loader.c -o loader -lbpf
# 运行(需要 root)
sudo ./loader /etc/passwd
# 另起一个终端测试
$ echo "test" >> /etc/passwd
bash: /etc/passwd: Permission denied
# 查看内核日志
sudo cat /sys/kernel/debug/tracing/trace_pipe
# 输出:BPF LSM: blocked write to protected inode 1310734.5 实测边界说明
- 上述程序使用 inode 号而非路径字符串,因为内核态路径解析复杂且容易触发 verifier 的边界检查
bpf_printk的输出需要 root 权限查看,且在高频场景下会丢日志- 程序退出后,BPF LSM 程序自动卸载,保护立即失效。如需持久化,需将程序作为 systemd 服务运行
- 在容器环境中,inode 号可能在不同挂载命名空间中不同,需要针对每个命名空间分别计算
五、结论:可编程控制面的价值与未闭合边界
BPF LSM 的工程价值可以用一句话概括:它把内核安全策略从"配置"变成了"代码"。这个转变带来的不只是语法层面的灵活性,更是交付方式、版本控制、测试验证和故障排查范式的全面升级。安全团队可以用 Git 管理策略、用 CI/CD 发布策略、用单元测试验证策略、用 Prometheus 监控策略命中率——这些在 SELinux/AppArmor 时代都是难以实现的。
但BPF LSM 不是银弹。它的能力边界在内核源码中被明确划定:disabled hooks 列表限制了可挂载点,verifier 的返回值检查限制了程序行为,major LSM 的优先执行顺序限制了策略覆盖范围。这些边界不是缺陷,而是内核安全架构的必要设计。试图突破它们(例如通过内核模块补丁禁用 verifier 检查)会引入比解决的问题更大的风险。
对于生产环境的决策者来说,以下三条结论可以作为参考:
-
如果你已经有成熟的 SELinux/AppArmor 策略体系,BPF LSM 的最佳角色是"快速响应层"——用于临时拦截新披露的漏洞、A/B 测试实验性策略、或为特定高价值工作负载提供精细化控制。不要试图用 BPF LSM 重写现有的 MAC 策略。
-
如果你的环境需要频繁更新安全策略(例如多租户云原生平台、快速迭代的 CI/CD 流水线),BPF LSM 的动态加载能力可以显著缩短策略发布周期。但要建立完整的策略版本管理、回滚机制和审计日志体系,否则动态更新本身会成为新的风险源。
-
如果你的合规要求包括 eBPF 程序签名验证,需要升级到 Kernel 6.18+ 并建立证书管理流程。签名验证增加了安全边界,但也增加了运维复杂性——证书过期、密钥泄露、签名策略冲突都是已知的风险点。
最后,BPF LSM 的真正意义可能不在于它本身,而在于它代表的趋势:内核安全正在从"静态配置"走向"可编程控制面"。这个趋势与云原生、GitOps、可观测性的大方向是一致的。理解它的能力边界,比盲目拥抱它的新特性更重要。内核安全没有捷径,BPF LSM 只是让这条路走得更灵活了一些——但路还是那条路,坑也还是那些坑。