Linux BPF 文件系统 kfuncs 详解:BPF LSM 程序如何安全地访问文件数据
BPF LSM 程序在 LSM 钩子中被执行时,常常需要读取文件相关的内核数据(如扩展属性、fs-verity 摘要),而这些数据无法仅靠寄存器参数直接获得。本文基于内核文档 fs_kfuncs.rst 与对应实现 fs/bpf_fs_kfuncs.c、fs/verity/measure.c,完整讲解 BPF 文件系统 kfuncs 的清单、访问权限规则、防递归设计以及 BTF 注册与过滤机制,读完后你将能在 BPF LSM 程序中正确使用这些 kfuncs 并理解其底层安全约束。
1. 为什么 BPF LSM 程序需要文件系统 kfuncs
BPF LSM(Linux Security Module)程序挂载在内核的安全钩子(security_*)上执行。钩子本身传入的参数通常是 struct file *、struct dentry * 等指针,但钩子参数并不携带文件系统内部状态——例如某个文件当前的 xattr 值、某文件的 fs-verity 树根摘要。为了让 BPF LSM 程序能拿到这些“文件系统数据”,内核提供了一组专门的 BPF kfuncs。
原文明确列出的两个基础 kfunc 是:
bpf_get_file_xattr()—— 读取文件的扩展属性(xattr);bpf_get_fsverity_digest()—— 读取文件的 fs-verity 摘要。
2. 两条防递归规则(原文档核心)
文档指出了这些 kfuncs 必须遵守的两条规则,目的是避免 LSM 钩子与文件系统调用之间的无限递归:
- 只允许从 BPF LSM 程序调用。 即 kfunc 的注册类型被限定为
BPF_PROG_TYPE_LSM。 - 不能反向调用其他 LSM 钩子(
security_*)。 原文给出的例子是:bpf_get_file_xattr()没有使用vfs_getxattr(),因为后者会进入 LSM 钩子security_inode_getxattr。若 BPF LSM 程序在security_inode_getxattr里再触发同一个钩子,就会形成递归。
从源码看这两条规则都有对应实现证据:
规则 1 的证据 —— kfunc 注册时的类型过滤。 bpf_get_fsverity_digest() 的注册代码中,过滤器明确拒绝了非 LSM 程序(fs/verity/measure.c):
static int bpf_get_fsverity_digest_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
if (!btf_id_set8_contains(&fsverity_set_ids, kfunc_id))
return 0;
/* Only allow to attach from LSM hooks, to avoid recursion */
return prog->type != BPF_PROG_TYPE_LSM ? -EACCES : 0;
}
文件系统 kfuncs 的主体则在 bpf_fs_kfuncs_init() 中通过 register_btf_kfunc_id_set(BPF_PROG_TYPE_LSM, ...) 注册(fs/bpf_fs_kfuncs.c),并额外向 BPF_PROG_TYPE_STRUCT_OPS 暴露了一个受过滤器约束的子集(见第 6 节)。
规则 2 的证据 —— 调用不经过 LSM 钩子的 VFS 内部接口。 bpf_get_file_xattr() 的实现最终落到 __vfs_getxattr(),而不是会触发 security_inode_getxattr 的 vfs_getxattr()(fs/bpf_fs_kfuncs.c):
__bpf_kfunc int bpf_get_dentry_xattr(struct dentry *dentry, const char *name__str,
struct bpf_dynptr *value_p)
{
...
ret = bpf_xattr_read_permission(name__str, inode);
if (ret)
return ret;
return __vfs_getxattr(dentry, inode, name__str, value, value_len);
}
同理,设置/删除 xattr 的 kfunc 成功执行后,特意不调用 security_inode_post_setxattr / security_inode_post_removexattr,源码注释直接说明了原因——否则会回调到同一个 kfunc,造成死锁(fs/bpf_fs_kfuncs.c):
if (!ret) {
fsnotify_xattr(dentry);
/* This xattr is set by BPF LSM, so we do not call
* security_inode_post_setxattr. Otherwise, we would
* risk deadlocks by calling back to the same kfunc.
*/
}
3. bpf_get_file_xattr() 实现深析
bpf_get_file_xattr(struct file *file, const char *name__str, struct bpf_dynptr *value_p) 的语义:读取 file 的扩展属性 name__str,把值写入调用方传入的 bpf_dynptr 输出缓冲区,返回值是 xattr 值的长度(成功时)或负错误码。
实现上有三层安全/正确性设计:
(1)属性名白名单。 bpf_get_file_xattr() 只是从 file 取出 dentry 后委托给 bpf_get_dentry_xattr();真正的权限检查在 bpf_xattr_read_permission() 中完成(fs/bpf_fs_kfuncs.c):
static bool match_security_bpf_prefix(const char *name__str)
{
return !strncmp(name__str, XATTR_NAME_BPF_LSM, XATTR_NAME_BPF_LSM_LEN);
}
static int bpf_xattr_read_permission(const char *name, struct inode *inode)
{
if (!inode)
return -EINVAL;
/* Allow reading xattr with user. and security.bpf. prefix */
if (strncmp(name, XATTR_USER_PREFIX, XATTR_USER_PREFIX_LEN) &&
!match_security_bpf_prefix(name))
return -EPERM;
return inode_permission(&nop_mnt_idmap, inode, MAY_READ);
}
也就是说,只允许读取两类 xattr:
user.前缀的 xattr;security.bpf.前缀的 xattr。
security.bpf. 前缀由 UAPI 头文件统一定义,是 BPF LSM 的私有 xattr 命名空间(include/uapi/linux/xattr.h):
#define XATTR_BPF_LSM_SUFFIX "bpf."
#define XATTR_NAME_BPF_LSM (XATTR_SECURITY_PREFIX XATTR_BPF_LSM_SUFFIX)
在通过前缀检查后,还会用 inode_permission(&nop_mnt_idmap, inode, MAY_READ) 做一次 POSIX 权限检查,防止 BPF LSM 程序绕过 DAC 读取无权访问的文件 xattr。
(2)使用 bpf_dynptr 作为输出缓冲区。 值通过 struct bpf_dynptr *value_p 传回,代码内部用 __bpf_dynptr_size() 取缓冲区长度、__bpf_dynptr_data_rw() 取可写指针。相比裸 char * 参数,dynptr 让 verifier 能精确校验缓冲区大小,避免越界写入。
(3)BTF 标记为 KF_SLEEPABLE。 在 BTF ID 表中该 kfunc 被标记为 KF_SLEEPABLE(fs/bpf_fs_kfuncs.c):
BTF_KFUNCS_START(bpf_fs_kfunc_set_ids)
BTF_ID_FLAGS(func, bpf_get_task_exe_file, KF_ACQUIRE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_put_file, KF_RELEASE)
BTF_ID_FLAGS(func, bpf_path_d_path)
BTF_ID_FLAGS(func, bpf_get_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_get_file_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_set_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_real_data_inode, KF_SLEEPABLE | KF_RET_NULL)
#ifdef CONFIG_NET
BTF_ID_FLAGS(func, bpf_sock_read_xattr, KF_RCU)
#endif
BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
KF_SLEEPABLE 意味着它可以在可睡眠的 LSM 钩子上调用(xattr 读取可能陷入文件系统阻塞),verifier 会据此约束调用点。
4. bpf_get_fsverity_digest() 实现深析
bpf_get_fsverity_digest(struct file *file, const struct bpf_dynptr *digest_p) 用于把文件的 fs-verity 摘要读入 struct fsverity_digest 型 dynptr。完整实现见 fs/verity/measure.c,关键行为如下:
- 缓冲区校验:dynptr 大小必须不小于
sizeof(struct fsverity_digest),且返回的数据指针必须满足该结构的对齐要求,否则返回-EINVAL; - 非 verity 文件的处理:通过
fsverity_get_info(inode)获取 verity 元数据,若返回 NULL(该文件不是 verity 文件)则返回-ENODATA; - 摘要拷贝与填充:先确定 hash 算法(
vi->tree_params.hash_alg),检查 dynptr 中除去结构体头部的剩余空间是否足够容纳该算法的摘要,不够则返回-EOVERFLOW;随后填入digest_algorithm、digest_size,用memcpy拷贝vi->file_digest,多余空间用memset补零。
vi = fsverity_get_info(inode);
if (!vi)
return -ENODATA; /* not a verity file */
hash_alg = vi->tree_params.hash_alg;
out_digest_sz = dynptr_sz - sizeof(struct fsverity_digest);
if (out_digest_sz < hash_alg->digest_size)
return -EOVERFLOW;
arg->digest_algorithm = hash_alg - fsverity_hash_algs;
arg->digest_size = hash_alg->digest_size;
/* copy digest */
memcpy(arg->digest, vi->file_digest, hash_alg->digest_size);
错误码语义总结:0 成功;-EINVAL dynptr 太小或对齐不满足;-ENODATA 文件未启用 fs-verity;-EOVERFLOW 摘要缓冲区不足以容纳当前算法的摘要长度。
该 kfunc 在启动时通过 fsverity_init_bpf() 注册到 LSM 类型(fs/verity/measure.c),且整个代码块被 #ifdef CONFIG_BPF_SYSCALL 包裹,即依赖 CONFIG_BPF_SYSCALL。
5. 同一实现文件中的其余文件系统 kfuncs
文档主体只列举了两个 kfunc,但承载它们的 fs/bpf_fs_kfuncs.c 中还实现了同族的若干 kfuncs,它们与文档中两条防递归规则遵循同一设计哲学,值得一并了解:
(1)进程可执行文件相关
bpf_get_task_exe_file(struct task_struct *task):等价于直接在内核上下文调用get_task_exe_file(),返回mm_struct->exe_file的引用;BTF 标记为KF_ACQUIRE | KF_RET_NULL,即返回一个必须释放的引用;bpf_put_file(struct file *file):标记为KF_RELEASE,内部就是fput(file)。源码注释强调:不配对调用bpf_put_file()释放由bpf_get_task_exe_file()取得的引用,程序会被 verifier 直接拒绝。
(2)路径解析
bpf_path_d_path(const struct path *path, char *buf, size_t buf__sz):d_path()的更安全封装。d_path()返回的指针可能指向缓冲区中间(从右向左填充),该 kfunc 用memmove把结果左移到缓冲区起始处,并返回含 NUL 字符的路径总长度;失败返回负值。注释指明它是bpf_d_path()旧 helper 的安全替代,应优先使用。
(3)xattr 写入/删除(有副作用,仅限 LSM)
bpf_set_dentry_xattr()/bpf_remove_dentry_xattr():只允许操作security.bpf.前缀的 xattr(写入权限检查在bpf_xattr_write_permission(),fs/bpf_fs_kfuncs.c);- 另有
bpf_set_dentry_xattr_locked()/bpf_remove_dentry_xattr_locked()两个“已持锁”变体:设置/删除 xattr 需要独占dentry->d_inode锁,而不同 LSM 钩子进 BPF 程序时 d_inode 的加锁状态不同——部分钩子(如inode_post_setxattr、inode_setattr、inode_unlink等,清单见d_inode_locked_hooksBTF ID 集,fs/bpf_fs_kfuncs.c)已经持有锁,应调用_locked变体,否则会在重复inode_lock()上死锁;内核提供bpf_lsm_has_d_inode_locked()供 LSM 调用路径查询当前钩子属于哪一类(fs/bpf_fs_kfuncs.c)。
(4)虚拟文件系统上的 xattr 读取
bpf_cgroup_read_xattr(struct cgroup *, ...)(CONFIG_CGROUPS):读取 cgroup 节点在 cgroupfs 上的 xattr,只允许user.前缀,内部走kernfs_xattr_get();bpf_sock_read_xattr(struct socket *, ...)(CONFIG_NET):读取 socket 在 sockfs 上的 xattr,同样只允许user.前缀,内部走sock_read_xattr(),并标记KF_RCU。
(5)叠合文件系统的数据 inode
bpf_real_data_inode(struct file *file):对 union/overlay 文件系统上的常规文件,解析出真正承载数据的底层(upper/lower)inode 而非 overlay inode;对非常规文件或非叠合文件系统则返回文件直接关联的 inode。返回 NULL 用KF_RET_NULL标注。
6. 注册、过滤器与访问控制机制
这些 kfuncs 并非对全部 BPF 程序类型开放。fs/bpf_fs_kfuncs.c 展示了完整的访问控制逻辑:
/* Side-effecting kfuncs that stay exclusive to LSM programs. */
BTF_SET_START(bpf_fs_kfunc_lsm_only_ids)
BTF_ID(func, bpf_set_dentry_xattr)
BTF_ID(func, bpf_remove_dentry_xattr)
BTF_SET_END(bpf_fs_kfunc_lsm_only_ids)
static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id))
return 0;
if (prog->type == BPF_PROG_TYPE_LSM)
return 0;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
return -EACCES;
/* ->st_ops is unset during the cfg pass; enforced once it is set. */
if (!prog->aux->st_ops)
return 0;
if (bpf_prog_is_binfmt_misc_ops(prog) &&
!btf_id_set_contains(&bpf_fs_kfunc_lsm_only_ids, kfunc_id))
return 0;
return -EACCES;
}
从源码结构看,其访问策略可以归纳为:
- LSM 程序:整个
bpf_fs_kfunc_set_ids集合全部可用,且是文档所述“只允许从 BPF LSM 函数调用”的兜底场景; - struct_ops 程序:仅当
st_ops是 binfmt_misc 相关 ops(即CONFIG_BINFMT_MISC_BPF场景)时才放行,且有副作用的bpf_set_dentry_xattr/bpf_remove_dentry_xattr通过bpf_fs_kfunc_lsm_only_ids集合被明确排除在 struct_ops 之外; - 其他程序类型:返回
-EACCES,直接拒绝。
初始化入口 bpf_fs_kfuncs_init() 是一个 late_initcall,先向 BPF_PROG_TYPE_LSM 注册整套集合,若 CONFIG_BINFMT_MISC_BPF 使能,再向 BPF_PROG_TYPE_STRUCT_OPS 注册同一套集合(过滤器在 struct_ops 场景下生效)。
7. 实践要点小结
结合文档与源码,在 BPF LSM 程序中使用这组 kfuncs 时的要点:
| 事项 | 说明 | 依据 |
|---|---|---|
| 调用场景 | 仅 BPF_PROG_TYPE_LSM 程序(binfmt_misc struct_ops 除外,且仅只读子集) |
fs/bpf_fs_kfuncs.c |
| 可读 xattr 前缀 | 仅 user. 与 security.bpf. |
fs/bpf_fs_kfuncs.c |
| 可写/删 xattr 前缀 | 仅 security.bpf.,且仅 LSM 程序 |
fs/bpf_fs_kfuncs.c |
| 睡眠属性 | 读/写/删 xattr 均 KF_SLEEPABLE,只能在可睡眠钩子调用 |
fs/bpf_fs_kfuncs.c |
| 持锁变体选择 | 已持有 d_inode 锁的钩子必须用 _locked 变体 |
fs/bpf_fs_kfuncs.c |
| 文件引用配对 | bpf_get_task_exe_file() 取得的引用必须经 bpf_put_file() 释放 |
fs/bpf_fs_kfuncs.c |
| verity 摘要错误码 | -ENODATA 非 verity 文件,-EOVERFLOW 缓冲区不足 |
fs/verity/measure.c |
这些 kfuncs 的核心设计思想与原文档的两条规则一脉相承:通过“只在 LSM 类型中注册 + 内部调用绕开 security_* 钩子的 VFS 接口 + 副作用操作不回触 post 钩子”三道防线,确保 BPF LSM 程序在读取或修改文件系统数据时不会引发钩子递归或自锁,同时以 xattr 前缀白名单和 POSIX 权限检查防止越权访问。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.22 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python470
cherry-studio🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端TypeScript2.01 K146
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python50371
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go21643
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java35051