首页
/ Linux BPF 文件系统 kfuncs 详解:BPF LSM 程序如何安全地访问文件数据

Linux BPF 文件系统 kfuncs 详解:BPF LSM 程序如何安全地访问文件数据

2026-09-13 12:24:00作者:段琳惟

BPF LSM 程序在 LSM 钩子中被执行时,常常需要读取文件相关的内核数据(如扩展属性、fs-verity 摘要),而这些数据无法仅靠寄存器参数直接获得。本文基于内核文档 fs_kfuncs.rst 与对应实现 fs/bpf_fs_kfuncs.cfs/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 钩子与文件系统调用之间的无限递归:

  1. 只允许从 BPF LSM 程序调用。 即 kfunc 的注册类型被限定为 BPF_PROG_TYPE_LSM
  2. 不能反向调用其他 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_getxattrvfs_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_SLEEPABLEfs/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,关键行为如下:

  1. 缓冲区校验:dynptr 大小必须不小于 sizeof(struct fsverity_digest),且返回的数据指针必须满足该结构的对齐要求,否则返回 -EINVAL
  2. 非 verity 文件的处理:通过 fsverity_get_info(inode) 获取 verity 元数据,若返回 NULL(该文件不是 verity 文件)则返回 -ENODATA
  3. 摘要拷贝与填充:先确定 hash 算法(vi->tree_params.hash_alg),检查 dynptr 中除去结构体头部的剩余空间是否足够容纳该算法的摘要,不够则返回 -EOVERFLOW;随后填入 digest_algorithmdigest_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_setxattrinode_setattrinode_unlink 等,清单见 d_inode_locked_hooks BTF 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;
}

从源码结构看,其访问策略可以归纳为:

  1. LSM 程序:整个 bpf_fs_kfunc_set_ids 集合全部可用,且是文档所述“只允许从 BPF LSM 函数调用”的兜底场景;
  2. 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 之外;
  3. 其他程序类型:返回 -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 权限检查防止越权访问。

登录后查看全文
热门项目推荐
相关项目推荐