ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/14 17:28:21 拓冰建站 浏览量
Linux BPF 文件系统 kfuncs 详解:BPF LSM 程序如何安全地访问文件数据 Linux BPF 文件系统 kfuncs 详解BPF LSM 程序如何安全地访问文件数据【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linuxBPF 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 程序需要文件系统 kfuncsBPF LSMLinux Security Module程序挂载在内核的安全钩子security_*上执行。钩子本身传入的参数通常是struct file *、struct dentry *等指针但钩子参数并不携带文件系统内部状态——例如某个文件当前的 xattr 值、某文件的 fs-verity 树根摘要。为了让 BPF LSM 程序能拿到这些“文件系统数据”内核提供了一组专门的 BPF kfuncs。原文明确列出的两个基础 kfunc 是bpf_get_file_xattr()—— 读取文件的扩展属性xattrbpf_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.cstatic 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.cif (!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.cstatic 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); }也就是说只允许读取两类 xattruser.前缀的 xattrsecurity.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 能精确校验缓冲区大小避免越界写入。3BTF 标记为 KF_SLEEPABLE。在 BTF ID 表中该 kfunc 被标记为KF_SLEEPABLEfs/bpf_fs_kfuncs.cBTF_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成功-EINVALdynptr 太小或对齐不满足-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 的安全替代应优先使用。3xattr 写入/删除有副作用仅限 LSMbpf_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叠合文件系统的数据 inodebpf_real_data_inode(struct file *file)对 union/overlay 文件系统上的常规文件解析出真正承载数据的底层upper/lowerinode 而非 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.cverity 摘要错误码-ENODATA非 verity 文件-EOVERFLOW缓冲区不足fs/verity/measure.c这些 kfuncs 的核心设计思想与原文档的两条规则一脉相承通过“只在 LSM 类型中注册 内部调用绕开security_*钩子的 VFS 接口 副作用操作不回触 post 钩子”三道防线确保 BPF LSM 程序在读取或修改文件系统数据时不会引发钩子递归或自锁同时以 xattr 前缀白名单和 POSIX 权限检查防止越权访问。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考