在eBPF(LSM)钩子(如inode_rename)中安全构建struct dentry*完整路径的方法咨询
在eBPF(LSM)钩子(如inode_rename)中安全构建struct dentry*完整路径的方法咨询
老哥,我太懂你这个困境了——在LSM eBPF的inode_rename钩子里头拿重命名文件/目录的完整路径,直接遍历dentry->d_parent用bpf_probe_read_kernel系列函数,要么被eBPF验证器拦下来,要么跑起来出runtime错误,这在eBPF的严格内存约束下确实是个高频踩坑点。结合CO-RE和Cilium工具链的场景,给你几个安全可行的方案:
方案1:带深度限制的栈缓冲区遍历(最推荐,兼容大部分内核版本)
eBPF验证器最讨厌“可能无限循环”的逻辑,所以核心思路是给遍历父dentry的过程加一个固定的最大深度限制,让验证器能确定循环一定会终止;同时用bpf_core_read保证CO-RE的跨内核版本兼容性,避免直接访问结构体字段的偏移问题。
给你一段可参考的代码实现:
#include <linux/dcache.h> #include <bpf/bpf_core_read.h> #include <bpf/bpf_helpers.h> #define MAX_PATH_DEPTH 32 // 限制最大目录深度,避免无限循环 #define MAX_PATH_LEN 256 // 栈上缓冲区大小,别超过eBPF栈上限(一般512/1024字节) static inline int build_full_path(struct dentry *dentry, char *buf, size_t buf_len) { if (!dentry || !buf || buf_len < 2) { return -1; } // 从缓冲区末尾往前写,避免路径拼接的顺序问题 char *pos = buf + buf_len - 1; *pos = '\0'; // 先写终止符 int depth = 0; while (dentry && depth < MAX_PATH_DEPTH) { struct qstr d_name = {}; // 用bpf_core_read安全读取d_name,兼容不同内核的结构体偏移 if (bpf_core_read(&d_name, sizeof(d_name), &dentry->d_name)) { return -1; } if (d_name.len == 0) { break; } // 检查缓冲区剩余空间是否足够放下当前目录名 if ((pos - buf) <= d_name.len) { return -2; // 缓冲区不足,返回错误或截断(按需处理) } // 拷贝当前目录名到缓冲区 pos -= d_name.len; if (bpf_probe_read_kernel_str(pos, d_name.len + 1, d_name.name)) { return -1; } // 处理根目录的特殊情况(父dentry等于自身) struct dentry *parent; if (bpf_core_read(&parent, sizeof(parent), &dentry->d_parent)) { return -1; } if (parent == dentry) { break; } // 添加路径分隔符(除非已经到缓冲区开头) if (pos > buf) { pos--; *pos = '/'; } // 移动到父dentry,继续遍历 dentry = parent; depth++; } // 如果遍历完还是空,说明是根目录,补上前导/ if (pos == buf + buf_len - 1) { *(--pos) = '/'; } // 可选:把路径移到缓冲区开头,方便后续使用 __builtin_memmove(buf, pos, (buf + buf_len) - pos); return 0; }
关键注意点:
- 固定深度限制:
MAX_PATH_DEPTH是核心,验证器会因为这个确定循环不会无限跑,这是过验证器的关键。 - CO-RE兼容性:所有内核结构体字段的读取都用
bpf_core_read,别直接写dentry->d_name,不然跨内核版本会因为结构体偏移变化报错。 - 缓冲区安全:从后往前写路径,能更直观地控制剩余空间,避免溢出。
方案2:利用内核辅助函数(仅适合较新内核)
如果你的目标内核版本在5.15以上,可以试试内核提供的bpf_d_path辅助函数(部分内核可能需要开启特定配置),它能直接帮你把dentry转换成字符串路径。不过要注意:
- 这个辅助函数的可用性依赖内核配置和版本,需要用CO-RE的方式检查是否存在;
- 同样要在eBPF程序里限制路径长度,避免缓冲区溢出。
示例代码片段:
#include <linux/sched.h> #include <bpf/bpf_helpers.h> static inline int get_path_via_bpf_dpath(struct dentry *dentry, char *buf, size_t buf_len) { if (!dentry || !buf || buf_len < 1) { return -1; } // struct vfsmount需要从dentry的d_sb->s_root->d_mnt读取,这里简化处理 struct vfsmount *mnt = {}; if (bpf_core_read(&mnt, sizeof(mnt), &dentry->d_sb->s_root->d_mnt)) { return -1; } // 调用bpf_d_path,返回路径长度(包含终止符) int ret = bpf_d_path(&dentry->d_parent, mnt, buf, buf_len); return ret > 0 ? 0 : -1; }
不过这个方案的兼容性不如方案1,如果你要适配多内核版本,优先选方案1。
方案3:内核态收集片段+用户态拼接(适合超深路径场景)
如果你的场景需要支持超过32层的极深目录,或者不想在eBPF栈上占用太多内存,可以在eBPF程序里只收集每个dentry的名称和唯一标识(比如d_inode的编号),然后把这些片段发送到用户态,在用户态拼接成完整路径。
但这个方案会增加内核态到用户态的通信开销,适合对路径完整性要求极高、但性能压力不大的场景。
最后再提几个通用避坑点:
- 永远不要在eBPF程序里写无边界的循环,验证器绝对会拦你;
- 所有内核内存的读取都要用
bpf_probe_read_kernel或bpf_core_read,别直接指针访问; - 栈缓冲区大小别超过eBPF的栈上限(默认一般是512字节,部分内核可配置到1024)。
内容来源于stack exchange
相关产品推荐
相关产品推荐

